Dependency Injection (DI، تزریق وابستگیها) — تکنیکی که در آن شیء وابستگیهای خود را از خارج دریافت میکند، نه اینکه خودش آنها را ایجاد کند. DI پیادهسازی اصل IoC (Inversion of Control) است و پایه Dagger، Hilt و Swinject را تشکیل میدهد. تزریق وابستگیها پیوستگی کد را کاهش میدهد، تستنویسی را ساده میکند و معماری را انعطافپذیر میسازد. در Android DI استاندارد از طریق Dagger Hilt گوگل است، در iOS — از طریق Swinject یا تزریق دستی. بیشتر — در Android DI Guide.
نکات اصلی
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 — لایهای بر روی Dagger گوگل، کتابخانه استاندارد DI برای Android. Hilt Dagger را سادهتر میکند: ایجاد دستی کامپوننتها را حذف میکند، @HiltAndroidApp، @AndroidEntryPoint و @Module را اضافه میکند. Hilt با چرخه حیات Android ادغام میشود: ViewModel، Activity، Fragment، Service، BroadcastReceiver میتوانند وابستگیها را از طریق حاشیهنویسی دریافت کنند. تولید کد در مرحله کامپایل انجام میشود — Dagger پیادهسازی کامپوننتها را تولید میکند که سربار runtime صفر دارد.
// 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 با کد متنباز. Swinject Container، Assemblies و scopeهای مختلف را فراهم میکند. برخلاف Dagger، Swinject در runtime کار میکند — وابستگیها به صورت پویا بدون تولید کد حل میشوند. این باعث میشود Swinject در تنظیم سادهتر باشد، اما دیباگ را دشوار میکند: خطای وابستگی حلنشده فقط در runtime ظاهر میشود. Swinject از Constructor Injection، Property Injection و Method Injection پشتیبانی میکند.
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 vs Service Locator — هر دو الگو مشکل مدیریت وابستگیها را حل میکنند، اما به روشهای متفاوت. DI وابستگیها را به شیء تزریق میکند، Service Locator یک ثبت جهانی فراهم میکند که شیء خودش وابستگیها را از آن درخواست میکند. DI وابستگیها را به وضوح از طریق سازنده (یا setter) اعلام میکند. Service Locator وابستگیها را پنهان میکند — آنها در داخل متد درخواست میشوند که امضا را کمتر informative میکند. DI راحتتر تست میشود: کافی است mock را در سازنده قرار دهید. Service Locator نیاز به تنظیم ثبت جهانی برای هر تست دارد.
| ویژگی | Dependency Injection | Service Locator | تزریق دستی |
|---|---|---|---|
| وضوح وابستگیها | در سازنده | پنهان در بدنه متد | آشکار |
| تستنویسی | Mock در سازنده | تنظیم Locator | Mock در سازنده |
| پیچیدگی تنظیم | نیازمند کانتینر DI | ثبت جهانی | ایجاد دستی |
| سربار runtime | Dagger — compile-time | Runtime lookup | ندارد |
DI vs تزریق دستی — بدون کانتینر DI، وابستگیها به صورت دستی در کارخانهها یا AppDelegate ایجاد میشوند. برای 5-10 کلاس تزریق دستی سادهتر است — نیازی به یادگیری Dagger یا Swinject نیست. برای 50+ کلاس تزریق دستی مشکلساز میشود: سازندههای با 5-6 پارامتر، ترتیب پیچیده ایجاد، تکرار کد. کانتینر DI این فرآیندها را خودکار میکند و چرخه حیات شفافی ارائه میدهد. تزریق دستی بدون کانتینر — انتخاب خوبی برای پروژههای کوچک و نمونههای اولیه است.
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 وابستگی.
// ❌ بد: 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 یک پیوند سفت و سخت بین کلاسها ایجاد میکند — بدون تغییر کد نمیتوانید پیادهسازی را جایگزین کنید. تستنویسی دشوار است: نمیتوانید به جای سرویس واقعی mock قرار دهید. SRP نقض میشود: کلاس هم مسئول منطق کسبوکار است و هم ایجاد وابستگیها. DI این مشکلات را از طریق تزریق وابستگیها از خارج و کار با انتزاعات حل میکند.
Dagger Hilt — استاندارد گوگل، DI کامپایلوقت با تولید کد، عملکرد بهتر و ادغام با Jetpack. Koin — DI زمان اجرا، تنظیم سادهتر اما کندتر با خطاهای runtime. برای پروژههای تولیدی Hilt را انتخاب کنید. Koin برای نمونههای اولیه و برنامههای کوچک مناسب است.
خیر. برای iOS در دسترس هستند: Swinject (runtime، محبوب)، Needle (compile-time از Uber)، Dip (سبک)، Weaver (مبتنی بر Sourcery). Apple کانتینر DI داخلی ارائه نمیدهد، اما تزریق دستی از طریق init یک روش استاندارد است. برای SwiftUI اغلب DI دستی از طریق Environment یا @StateObject بدون کتابخانههای خارجی کافی است.
بله. تزریق دستی از طریق سازنده — DI بدون فریمورک است. Service Locator — جایگزین بدون فریمورک. کارخانهها و Factory Method — همچنین نوعی DI. فریمورک (Dagger, Swinject) ثبت معمول و حل وابستگیها را خودکار میکند، اما برای 10-20 کلاس DI دستی کافی است.
DI — تکنیکی (الگو) است که اصل Inversion of Control را پیادهسازی میکند. برخلاف الگوهای GoF، DI ساختار سختگیرانه 3-4 کلاسی ندارد. DI روش سازماندهی وابستگیهاست، نه یک الگوی طراحی. کانتینرهای DI (Dagger, Swinject) فریمورکهایی هستند که این تکنیک را خودکار میکنند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید