Tiêm phụ thuộc (DI) là một kỹ thuật trong đó một đối tượng nhận các phụ thuộc của nó từ bên ngoài thay vì tự tạo ra chúng. DI là một triển khai của nguyên lý IoC (Đảo ngược điều khiển) và là nền tảng của Dagger, Hilt và Swinject. Tiêm phụ thuộc làm giảm sự kết nối của mã, đơn giản hóa việc kiểm thử và làm cho kiến trúc trở nên linh hoạt. Trên Android, DI là tiêu chuẩn thông qua Dagger Hilt của Google; trên iOS, thông qua Swinject hoặc tiêm thủ công. Tìm hiểu thêm trong Hướng dẫn DI Android.
Những điểm chính
Tiêm phụ thuộc là một kỹ thuật trong đó một đối tượng nhận các phụ thuộc (dịch vụ, kho lưu trữ, cấu hình) thông qua hàm tạo, setter hoặc giao diện, thay vì tự tạo chúng bằng new. Mục tiêu của DI là giảm sự kết nối giữa các lớp. Nếu một lớp tự tạo phụ thuộc, nó bị ràng buộc chặt chẽ với các triển khai cụ thể, gây khó khăn cho việc kiểm thử và sửa đổi. Với DI, lớp làm việc với một sự trừu tượng (giao thức/giao diện) và triển khai cụ thể được cung cấp từ bên ngoài.
Ba cách tiêm — Tiêm qua hàm tạo (qua init/constructor), Tiêm qua setter (qua thuộc tính/setter), Tiêm qua giao diện (qua phương thức giao diện). Tiêm qua hàm tạo là cách ưa thích: các phụ thuộc hiển thị rõ ràng trong chữ ký và đối tượng luôn được tạo ở trạng thái hợp lệ. Tiêm qua setter được sử dụng cho các phụ thuộc tùy chọn với giá trị mặc định. Tiêm qua giao diện hiếm gặp, chủ yếu cho các container DI.
| Loại DI | Cách thức | Khi nào sử dụng | Ví dụ |
|---|---|---|---|
| Hàm tạo | Tham số bộ khởi tạo | Phụ thuộc bắt buộc | init(service: ServiceProtocol) |
| Thuộc tính | Thuộc tính lớp | Phụ thuộc tùy chọn | var service: ServiceProtocol? |
| Phương thức | Tham số phương thức | Phụ thuộc tạm thời | func doWork(with service: Service) |
Container DI — một thư viện quản lý việc tạo và vòng đời của các phụ thuộc. Container chứa các đăng ký kiểu (mỗi kiểu trừu tượng được ánh xạ tới một triển khai cụ thể) và một factory để tạo các đối tượng với phụ thuộc đã được giải quyết. Trên Android — Dagger/Hilt, trên iOS — Swinject, Needle, Dip. Container có thể quản lý phạm vi: singleton (một thể hiện cho toàn bộ ứng dụng), phạm vi tính năng (theo màn hình) hoặc một đối tượng mới cho mỗi yêu cầu.
Dagger Hilt là một lớp bọc quanh Dagger của Google, thư viện DI tiêu chuẩn cho Android. Hilt đơn giản hóa Dagger: loại bỏ việc tạo component thủ công, thêm @HiltAndroidApp, @AndroidEntryPoint và @Module. Hilt tích hợp với vòng đời Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver có thể nhận phụ thuộc thông qua chú thích. Việc sinh mã xảy ra tại thời điểm biên dịch — Dagger tạo ra các triển khai component, mang lại chi phí runtime bằng không.
// Lớp ứng dụng
@HiltAndroidApp
class MyApp : Application()
// Module — xác định cách tạo phụ thuộc
@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 nhận phụ thuộc qua hàm tạo
@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 kích hoạt DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Các component và phạm vi của Dagger — @Singleton (toàn bộ ứng dụng), @ActivityScoped (theo Activity), @FragmentScoped (theo Fragment), @ViewModelScoped (theo ViewModel). Việc chọn phạm vi quyết định thời gian sống của đối tượng. @Singleton — một thể hiện cho mỗi tiến trình, phù hợp cho OkHttpClient và cơ sở dữ liệu. @ActivityScoped — đối tượng sống trong suốt vòng đời của Activity, dành cho các phụ thuộc cấp màn hình. @ViewModelScoped — mới trong Hilt 2.45+, đối tượng sống trong suốt vòng đời của ViewModel, thuận tiện cho phạm vi coroutine.
Swinject là một framework DI mã nguồn mở phổ biến cho iOS. Swinject cung cấp Container, Assemblies và nhiều phạm vi khác nhau. Không giống như Dagger, Swinject hoạt động ở thời gian chạy — các phụ thuộc được giải quyết động mà không cần sinh mã. Điều này làm cho Swinject dễ cấu hình hơn, nhưng việc gỡ lỗi khó khăn hơn: lỗi phụ thuộc chưa được giải quyết chỉ xuất hiện ở thời gian chạy. Swinject hỗ trợ Tiêm qua hàm tạo, Tiêm qua thuộc tính và Tiêm qua phương thức.
import Swinject
// Assembly — một nhóm đăng ký
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 qua Tiêm qua hàm tạo
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
}
}
}
// Thiết lập DI trong AppDelegate hoặc App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// Tiêm qua thuộc tính cho UIKit ViewController
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Phạm vi của Swinject — .transient (đối tượng mới mỗi lần), .container (singleton cho mỗi container), .graph (mặc định — đối tượng được chia sẻ trong một đồ thị phụ thuộc). Đối với ứng dụng iOS, .container và .transient là đủ. Swinject cũng hỗ trợ Assembler — nhóm Assembly cho kiến trúc mô-đun. Để kiểm thử, Assembly được thay thế bằng MockAssembly, cho phép thay thế phụ thuộc mà không thay đổi mã sản xuất.
DI vs Service Locator — cả hai mẫu đều giải quyết vấn đề quản lý phụ thuộc, nhưng theo cách khác nhau. DI tiêm phụ thuộc vào đối tượng; Service Locator cung cấp một sổ đăng ký toàn cục mà từ đó đối tượng tự yêu cầu phụ thuộc. DI khai báo phụ thuộc một cách rõ ràng thông qua hàm tạo (hoặc setter). Service Locator ẩn phụ thuộc — chúng được yêu cầu bên trong phương thức, làm cho chữ ký kém thông tin hơn. DI dễ kiểm thử hơn: chỉ cần truyền mock vào hàm tạo. Service Locator yêu cầu thiết lập sổ đăng ký toàn cục cho mỗi bài kiểm thử.
| Đặc điểm | Tiêm phụ thuộc | Service Locator | Tiêm thủ công |
|---|---|---|---|
| Rõ ràng của phụ thuộc | Trong hàm tạo | Ẩn trong thân phương thức | Rõ ràng |
| Kiểm thử | Mock trong hàm tạo | Thiết lập Locator | Mock trong hàm tạo |
| Độ phức tạp thiết lập | Cần container DI | Sổ đăng ký toàn cục | Tạo thủ công |
| Chi phí runtime | Dagger — thời gian biên dịch | Tìm kiếm runtime | Không có |
DI vs tiêm thủ công — không có container DI, các phụ thuộc được tạo thủ công trong factory hoặc AppDelegate. Đối với 5-10 lớp, tiêm thủ công đơn giản hơn — không cần học Dagger hay Swinject. Đối với 50+ lớp, tiêm thủ công trở nên có vấn đề: hàm tạo với 5-6 tham số, thứ tự tạo phức tạp, trùng lặp mã. Container DI tự động hóa các quy trình này và cung cấp quản lý vòng đời rõ ràng. Tiêm thủ công không có container là một lựa chọn tốt cho các dự án nhỏ và nguyên mẫu.
Tiêm qua hàm tạo — tiêu chuẩn. Luôn sử dụng Tiêm qua hàm tạo cho các phụ thuộc bắt buộc. Điều này làm cho phụ thuộc rõ ràng và đối tượng luôn sẵn sàng hoạt động. Tiêm qua setter — chỉ cho các phụ thuộc tùy chọn (ví dụ: delegate hoặc listener). Tiêm qua giao diện — không sử dụng trừ khi bạn đang viết thư viện DI của riêng mình. Tiêm qua hàm tạo là cách duy nhất để đảm bảo rằng một đối tượng được tạo ở trạng thái hợp lệ.
Một lớp — một trách nhiệm. Nếu hàm tạo của một lớp yêu cầu 5+ tham số, lớp đó có khả năng vi phạm Nguyên lý Trách nhiệm Đơn nhất. Chia lớp thành nhiều lớp với ít phụ thuộc hơn. Dấu hiệu: nếu bạn đang viết một lớp ServiceManager với 6 dịch vụ khác nhau — đó là phản mẫu God Object. Trích xuất logic kinh doanh vào Use Cases (Interactors), mỗi cái với 1-2 phụ thuộc.
// ❌ Tệ: 6 phụ thuộc — 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
)
// ✅ Tốt: Use Cases với 1-2 phụ thuộc
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
Phạm vi và vòng đời — chọn phạm vi phù hợp cho mỗi phụ thuộc. Singleton: OkHttpClient, cơ sở dữ liệu, SharedPreferences. Phạm vi tính năng: kho lưu trữ, Use Cases (nếu chúng không có trạng thái). Transient: Value Objects, DateFormatter, trình phân tích. Lỗi phạm vi là một vấn đề phổ biến: một singleton lưu trữ trạng thái màn hình gây ra rò rỉ. Trên Android, @ActivityScoped của Hilt giải quyết vấn đề này; trong Swinject, sử dụng .container một cách thận trọng.
Các câu hỏi thường gặp
new tạo ra sự kết nối chặt chẽ giữa các lớp — bạn không thể thay đổi triển khai mà không sửa đổi mã. Việc kiểm thử trở nên khó khăn: bạn không thể tiêm mock thay vì dịch vụ thực. SRP bị vi phạm: lớp chịu trách nhiệm cho cả logic kinh doanh và tạo phụ thuộc. DI giải quyết những vấn đề này bằng cách tiêm phụ thuộc từ bên ngoài và làm việc với các trừu tượng.
Dagger Hilt là tiêu chuẩn từ Google, DI thời gian biên dịch với sinh mã, hiệu suất tốt hơn và tích hợp Jetpack. Koin là DI thời gian chạy, dễ cấu hình hơn, nhưng chậm hơn và có lỗi runtime. Chọn Hilt cho các dự án sản xuất. Koin phù hợp cho nguyên mẫu và ứng dụng nhỏ.
Không. Cho iOS có sẵn: Swinject (runtime, phổ biến), Needle (thời gian biên dịch từ Uber), Dip (nhẹ), Weaver (dựa trên Sourcery). Apple không cung cấp container DI tích hợp, nhưng tiêm thủ công qua init là thực hành tiêu chuẩn. Đối với SwiftUI, DI thủ công qua Environment hoặc @StateObject mà không cần thư viện bên ngoài thường là đủ.
Có. Tiêm thủ công qua hàm tạo là DI không cần framework. Service Locator là một giải pháp thay thế không cần framework. Factory và Factory Method cũng là các dạng của DI. Framework (Dagger, Swinject) tự động hóa việc đăng ký thường lệ và giải quyết phụ thuộc, nhưng đối với 10-20 lớp, DI thủ công là đủ.
DI là một kỹ thuật (khuôn mẫu) triển khai nguyên lý Đảo ngược điều khiển. Không giống như các mẫu GoF, DI không có cấu trúc 3-4 lớp nghiêm ngặt. DI là một cách tổ chức phụ thuộc, không phải mẫu thiết kế. Các container DI (Dagger, Swinject) là framework tự động hóa kỹ thuật này.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm