DIP: مبانی، وارونگی وابستگی‌ها در توسعه

نویسنده: IT Sectr منتشر شده: 2026-05-12 زمان مطالعه: 9 دقیقه

DIP (Dependency Inversion Principle) — پنجمین اصل SOLID است که قوانین ساخت وابستگی‌ها بین ماژول‌ها را تعریف می‌کند: ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند، هر دو باید به انتزاع‌ها وابسته باشند. انتزاع‌ها نباید به جزئیات وابسته باشند — جزئیات باید به انتزاع‌ها وابسته باشند. این اصل که توسط رابرت مارتین در Clean Architecture (2017) شرح داده شده، اساس معماری کم‌اتصال را تشکیل می‌دهد. بر اساس این کتاب، اصل وارونگی وابستگی‌ها اتصالات سفت و سخت بین لایه‌های برنامه را از بین می‌برد.

نکات اصلی

  • DIP — اصل وارونگی وابستگی‌ها، پنجمین اصل در SOLID، درباره مرزهای معماری
  • ماژول‌های سطح بالا نباید ماژول‌های سطح پایین را وارد کنند — فقط انتزاع‌ها
  • DIP ≠ DI: Dependency Inversion — اصل معماری، Dependency Injection — روش پیاده‌سازی آن
  • انتزاع‌ها متعلق به ماژول سطح بالا هستند و پیاده‌سازی‌ها متعلق به سطح پایین
  • DIP سلسله‌مراتب سنتی وابستگی‌ها را در معماری‌های چندلایه معکوس می‌کند

DIP (Dependency Inversion Principle) چیست؟

DIP (Dependency Inversion Principle) — اصل وارونگی وابستگی‌ها است که تصور سنتی از جهت وابستگی‌ها بین ماژول‌ها را معکوس می‌کند. ماژول‌های سطح بالا (منطق کسب‌وکار) نباید مستقیماً به ماژول‌های سطح پایین (پایگاه داده، شبکه، UI) وابسته باشند. در عوض، هر دو سطح به انتزاع‌هایی وابسته هستند که در ماژول سطح بالا تعریف می‌شوند.

فرمول‌بندی رسمی DIP شامل دو قانون است: الف — ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند، هر دو باید به انتزاع‌ها وابسته باشند. ب — انتزاع‌ها نباید به جزئیات وابسته باشند، جزئیات باید به انتزاع‌ها وابسته باشند. قانون دوم نتیجه قانون اول است: اگر انتزاع به جزئیات وابسته باشد، نمی‌تواند پایه پایدار برای ماژول سطح بالا باشد.

بدون DIP معماری معمولی به این شکل است: BusinessLogic → DatabaseRepository — منطق کسب‌وکار مستقیماً به یک مخزن خاص وابسته است. با DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic از وجود DatabaseRepository اطلاعی ندارد، فقط رابط DatabaseService را می‌شناسد که در خارج از منطق کسب‌وکار پیاده‌سازی می‌شود.

جهت وابستگی‌ها در DIP

وارونگی به این معنی است که جریان کنترل و جریان وابستگی‌ها در جهت‌های مخالف هدایت می‌شوند. جریان کنترل از بالا به پایین می‌رود: UI → ViewModel → UseCase → Repository. جریان وابستگی‌ها از پایین به بالا می‌رود: Repository رابط تعریف‌شده در UseCase را پیاده‌سازی می‌کند. Repository (سطح پایین) به UseCase (سطح بالا) وابسته است.

این وارونگی تفاوت کلیدی بین DIP و تقسیم‌بندی معمولی به لایه‌هاست. در معماری لایه‌ای سنتی، هر لایه به لایه پایین‌تر وابسته است. در معماری با DIP، همه لایه‌ها به انتزاع‌ها وابسته هستند و پیاده‌سازی این انتزاع‌ها در لایه زیرساخت قرار دارد که از طریق مکانیزم‌های DI به لایه‌های بالایی متصل می‌شود.

اصل وارونگی وابستگی‌ها چگونه کار می‌کند

مکانیزم DIP از طریق تعریف انتزاع‌ها در ماژول‌های سطح بالا و پیاده‌سازی آنها در ماژول‌های سطح پایین انجام می‌شود. ماژول سطح بالا رابطی را برای عملکرد مورد نیاز خود اعلام می‌کند. ماژول سطح پایین این رابط را پیاده‌سازی می‌کند. اتصال (wiring) در سطح ریشه ترکیب برنامه انجام می‌شود.

فرآیند اعمال DIP در کد موجود: رابط را برای ماژول سطح پایین استخراج کنید، این رابط را به ماژول سطح بالا (یا یک لایه انتزاعی جداگانه) منتقل کنید، وابستگی ماژول بالا را به رابط بازنویسی کنید، ماژول سطح پایین را مجبور به پیاده‌سازی این رابط کنید. پس از این مراحل جهت وابستگی به عکس تغییر کرد.

DIP نیاز به مکانیزم ریشه ترکیب دارد — نقطه‌ای در برنامه که در آن همه وابستگی‌ها ایجاد و به یکدیگر متصل می‌شوند. در Android این Application.get() یا کامپوننت Hilt است، در iOS — AppDelegate یا SceneDelegate. ریشه ترکیب تنها جایی است که کد از پیاده‌سازی‌های خاص اطلاع دارد.

جداسازی لایه‌ها از طریق DIP

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 (Dependency Injection)

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 — جزئیات فنی.

نمونه‌های DIP در توسعه موبایل

بیایید نمونه Android اعمال DIP به لایه داده را بررسی کنیم. بدون DIP، ViewModel مستقیماً RoomDatabase و DAO را ایجاد می‌کند. با DIP — ViewModel به رابط UserRepository وابسته است و پیاده‌سازی خاص RoomUserRepository از خارج تأمین می‌شود.

kotlin
// انتزاع متعلق به لایه 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 و پروتکل برای ناوبری:

swift
// انتزاع ناوبری در لایه 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 مستقل می‌کند.

ابزارهای DIP: Dagger, Hilt, Koin

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 بر اساس لایه‌ها

ماژول‌های DI باید با لایه‌های معماری مطابقت داشته باشند و به DomainModule, DataModule, PresentationModule تقسیم شوند. DomainModule فقط انتزاع‌ها و UseCase را ارائه می‌دهد. DataModule پیاده‌سازی‌هایی برای انتزاع‌ها ارائه می‌دهد. PresentationModule ViewModel را به UseCase متصل می‌کند. چنین سازماندهی تضمین می‌کند که لایه domain مستقل باقی می‌ماند.

در هنگام مهاجرت بین فریم‌ورک‌های DI (مثلاً از Koin به Hilt) ساختار DomainModule تغییر نمی‌کند — فقط روش‌های اتصال در DataModule و PresentationModule تغییر می‌کنند. DIP جداسازی منطق domain را تضمین می‌کند، و فریم‌ورک DI مکانیزم فنی اتصال است.

سؤالات متداول

آیا همیشه باید DIP را اعمال کرد؟

DIP در مرزهای معماری — بین لایه‌های برنامه (domain → data, presentation → domain) ضروری است. درون یک لایه، DIP ممکن است اضافی باشد. به عنوان مثال، کلاس کمکی StringFormatter درون لایه domain نیازی به رابط ندارد — اگر پیش‌نیازی برای جایگزینی آن وجود نداشته باشد.

آیا DIP همان تزریق وابستگی است؟

خیر. DIP — اصل: ماژول‌ها باید به انتزاع‌ها وابسته باشند. DI — الگو: شیء وابستگی‌ها را از خارج دریافت می‌کند، نه اینکه خودش آنها را ایجاد کند. DI — روش پیاده‌سازی DIP است، اما می‌توان بدون DI از DIP پیروی کرد (از طریق کارخانه‌ها یا مکان‌یاب سرویس). DI بدون DIP ممکن است اما ارزش معماری ندارد.

رابط‌ها برای DIP کجا تعریف شوند؟

رابط‌ها متعلق به ماژولی هستند که از آنها استفاده می‌کند، نه ماژولی که آنها را پیاده‌سازی می‌کند. UserRepository در لایه domain اعلام می‌شود و در لایه data پیاده‌سازی می‌شود. این قانون کلیدی DIP است: مالک انتزاع مصرف‌کننده است، نه تأمین‌کننده پیاده‌سازی.

DIP چگونه بر تست‌نویسی تأثیر می‌گذارد؟

DIP تست‌نویسی را ممکن می‌سازد در لایه‌های ایزوله. ViewModel که به UserRepository (رابط) وابسته است با پیاده‌سازی mock بدون پایگاه داده تست می‌شود. بدون DIP، ViewModel به RoomUserRepository وابسته بود و برای هر تست نیاز به راه‌اندازی پایگاه داده داشت. DIP + DI جداسازی کامل ماژول‌ها را در هنگام تست فراهم می‌کنند.

کدام فریم‌ورک DI را برای Android انتخاب کنیم؟

Hilt — انتخاب استاندارد برای پروژه‌های Android، توصیه‌شده توسط Google. Koin — جایگزین برای پروژه‌های Kotlin Multiplatform. Dagger 2 — برای پروژه‌های موجود که مهاجرت به Hilt غیرموجه است. انتخاب فریم‌ورک نیاز به رعایت DIP در سطح معماری را برطرف نمی‌کند.

خلاصه

  • DIP (Dependency Inversion Principle) — پنجمین اصل SOLID درباره مرزهای معماری از طریق انتزاع‌ها
  • ماژول‌های سطح بالا به ماژول‌های سطح پایین وابسته نیستند — هر دو به انتزاع‌ها وابسته هستند
  • DIP ≠ DI: اصل در مقابل الگوی پیاده‌سازی؛ DI — روش، DIP — هدف
  • انتزاع‌ها متعلق به مصرف‌کننده هستند (لایه domain)، نه تأمین‌کننده (لایه data)
  • ریشه ترکیب — تنها جایی در برنامه که وابستگی‌های خاص جمع‌آوری می‌شوند
  • Hilt, Koin, Dagger — ابزارهای DI که اتصال را خودکار می‌کنند اما جایگزین تصمیم معماری DIP نمی‌شوند
  • لایه domain ساخته‌شده بر اساس DIP از فریم‌ورک‌ها، UI و کتابخانه‌های زیرساخت مستقل می‌ماند

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید