DIP (Dependency Inversion Principle) — nguyên tắc SOLID thứ năm xác định các quy tắc xây dựng phụ thuộc giữa các mô-đun: các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp, cả hai nên phụ thuộc vào các trừu tượng. Các trừu tượng không nên phụ thuộc vào chi tiết — các chi tiết nên phụ thuộc vào các trừu tượng. Nguyên tắc này, được Robert Martin mô tả trong Clean Architecture (2017), là nền tảng của kiến trúc kết nối lỏng lẻo. Theo cuốn sách này, nguyên tắc đảo ngược phụ thuộc loại bỏ các kết nối cứng nhắc giữa các tầng của ứng dụng.
Những điểm chính
DIP (Dependency Inversion Principle) là nguyên tắc đảo ngược phụ thuộc làm đảo ngược quan điểm truyền thống về hướng phụ thuộc giữa các mô-đun. Các mô-đun cấp cao (logic nghiệp vụ) không nên phụ thuộc trực tiếp vào các mô-đun cấp thấp (cơ sở dữ liệu, mạng, UI). Thay vào đó, cả hai cấp đều phụ thuộc vào các trừu tượng được định nghĩa trong mô-đun cấp cao.
Công thức chính thức của DIP bao gồm hai quy tắc: A — các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp, cả hai nên phụ thuộc vào các trừu tượng. B — các trừu tượng không nên phụ thuộc vào chi tiết, các chi tiết nên phụ thuộc vào các trừu tượng. Quy tắc thứ hai xuất phát từ quy tắc thứ nhất: nếu một trừu tượng phụ thuộc vào chi tiết, nó không thể là nền tảng ổn định cho một mô-đun cấp cao.
Không có DIP, một kiến trúc điển hình trông như thế này: BusinessLogic → DatabaseRepository — logic nghiệp vụ phụ thuộc trực tiếp vào một kho lưu trữ cụ thể. Với DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic không biết về sự tồn tại của DatabaseRepository, nó chỉ biết giao diện DatabaseService, được triển khai bên ngoài logic nghiệp vụ.
Đảo ngược có nghĩa là luồng điều khiển và luồng phụ thuộc đi theo hướng ngược nhau. Luồng điều khiển đi từ trên xuống dưới: UI → ViewModel → UseCase → Repository. Luồng phụ thuộc đi từ dưới lên trên: Repository triển khai một giao diện được định nghĩa trong UseCase. Repository (cấp thấp) phụ thuộc vào UseCase (cấp cao).
Sự đảo ngược này là sự khác biệt chính giữa DIP và sự phân tách tầng thông thường. Trong kiến trúc phân tầng truyền thống, mỗi tầng phụ thuộc vào tầng bên dưới. Trong kiến trúc với DIP, tất cả các tầng phụ thuộc vào các trừu tượng, trong khi việc triển khai các trừu tượng này nằm ở tầng cơ sở hạ tầng, được “cắm” vào các tầng trên thông qua cơ chế DI.
Cơ chế DIP được triển khai bằng cách định nghĩa các trừu tượng trong các mô-đun cấp cao và thực hiện chúng trong các mô-đun cấp thấp. Mô-đun cấp cao khai báo một giao diện cho chức năng nó cần. Mô-đun cấp thấp triển khai giao diện này. Việc kết nối (wiring) diễn ra tại gốc thành phần (composition root) của ứng dụng.
Quy trình đưa DIP vào mã hiện có: trích xuất một giao diện cho mô-đun cấp thấp, di chuyển giao diện này vào mô-đun cấp cao (hoặc một tầng trừu tượng riêng biệt), viết lại phụ thuộc của mô-đun cấp cao để sử dụng giao diện, yêu cầu mô-đun cấp thấp triển khai giao diện này. Sau các bước này, hướng phụ thuộc đã được đảo ngược.
DIP yêu cầu một cơ chế gốc thành phần — một điểm trong ứng dụng nơi tất cả các phụ thuộc được tạo và kết nối với nhau. Trong Android, đó là Application.get() hoặc thành phần Hilt, trong iOS — AppDelegate hoặc SceneDelegate. Gốc thành phần là nơi duy nhất mà mã biết về các triển khai cụ thể.
DIP tạo ra các ranh giới kiến trúc giữa các tầng của ứng dụng. Khi ViewModel phụ thuộc vào giao diện UserRepository, một ranh giới hình thành giữa tầng trình bày và tầng miền: ViewModel (trình bày) không biết dữ liệu đến từ đâu. Ranh giới này cho phép thay đổi triển khai UserRepository (Room → REST → Mock) mà không ảnh hưởng đến ViewModel. Càng nhiều ranh giới như vậy, ứng dụng càng chống chịu tốt với các thay đổi của framework và thư viện.
Trong kiến trúc Android được Google khuyến nghị, DIP được triển khai thông qua UseCase nằm trong tầng miền và phụ thuộc vào các giao diện Repository. RepositoryImpl nằm trong tầng dữ liệu và triển khai các giao diện này. Tầng trình bày (ViewModel) phụ thuộc vào UseCase. Hướng phụ thuộc đi từ trình bày đến miền, từ miền đến dữ liệu — nhưng không có tầng nào biết về các triển khai cụ thể của tầng khác.
DIP và DI thường bị nhầm lẫn, nhưng chúng là các khái niệm khác nhau. DIP là một nguyên tắc kiến trúc (LÀM GÌ: phụ thuộc vào các trừu tượng). DI là một mẫu triển khai (LÀM THẾ NÀO: truyền phụ thuộc qua hàm tạo). DIP trả lời câu hỏi “các mô-đun nên dựa trên cái gì?”, DI trả lời “các đối tượng nhận phụ thuộc của chúng như thế nào?”.
Dependency Injection là cách tiêm phụ thuộc vào một đối tượng thông qua hàm tạo, phương thức hoặc thuộc tính. Khi một lớp Kotlin nhận giao diện Repository qua hàm tạo của nó — đó là DI. Còn việc một lớp ViewModel phụ thuộc vào giao diện Repository thay vì một triển khai cụ thể của RoomRepository — đó là DIP. DI là công cụ, DIP là mục tiêu.
Bạn có thể tuân theo DIP mà không cần framework DI: kết nối thủ công các phụ thuộc trong gốc thành phần cũng là DI (DI thủ công). Bạn có thể sử dụng framework DI (Dagger, Hilt, Koin) vi phạm DIP: nếu ViewModel trực tiếp tạo đối tượng Repository qua new() — DIP bị vi phạm, ngay cả khi framework đã được cài đặt. DIP là một quyết định kiến trúc, DI là một chi tiết kỹ thuật.
Hãy xem ví dụ Android về việc áp dụng DIP cho tầng dữ liệu. Không có DIP, ViewModel trực tiếp tạo RoomDatabase và DAO. Với DIP — ViewModel phụ thuộc vào giao diện UserRepository, và triển khai cụ thể RoomUserRepository được cung cấp từ bên ngoài.
// Trừu tượng thuộc về tầng miền (cấp cao)
interface UserRepository {
fun getUser(id: Int): User
}
// Tầng miền chỉ phụ thuộc vào trừu tượng
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// Triển khai trong tầng dữ liệu phụ thuộc vào trừu tượng của tầng miền
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Gốc thành phần
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
Ví dụ iOS với Application Coordinator và giao thức điều hướng:
// Trừu tượng điều hướng trong tầng miền
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel phụ thuộc vào trừu tượng, không phụ thuộc vào UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (tầng UIKit) triển khai giao thức của tầng miền
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// Mã điều hướng UIKit
}
func navigateToLogin() {
// Mã điều hướng UIKit
}
}
Điểm chính: AuthViewModel (miền) không biết về sự tồn tại của AppCoordinator (UIKit). Nó chỉ biết giao thức AuthNavigation. Nếu UIKit được thay thế bằng SwiftUI vào ngày mai — AuthViewModel không yêu cầu thay đổi. DIP làm cho tầng miền độc lập với các framework và thư viện UI.
Hilt là công cụ DI tiêu chuẩn cho Android, được Google khuyến nghị. Nó được tích hợp trong Jetpack, hỗ trợ ViewModel, Fragment, Service và các thành phần Android khác. Hilt tự động hóa việc tạo gốc thành phần thông qua các chú thích @Module, @Provides, @Inject. Sử dụng Hilt không đảm bảo tuân thủ DIP — giao diện UserRepository phải được định nghĩa trong tầng miền, không phải trong tầng dữ liệu.
Koin là một framework DI nhẹ cho Kotlin không cần sinh mã hoặc xử lý chú thích. DSL của Koin (module, single, factory) dễ học hơn, nhưng việc kiểm tra phụ thuộc diễn ra trong thời gian chạy thay vì thời gian biên dịch. Koin phổ biến trong các dự án đa nền tảng (KMP) nhờ hỗ trợ iOS.
Dagger 2 là tiền thân của Hilt, vẫn được sử dụng trong các dự án lớn. Dagger tạo mã DI tại thời gian biên dịch, mang lại hiệu suất tối đa và chẩn đoán lỗi tại thời gian xây dựng. Hilt được xây dựng trên Dagger và cung cấp API đơn giản hóa. Đối với các dự án mới, Google khuyến nghị Hilt làm framework DI chính.
Các mô-đun DI nên tương ứng với các tầng kiến trúc và được phân chia thành DomainModule, DataModule, PresentationModule. DomainModule chỉ cung cấp các trừu tượng và UseCase. DataModule cung cấp các triển khai cho các trừu tượng. PresentationModule kết nối ViewModel với UseCase. Tổ chức này đảm bảo rằng tầng miền vẫn độc lập với các thư viện cơ sở hạ tầng.
Khi di chuyển giữa các framework DI (ví dụ: từ Koin sang Hilt), cấu trúc của DomainModule không thay đổi — chỉ các phương thức kết nối trong DataModule và PresentationModule thay đổi. DIP đảm bảo sự cô lập của logic miền, trong khi framework DI là một cơ chế kết nối kỹ thuật.
Các câu hỏi thường gặp
DIP cần thiết tại các ranh giới kiến trúc — giữa các tầng của ứng dụng (miền → dữ liệu, trình bày → miền). Trong cùng một tầng, DIP có thể là thừa. Ví dụ, một lớp tiện ích StringFormatter trong tầng miền không yêu cầu giao diện — nếu không có lý do để thay thế nó.
Không. DIP là một nguyên tắc: các mô-đun nên phụ thuộc vào các trừu tượng. DI là một mẫu: một đối tượng nhận phụ thuộc từ bên ngoài thay vì tự tạo chúng. DI là một cách để triển khai DIP, nhưng DIP có thể được tuân theo mà không cần DI (thông qua factory hoặc service locator). DI không có DIP là có thể nhưng không có giá trị kiến trúc.
Các giao diện thuộc về mô-đun sử dụng chúng, không phải mô-đun triển khai chúng. UserRepository được khai báo trong tầng miền và được triển khai trong tầng dữ liệu. Đây là quy tắc chính của DIP: chủ sở hữu của trừu tượng là người tiêu dùng, không phải người cung cấp triển khai.
DIP làm cho việc kiểm thử có thể trên các tầng cô lập. Một ViewModel phụ thuộc vào UserRepository (giao diện) có thể được kiểm thử với triển khai giả (mock) mà không cần cơ sở dữ liệu. Không có DIP, ViewModel sẽ phụ thuộc vào RoomUserRepository và yêu cầu thiết lập cơ sở dữ liệu cho mỗi bài kiểm thử. DIP + DI cung cấp sự cô lập hoàn toàn mô-đun trong quá trình kiểm thử.
Hilt là lựa chọn tiêu chuẩn cho các dự án Android, được Google khuyến nghị. Koin là một lựa chọn thay thế cho các dự án Kotlin Multiplatform. Dagger 2 dành cho các dự án hiện có nơi việc di chuyển sang Hilt không hợp lý. Việc chọn framework không loại bỏ nhu cầu tuân theo DIP ở cấp độ kiến trúc.
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