Dependency Injection: چیست، تزریق وابستگی‌ها در iOS و Android

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

Dependency Injection (DI، تزریق وابستگی‌ها) — تکنیکی که در آن شیء وابستگی‌های خود را از خارج دریافت می‌کند، نه اینکه خودش آن‌ها را ایجاد کند. DI پیاده‌سازی اصل IoC (Inversion of Control) است و پایه Dagger، Hilt و Swinject را تشکیل می‌دهد. تزریق وابستگی‌ها پیوستگی کد را کاهش می‌دهد، تست‌نویسی را ساده می‌کند و معماری را انعطاف‌پذیر می‌سازد. در Android DI استاندارد از طریق Dagger Hilt گوگل است، در iOS — از طریق Swinject یا تزریق دستی. بیشتر — در Android DI Guide.

نکات اصلی

  • Dependency Injection — وابستگی‌ها به شیء از خارج منتقل می‌شوند، نه در داخل ایجاد می‌شوند
  • Inversion of Control — DI اصل IoC را پیاده‌سازی می‌کند، کنترل جریان به کانتینر منتقل می‌شود
  • Dagger Hilt — استاندارد DI برای Android، مبتنی بر Dagger گوگل
  • Swinject — فریم‌ورک محبوب DI برای iOS و Swift
  • کاهش پیوستگی — کلاس به انتزاعات وابسته است، نه به پیاده‌سازی‌های مشخص

Dependency Injection چیست: ماهیت و انواع DI

Dependency Injection — تکنیکی که در آن شیء وابستگی‌ها (سرویس‌ها، مخازن، تنظیمات) را از طریق سازنده، setter یا رابط دریافت می‌کند، نه اینکه خودش آن‌ها را با new ایجاد کند. هدف DI کاهش پیوستگی (coupling) بین کلاس‌هاست. اگر کلاس خودش وابستگی‌ها را ایجاد کند، به پیاده‌سازی‌های مشخصی وابسته می‌شود که تست‌نویسی و تغییر را دشوار می‌کند. در DI کلاس با انتزاع (protocol/interface) کار می‌کند و پیاده‌سازی مشخص از خارج تأمین می‌شود.

سه روش تزریق — Constructor Injection (از طریق init/constructor)، Setter Injection (از طریق ویژگی/setter)، Interface Injection (از طریق متد رابط). Constructor Injection — روش ترجیحی: وابستگی‌ها به وضوح در امضا قابل مشاهده هستند، شیء همیشه در حالت معتبر ایجاد می‌شود. Setter Injection برای وابستگی‌های اختیاری با مقدار پیش‌فرض استفاده می‌شود. Interface Injection — به ندرت، عمدتاً برای کانتینرهای DI.

نوع DIروشزمان استفادهمثال
Constructorپارامترهای مقداردهیوابستگی‌های اجباریinit(service: ServiceProtocol)
Propertyویژگی کلاسوابستگی‌های اختیاریvar service: ServiceProtocol?
Methodپارامتر متدوابستگی‌های موقتfunc doWork(with service: Service)

کانتینر DI — کتابخانه‌ای که ایجاد و چرخه حیات وابستگی‌ها را مدیریت می‌کند. کانتینر شامل ثبت انواع (هر نوع انتزاعی با یک پیاده‌سازی مشخص مرتبط می‌شود) و کارخانه برای ایجاد اشیاء با وابستگی‌های حل‌شده است. در Android — Dagger/Hilt، در iOS — Swinject، Needle، Dip. کانتینر می‌تواند scope را مدیریت کند: singleton (یک نمونه برای کل برنامه)، scope ویژگی (برای یک صفحه) یا شیء جدید در هر درخواست.

Dagger Hilt: DI برای Android با تولید کد

Dagger Hilt — لایه‌ای بر روی Dagger گوگل، کتابخانه استاندارد DI برای Android. Hilt Dagger را ساده‌تر می‌کند: ایجاد دستی کامپوننت‌ها را حذف می‌کند، @HiltAndroidApp، @AndroidEntryPoint و @Module را اضافه می‌کند. Hilt با چرخه حیات Android ادغام می‌شود: ViewModel، Activity، Fragment، Service، BroadcastReceiver می‌توانند وابستگی‌ها را از طریق حاشیه‌نویسی دریافت کنند. تولید کد در مرحله کامپایل انجام می‌شود — Dagger پیاده‌سازی کامپوننت‌ها را تولید می‌کند که سربار runtime صفر دارد.

kotlin
// Application class
@HiltAndroidApp
class MyApp : Application()

// Module — تعیین می‌کند که چگونه وابستگی‌ها ایجاد شوند
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideApiService(client: OkHttpClient): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .client(client)
            .build()
            .create(ApiService::class.java)
    }
}

// ViewModel وابستگی را از طریق سازنده دریافت می‌کند
@HiltViewModel
class MainViewModel @Inject constructor(
    private val apiService: ApiService
) : ViewModel() {
    private val _state = MutableStateFlow(MainState.Loading)
    val state: StateFlow<MainState> = _state.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _state.value = MainState.Success(apiService.getData())
        }
    }
}

// Activity — @AndroidEntryPoint DI را فعال می‌کند
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

کامپوننت‌های Dagger و محدوده‌های دید — @Singleton (برای کل برنامه)، @ActivityScoped (برای Activity)، @FragmentScoped (برای Fragment)، @ViewModelScoped (برای ViewModel). انتخاب scope عمر شیء را تعیین می‌کند. @Singleton — یک نمونه برای فرآیند، مناسب برای OkHttpClient و پایگاه داده. @ActivityScoped — شیء تا زمانی که Activity زنده است زنده می‌ماند، برای وابستگی‌های صفحه. @ViewModelScoped — جدید در Hilt 2.45+، شیء تا زمانی که ViewModel زنده است زنده می‌ماند، که برای scopeهای کوروتین مناسب است.

Swinject: DI برای iOS در Swift

Swinject — فریم‌ورک محبوب DI برای iOS با کد متن‌باز. Swinject Container، Assemblies و scopeهای مختلف را فراهم می‌کند. برخلاف Dagger، Swinject در runtime کار می‌کند — وابستگی‌ها به صورت پویا بدون تولید کد حل می‌شوند. این باعث می‌شود Swinject در تنظیم ساده‌تر باشد، اما دیباگ را دشوار می‌کند: خطای وابستگی حل‌نشده فقط در runtime ظاهر می‌شود. Swinject از Constructor Injection، Property Injection و Method Injection پشتیبانی می‌کند.

swift
import Swinject

// Assembly — گروه ثبت‌ها
class NetworkAssembly: Assembly {
    func assemble(container: Container) {
        container.register(NetworkServiceProtocol.self) { _ in
            NetworkService()
        }.inObjectScope(.container) // singleton

        container.register(UserRepositoryProtocol.self) { r in
            UserRepository(
                networkService: r.resolve(NetworkServiceProtocol.self)!
            )
        }
    }
}

// ViewModel از طریق Constructor Injection
class ProfileViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    @Published var user: User?
    func loadUser() {
        repository.fetchUser { [weak self] user in
            self?.user = user
        }
    }
}

// تنظیم DI در AppDelegate یا App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

// Property Injection برای UIKit ViewController
container.register(UserViewController.self) { r in
    let vc = UserViewController()
    vc.viewModel = r.resolve(ProfileViewModel.self)
    return vc
}

scopeهای Swinject — .transient (هر بار شیء جدید)، .container (singleton برای کانتینر)، .graph (پیش‌فرض — شیء در یک گراف وابستگی به اشتراک گذاشته می‌شود). برای برنامه‌های iOS .container و .transient کافی هستند. Swinject همچنین از Assembler پشتیبانی می‌کند — گروه‌بندی Assembly برای معماری ماژولار. برای تست‌ها Assemblyها با MockAssembly جایگزین می‌شوند که امکان جایگزینی وابستگی‌ها را بدون تغییر کد تولیدی فراهم می‌کند.

مقایسه DI با Service Locator و تزریق دستی

DI vs Service Locator — هر دو الگو مشکل مدیریت وابستگی‌ها را حل می‌کنند، اما به روش‌های متفاوت. DI وابستگی‌ها را به شیء تزریق می‌کند، Service Locator یک ثبت جهانی فراهم می‌کند که شیء خودش وابستگی‌ها را از آن درخواست می‌کند. DI وابستگی‌ها را به وضوح از طریق سازنده (یا setter) اعلام می‌کند. Service Locator وابستگی‌ها را پنهان می‌کند — آن‌ها در داخل متد درخواست می‌شوند که امضا را کمتر informative می‌کند. DI راحت‌تر تست می‌شود: کافی است mock را در سازنده قرار دهید. Service Locator نیاز به تنظیم ثبت جهانی برای هر تست دارد.

ویژگیDependency InjectionService Locatorتزریق دستی
وضوح وابستگی‌هادر سازندهپنهان در بدنه متدآشکار
تست‌نویسیMock در سازندهتنظیم LocatorMock در سازنده
پیچیدگی تنظیمنیازمند کانتینر DIثبت جهانیایجاد دستی
سربار runtimeDagger — compile-timeRuntime lookupندارد

DI vs تزریق دستی — بدون کانتینر DI، وابستگی‌ها به صورت دستی در کارخانه‌ها یا AppDelegate ایجاد می‌شوند. برای 5-10 کلاس تزریق دستی ساده‌تر است — نیازی به یادگیری Dagger یا Swinject نیست. برای 50+ کلاس تزریق دستی مشکل‌ساز می‌شود: سازنده‌های با 5-6 پارامتر، ترتیب پیچیده ایجاد، تکرار کد. کانتینر DI این فرآیندها را خودکار می‌کند و چرخه حیات شفافی ارائه می‌دهد. تزریق دستی بدون کانتینر — انتخاب خوبی برای پروژه‌های کوچک و نمونه‌های اولیه است.

بهترین روش‌های Dependency Injection

Constructor Injection — استاندارد. همیشه از Constructor Injection برای وابستگی‌های اجباری استفاده کنید. این کار وابستگی‌ها را آشکار می‌کند و شیء همیشه آماده کار است. Setter Injection — فقط برای وابستگی‌های اختیاری (مثلاً delegate یا listener). Interface Injection — استفاده نکنید، مگر اینکه کتابخانه DI خود را بنویسید. Constructor Injection — تنها راه تضمین ایجاد شیء در حالت معتبر است.

یک کلاس — یک مسئولیت. اگر سازنده کلاس به 5+ پارامتر نیاز دارد، احتمالاً کلاس اصل Single Responsibility Principle را نقض می‌کند. کلاس را به چند کلاس با وابستگی‌های کمتر تقسیم کنید. نشانه: اگر کلاس ServiceManager با 6 سرویس مختلف می‌نویسید — این ضدالگو God Object است. منطق کسب‌وکار را به Use Cases (Interactors) منتقل کنید، هر کدام با 1-2 وابستگی.

kotlin
// ❌ بد: 6 وابستگی — God Object
class ProfileViewModel @Inject constructor(
    private val api: ApiService,
    private val db: Database,
    private val analytics: Analytics,
    private val prefs: Preferences,
    private val location: LocationProvider,
    private val notification: NotificationManager
)

// ✅ خوب: Use Cases با 1-2 وابستگی
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Scope و چرخه حیات — scope مناسب را برای هر وابستگی انتخاب کنید. Singleton‌ها: OkHttpClient، پایگاه داده، SharedPreferences. Scope ویژگی: مخازن، Use Cases (اگر حالت ندارند). Transient: Value Objects، DateFormatter، تجزیه‌گرها. خطای scope — مشکل رایج: singleton که حالت صفحه را ذخیره می‌کند منجر به نشت حافظه می‌شود. در Android Hilt @ActivityScoped این مشکل را حل می‌کند، در Swinject — .container با احتیاط.

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

اگر می‌توان با new ایجاد کرد، DI به چه درد می‌خورد؟

new یک پیوند سفت و سخت بین کلاس‌ها ایجاد می‌کند — بدون تغییر کد نمی‌توانید پیاده‌سازی را جایگزین کنید. تست‌نویسی دشوار است: نمی‌توانید به جای سرویس واقعی mock قرار دهید. SRP نقض می‌شود: کلاس هم مسئول منطق کسب‌وکار است و هم ایجاد وابستگی‌ها. DI این مشکلات را از طریق تزریق وابستگی‌ها از خارج و کار با انتزاعات حل می‌کند.

Dagger Hilt یا Koin — کدام را برای Android انتخاب کنیم؟

Dagger Hilt — استاندارد گوگل، DI کامپایل‌وقت با تولید کد، عملکرد بهتر و ادغام با Jetpack. Koin — DI زمان اجرا، تنظیم ساده‌تر اما کندتر با خطاهای runtime. برای پروژه‌های تولیدی Hilt را انتخاب کنید. Koin برای نمونه‌های اولیه و برنامه‌های کوچک مناسب است.

آیا Swinject تنها DI برای iOS است؟

خیر. برای iOS در دسترس هستند: Swinject (runtime، محبوب)، Needle (compile-time از Uber)، Dip (سبک)، Weaver (مبتنی بر Sourcery). Apple کانتینر DI داخلی ارائه نمی‌دهد، اما تزریق دستی از طریق init یک روش استاندارد است. برای SwiftUI اغلب DI دستی از طریق Environment یا @StateObject بدون کتابخانه‌های خارجی کافی است.

آیا می‌توان DI را بدون فریم‌ورک استفاده کرد؟

بله. تزریق دستی از طریق سازنده — DI بدون فریم‌ورک است. Service Locator — جایگزین بدون فریم‌ورک. کارخانه‌ها و Factory Method — همچنین نوعی DI. فریم‌ورک (Dagger, Swinject) ثبت معمول و حل وابستگی‌ها را خودکار می‌کند، اما برای 10-20 کلاس DI دستی کافی است.

DI یک الگو است یا اصل؟

DI — تکنیکی (الگو) است که اصل Inversion of Control را پیاده‌سازی می‌کند. برخلاف الگوهای GoF، DI ساختار سختگیرانه 3-4 کلاسی ندارد. DI روش سازماندهی وابستگی‌هاست، نه یک الگوی طراحی. کانتینرهای DI (Dagger, Swinject) فریم‌ورک‌هایی هستند که این تکنیک را خودکار می‌کنند.

خلاصه

  • DI — تکنیک تزریق وابستگی‌ها از خارج از طریق سازنده، setter یا متد
  • Dagger Hilt — استاندارد DI برای Android با تولید کد کامپایل‌وقت و @HiltViewModel
  • Swinject — DI زمان اجرا برای iOS با Container، Assembly و scopeها
  • Constructor Injection — روش ترجیحی برای وابستگی‌های اجباری
  • Scope — Singleton برای سرویس‌های بدون حالت، scope ویژگی برای وابستگی‌های صفحه
  • تست‌نویسی — DI جایگزینی وابستگی‌ها با mock را بدون تغییر کد ساده می‌کند

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

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

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

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