Dependency Injection: nó là gì, tiêm phụ thuộc trong iOS và Android

Tác giả: IT Sectr Đã đăng: 2026-02-18 Thời gian đọc: 9 phút

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 — các phụ thuộc được truyền vào đối tượng từ bên ngoài, không được tạo bên trong
  • Đảo ngược điều khiển — DI triển khai nguyên lý IoC, luồng điều khiển được chuyển cho container
  • Dagger Hilt — tiêu chuẩn DI cho Android, dựa trên Dagger của Google
  • Swinject — framework DI phổ biến cho iOS và Swift
  • Giảm kết nối — một lớp phụ thuộc vào các trừu tượng, không phải triển khai cụ thể

Dependency Injection là gì: bản chất và các loại DI

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 DICách thứcKhi nào sử dụngVí dụ
Hàm tạoTham số bộ khởi tạoPhụ thuộc bắt buộcinit(service: ServiceProtocol)
Thuộc tínhThuộc tính lớpPhụ thuộc tùy chọnvar service: ServiceProtocol?
Phương thứcTham số phương thứcPhụ thuộc tạm thờifunc 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: DI cho Android với sinh mã

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.

kotlin
// 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: DI cho iOS bằng Swift

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.

swift
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.

So sánh DI với Service Locator và tiêm thủ công

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ểmTiêm phụ thuộcService LocatorTiêm thủ công
Rõ ràng của phụ thuộcTrong hàm tạoẨn trong thân phương thứcRõ ràng
Kiểm thửMock trong hàm tạoThiết lập LocatorMock trong hàm tạo
Độ phức tạp thiết lậpCần container DISổ đăng ký toàn cụcTạo thủ công
Chi phí runtimeDagger — thời gian biên dịchTìm kiếm runtimeKhô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.

Các thực hành tốt nhất về Tiêm phụ thuộc

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.

kotlin
// ❌ 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

Tại sao tôi cần DI nếu tôi có thể chỉ sử dụng new?

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 hay Koin — chọn cái nào cho Android?

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ỏ.

Swinject có phải là lựa chọn DI duy nhất cho iOS không?

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à đủ.

Tôi có thể sử dụng DI mà không cần framework không?

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 mẫu hay một nguyên 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

  • DI — một kỹ thuật tiêm phụ thuộc từ bên ngoài qua hàm tạo, setter hoặc phương thức
  • Dagger Hilt — tiêu chuẩn DI cho Android với sinh mã thời gian biên dịch và @HiltViewModel
  • Swinject — DI runtime cho iOS với Container, Assembly và phạm vi
  • Tiêm qua hàm tạo — cách ưa thích cho các phụ thuộc bắt buộc
  • Phạm vi — Singleton cho dịch vụ không trạng thái, phạm vi tính năng cho phụ thuộc cấp màn hình
  • Kiểm thử — DI đơn giản hóa việc thay thế phụ thuộc bằng mock mà không thay đổi mã

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.

Thảo luận dự án

Đọc thêm