- Published on
Android Dependency Injection: Kiến Thức Cần Biết
- Authors
- Name
- Dan Tech
- @dan_0xff
Dependency Injection là gì
Dependency (sự phục thuộc), Injection (sự truyền vào)
Dependency Injection là phương pháp mà lập trình viên dùng để quản lý sự phụ thuộc giữa các Class, Module trong chương trình thông qua phương thức Injection (truyền vào)
Injection (truyền vào) ở đây thông thường sẽ được truyền vào thông qua constructor, trong một vài trường hợp hiếm hoi ta có thể Inject thông qua các hàm set của Class (cách này có thể làm được nhưng không nên dùng vì nó là hack, làm vỡ đi kiến trúc vững chắc của dự án, sinh ra rất nhiều Unit test không đáng có).
Tại sao lại cần Dependency Injection?
Trước tiên cần phải làm rõ, Dependency Injection không phải là thứ gì đó quá cao siêu to lớn mà chỉ những bậc hiền nhân tinh hoa mới biết hoặc sử dụng. Dependency Injection nó chỉ là 1 phương pháp, 1 cách tiếp cận để quản lý kiến trúc code một cách tối ưu cho lập trình viên (trong việc viết Unit Test, xây dựng, bảo trì tính năng)
Bạn đã, đang và sẽ sử dụng Dependency Injection dù chưa bao giờ đọc đến bài viết này.
Ví dụ thực tế
Trong dự án ứng dụng TODO có 3 tính năng
- Đọc lên tất cả các TODO,
- Add 1 TODO
- Xóa 1 TODO
Để làm 3 tính năng này ta có thể tạo ra 3 Use Case Class
class LoadAllTODO() {
private val todoDataSource: DataSource = DataSourceImpl()
suspend fun run(): List {
return todoDataSource.allTodo()
}
}
class AddTODO() {
private val todoDataSource: DataSource = DataSourceImpl()
suspend fun run(todo: TODO) : Int {
return todoDataSource.add(todo)
}
}
class DeleteTODO() {
private val todoDataSource: DataSource = DataSourceImpl()
suspend fun run(todoId: Int): Int {
return todoDataSource.deleteTODO(todoId)
}
}
// Example Use Cases - Khóa học lập trình Android
Cách khai báo trên chạy được, nhưng nó có vấn đề hãy cùng mình phần tích:
- todoDataSource được khởi tạo ngay bên trong các Use Case class. Điều này khiến cho mã nguồn của ứng dụng trở nên đóng kín, việc triển khai test cho các Use Case class này cũng sẽ gặp trở ngại khi ta khó có thể toàn quyền kiểm soát dữ kiện todoDataSource trong Unit test. —> Bad practice, vì chúng ta sẽ không thể kiếm soát được từng Use Case một cách độc lập. (Chi tiết về Unit Test chúng ta sẽ cùng đào sâu ở chương sau)
- Giá trị của todoDataSource bị lặp đi lặp lại trong các Use Case một cách không đáng có. Ta có thể gom chúng lại thành 1 nguồn để đảm bảo tính đúng và đồng nhất của dữ liệu.
Cải thiện kiến trúc DI
class LoadAllTODO(private val todoDataSource: DataSource) {
suspend fun run(): List {
return todoDataSource.allTodo()
}
}
class AddTODO(private val todoDataSource: DataSource) {
suspend fun run(todo: TODO) : Int {
return todoDataSource.add(todo)
}
}
class DeleteTODO(private val todoDataSource: DataSource) {
suspend fun run(todoId: Int): Int {
return todoDataSource.deleteTODO(todoId)
}
}
// Cách tiếp cận mới
Lúc này các todoDataSource đã được truyền từ ngoài vào. Điều này đồng nghĩa, khi triển khai Unit Test, lập trình viên chỉ cần tập trung vào logic bên trong hàm run, và hoàn toàn có thể mock các logic được inject từ ngoài vào (chi tiết về Unit Test sẽ được chia sẻ.
Đồng thời trong chương trình thật, ta dễ dàng tạo và cung cấp (Create and Provide) instance của DataSource cho các Use Case. Đây là 1 best practice trong thiết kế lớp trong phần mềm.
Đây chính là Dependency Injection và lợi ích của nó.
3 trường phái Dependency Injection
Manually DI
Đây là trường phái quản lý Dependency không sử dụng thư viện hỗ trợ. Tất cả các Instance, Creator, Provider của các Class đều được tạo ra từ mã nguồn thủ công của lập trình viên. Điều này dễ dàng ở các dự án nhỏ, tuy nhiên theo thời gian dự án lớn dần sẽ gặp nhiều trở ngại trong việc quản lý vòng đời của các Instance được tạo ra. Đồng thời dự án cũng sẽ dính nhiều boilerplate code không đáng có.
class MainActivity : AppCompatActivity {
private val todoDataSource: DataSource = DataSource.INSTANCE
private val loadAllTODO = LoadAllTODO(todoDataSource)
private val addTODO = AddTODO(todoDataSource)
private val deleteTODO = DeleteTODO(todoDataSource)
private val viewModel: MainViewModel =
ViewModelProvider(
activity,
viewModelFactory {
MainViewModel(loadAllTODO, addTODO, deleteTODO)
})[XLauncherViewModel::class.java]
}
Thư viện hỗ trợ: No
Độ khó: 10^10 cho các dự án lớn
RunTime DI
Đây là hướng tiếp cận quản lý Dependency Injection bằng cách xác định và Inject các dependency vào trong 1 Class, Instance tại thời điểm chạy chương trình. Điều này có nghĩa là Instance được tạo ra không hề biết trước các phụ thuộc (Dependencies) của nó đã được tồn tại trước đó hay chưa. Trường hợp các phụ thuộc này chưa được tạo ra trước đó sẽ có một exception được throw ra và dẫn đến crash.
Thư viện hỗ trợ: Koin
Độ khó: 8
Compile time DI
Đây là hướng tiếp cận quản lý Dependency Injection ngược lại với Run Time DI, ở cách này các Class, Instance được xác định rõ ràng các Dependencies được tạo ra và Inject vào là ở đâu, thuộc Class nào. Các thông tin config này được thư viện hỗ trợ tạo ra sẵn lúc build dự án và tích hợp trong binary code của chương trình khi thực thi.
Thư viện hỗ trợ: Dagger, Hilt
Độ khó: 8