DIP (Dependency Inversion Principle) — پنجمین اصل SOLID است که قوانین ساخت وابستگیها بین ماژولها را تعریف میکند: ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند، هر دو باید به انتزاعها وابسته باشند. انتزاعها نباید به جزئیات وابسته باشند — جزئیات باید به انتزاعها وابسته باشند. این اصل که توسط رابرت مارتین در Clean Architecture (2017) شرح داده شده، اساس معماری کماتصال را تشکیل میدهد. بر اساس این کتاب، اصل وارونگی وابستگیها اتصالات سفت و سخت بین لایههای برنامه را از بین میبرد.
نکات اصلی
DIP (Dependency Inversion Principle) — اصل وارونگی وابستگیها است که تصور سنتی از جهت وابستگیها بین ماژولها را معکوس میکند. ماژولهای سطح بالا (منطق کسبوکار) نباید مستقیماً به ماژولهای سطح پایین (پایگاه داده، شبکه، UI) وابسته باشند. در عوض، هر دو سطح به انتزاعهایی وابسته هستند که در ماژول سطح بالا تعریف میشوند.
فرمولبندی رسمی DIP شامل دو قانون است: الف — ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند، هر دو باید به انتزاعها وابسته باشند. ب — انتزاعها نباید به جزئیات وابسته باشند، جزئیات باید به انتزاعها وابسته باشند. قانون دوم نتیجه قانون اول است: اگر انتزاع به جزئیات وابسته باشد، نمیتواند پایه پایدار برای ماژول سطح بالا باشد.
بدون DIP معماری معمولی به این شکل است: BusinessLogic → DatabaseRepository — منطق کسبوکار مستقیماً به یک مخزن خاص وابسته است. با DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic از وجود DatabaseRepository اطلاعی ندارد، فقط رابط DatabaseService را میشناسد که در خارج از منطق کسبوکار پیادهسازی میشود.
وارونگی به این معنی است که جریان کنترل و جریان وابستگیها در جهتهای مخالف هدایت میشوند. جریان کنترل از بالا به پایین میرود: UI → ViewModel → UseCase → Repository. جریان وابستگیها از پایین به بالا میرود: Repository رابط تعریفشده در UseCase را پیادهسازی میکند. Repository (سطح پایین) به UseCase (سطح بالا) وابسته است.
این وارونگی تفاوت کلیدی بین DIP و تقسیمبندی معمولی به لایههاست. در معماری لایهای سنتی، هر لایه به لایه پایینتر وابسته است. در معماری با DIP، همه لایهها به انتزاعها وابسته هستند و پیادهسازی این انتزاعها در لایه زیرساخت قرار دارد که از طریق مکانیزمهای DI به لایههای بالایی متصل
میشود.
مکانیزم DIP از طریق تعریف انتزاعها در ماژولهای سطح بالا و پیادهسازی آنها در ماژولهای سطح پایین انجام میشود. ماژول سطح بالا رابطی را برای عملکرد مورد نیاز خود اعلام میکند. ماژول سطح پایین این رابط را پیادهسازی میکند. اتصال (wiring) در سطح ریشه ترکیب برنامه انجام میشود.
فرآیند اعمال DIP در کد موجود: رابط را برای ماژول سطح پایین استخراج کنید، این رابط را به ماژول سطح بالا (یا یک لایه انتزاعی جداگانه) منتقل کنید، وابستگی ماژول بالا را به رابط بازنویسی کنید، ماژول سطح پایین را مجبور به پیادهسازی این رابط کنید. پس از این مراحل جهت وابستگی به عکس تغییر کرد.
DIP نیاز به مکانیزم ریشه ترکیب دارد — نقطهای در برنامه که در آن همه وابستگیها ایجاد و به یکدیگر متصل میشوند. در Android این Application.get() یا کامپوننت Hilt است، در iOS — AppDelegate یا SceneDelegate. ریشه ترکیب تنها جایی است که کد از پیادهسازیهای خاص اطلاع دارد.
DIP مرزهای معماری را تشکیل میدهد بین لایههای برنامه. وقتی ViewModel به رابط UserRepository وابسته است، بین لایههای presentation و domain مرزی ایجاد میشود: ViewModel (presentation) نمیداند دادهها از کجا میآیند. این مرز امکان تغییر پیادهسازی UserRepository (Room → REST → Mock) را بدون تأثیر بر ViewModel فراهم میکند. هرچه چنین مرزهایی بیشتر باشد، برنامه در برابر تغییرات فریمورکها و کتابخانهها مقاومتر است.
در معماری Android توصیهشده توسط Google، DIP از طریق UseCase که در لایه domain قرار دارند و به رابطهای Repository وابسته هستند پیادهسازی میشود. RepositoryImpl در لایه data قرار دارند و این رابطها را پیادهسازی میکنند. لایه presentation (ViewModel) به UseCase وابسته است. جهت وابستگیها از presentation به domain، از domain به data میرود — اما هیچ لایهای از پیادهسازیهای خاص لایه دیگر اطلاع ندارد.
DIP و DI اغلب اشتباه گرفته میشوند، اما مفاهیم متفاوتی هستند. DIP — اصل معماری (چه کاری باید کرد: به انتزاعها وابسته بود). DI — الگوی پیادهسازی (چگونه این کار را کرد: انتقال وابستگیها از طریق سازنده). DIP به این سؤال پاسخ میدهد که «ماژولها باید بر چه اساسی باشند؟»، DI — «اشیاء چگونه وابستگیهای خود را دریافت میکنند؟».
Dependency Injection — روش تزریق وابستگیها به یک شیء از طریق سازنده، متد یا ویژگی. وقتی در کلاس Kotlin رابط Repository از طریق سازنده منتقل میشود — این DI است. و اینکه کلاس ViewModel به رابط Repository وابسته است نه به پیادهسازی خاص RoomRepository — این DIP است. DI — ابزار، DIP — هدف.
میتوان بدون فریمورک DI از DIP پیروی کرد: اتصال دستی وابستگیها در ریشه ترکیب نیز DI است (manual DI). میتوان از فریمورک DI (Dagger, Hilt, Koin) استفاده کرد در حالی که DIP نقض میشود: اگر ViewModel مستقیماً شیء Repository را از طریق new() ایجاد کند — DIP نقض شده، حتی اگر فریمورک نصب شده باشد. DIP — تصمیم معماری، DI — جزئیات فنی.
بیایید نمونه Android اعمال DIP به لایه داده را بررسی کنیم. بدون DIP، ViewModel مستقیماً RoomDatabase و DAO را ایجاد میکند. با DIP — ViewModel به رابط UserRepository وابسته است و پیادهسازی خاص RoomUserRepository از خارج تأمین میشود.
// انتزاع متعلق به لایه domain است (سطح بالا)
interface UserRepository {
fun getUser(id: Int): User
}
// لایه domain فقط به انتزاع وابسته است
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// پیادهسازی در لایه data به انتزاع لایه domain وابسته است
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// ریشه ترکیب
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
نمونه iOS با Application Coordinator و پروتکل برای ناوبری:
// انتزاع ناوبری در لایه domain
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel به انتزاع وابسته است، نه به UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (لایه UIKit) پروتکل لایه domain را پیادهسازی میکند
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// کد ناوبری UIKit
}
func navigateToLogin() {
// کد ناوبری UIKit
}
}
نکته کلیدی: AuthViewModel (domain) از وجود AppCoordinator (UIKit) اطلاعی ندارد. او فقط پروتکل AuthNavigation را میشناسد. اگر فردا UIKit با SwiftUI جایگزین شود — AuthViewModel نیازی به تغییر ندارد. DIP لایه domain را از فریمورکها و کتابخانههای UI مستقل میکند.
Hilt — ابزار استاندارد DI برای Android، توصیهشده توسط Google. در Jetpack ادغام شده، از ViewModel, Fragment, Service و سایر کامپوننتهای Android پشتیبانی میکند. Hilt ایجاد ریشه ترکیب را از طریق annotationهای @Module, @Provides, @Inject خودکار میکند. استفاده از Hilt تضمینکننده رعایت DIP نیست — رابط UserRepository باید در لایه domain تعریف شود، نه در لایه data.
Koin — فریمورک DI سبک برای Kotlin بدون تولید کد و پردازشگر annotation. DSL کویین (module, single, factory) برای یادگیری سادهتر است، اما بررسی وابستگیها در زمان اجرا انجام میشود، نه در زمان کامپایل. Koin در پروژههای چندسکویی (KMP) به دلیل پشتیبانی از iOS محبوب است.
Dagger 2 — predecessor هیلت، هنوز در پروژههای بزرگ استفاده میشود. Dagger کد DI را در زمان کامپایل تولید میکند که حداکثر عملکرد و تشخیص خطا در مرحله ساخت را فراهم میکند. Hilt بر روی Dagger ساخته شده و API سادهتری ارائه میدهد. برای پروژههای جدید Google Hilt را به عنوان فریمورک اصلی DI توصیه میکند.
ماژولهای DI باید با لایههای معماری مطابقت داشته باشند و به DomainModule, DataModule, PresentationModule تقسیم شوند. DomainModule فقط انتزاعها و UseCase را ارائه میدهد. DataModule پیادهسازیهایی برای انتزاعها ارائه میدهد. PresentationModule ViewModel را به UseCase متصل میکند. چنین سازماندهی تضمین میکند که لایه domain مستقل باقی میماند.
در هنگام مهاجرت بین فریمورکهای DI (مثلاً از Koin به Hilt) ساختار DomainModule تغییر نمیکند — فقط روشهای اتصال در DataModule و PresentationModule تغییر میکنند. DIP جداسازی منطق domain را تضمین میکند، و فریمورک DI مکانیزم فنی اتصال است.
سؤالات متداول
DIP در مرزهای معماری — بین لایههای برنامه (domain → data, presentation → domain) ضروری است. درون یک لایه، DIP ممکن است اضافی باشد. به عنوان مثال، کلاس کمکی StringFormatter درون لایه domain نیازی به رابط ندارد — اگر پیشنیازی برای جایگزینی آن وجود نداشته باشد.
خیر. DIP — اصل: ماژولها باید به انتزاعها وابسته باشند. DI — الگو: شیء وابستگیها را از خارج دریافت میکند، نه اینکه خودش آنها را ایجاد کند. DI — روش پیادهسازی DIP است، اما میتوان بدون DI از DIP پیروی کرد (از طریق کارخانهها یا مکانیاب سرویس). DI بدون DIP ممکن است اما ارزش معماری ندارد.
رابطها متعلق به ماژولی هستند که از آنها استفاده میکند، نه ماژولی که آنها را پیادهسازی میکند. UserRepository در لایه domain اعلام میشود و در لایه data پیادهسازی میشود. این قانون کلیدی DIP است: مالک انتزاع مصرفکننده است، نه تأمینکننده پیادهسازی.
DIP تستنویسی را ممکن میسازد در لایههای ایزوله. ViewModel که به UserRepository (رابط) وابسته است با پیادهسازی mock بدون پایگاه داده تست میشود. بدون DIP، ViewModel به RoomUserRepository وابسته بود و برای هر تست نیاز به راهاندازی پایگاه داده داشت. DIP + DI جداسازی کامل ماژولها را در هنگام تست فراهم میکنند.
Hilt — انتخاب استاندارد برای پروژههای Android، توصیهشده توسط Google. Koin — جایگزین برای پروژههای Kotlin Multiplatform. Dagger 2 — برای پروژههای موجود که مهاجرت به Hilt غیرموجه است. انتخاب فریمورک نیاز به رعایت DIP در سطح معماری را برطرف نمیکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید