SOLID là tập hợp 5 nguyên lý quan trọng trong lập trình hướng đối tượng (Object-Oriented) giúp code dễ bảo trì, mở rộng, và giảm thiểu các lỗi phát sinh.
Trong bài viết này, chúng ta sẽ bàn về The Dependency Inversion Principle (DIP) - chữ "D" trong bộ nguyên lý.
Đầu tiên, cùng đi qua nội dung nguyên lý và phân tích từng khía cạnh của nó với ví dụ ở bài toán quen thuộc của ta. Sau đó, sẽ bàn tới một số nội dung mở rộng hơn như về cách áp dụng DIP ở cấp độ cao hơn và bàn về sự phụ thuộc.
Ví dụ
Vẫn là ví dụ quen thuộc: Tìm tài xế cho đơn hàng
Nội dung nguyên lý DIP
Nguyên lý DIP phát biểu rằng:
Module cấp cao không nên phụ thuộc vào bất cứ thứ gì trong các module cấp thấp. Cả hai nên phụ thuộc vào các trừu tượng.
Trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào trừu tượng.
Nguyên lý DIP thường được biết tới qua chữ "Depenpency" trong "Dependency Injection". Một số người nhầm nó với "Dependency Injection". Tuy nhiên, hai khái niệm này là khác nhau, trong khi DIP nói về chiều của sự phụ thuộc giữa các thành phần thì DI nói về sự cung cấp các phụ thuộc cho một đối tượng phần mềm khác.
DIP nói riêng và SOLID nói chung là các nguyên lý hướng tới cách thiết kế class chứ không phải module/component. Tuy nhiên, DIP vẫn đúng nếu áp dụng vào module bởi vì ta hoàn toàn có thể thiết kế ra những module đóng vai trò cấp cao, cấp thấp khác nhau trong hệ thống của mình.
Xuất phát với thói quen
Phát biểu của DIP khiến chúng ta nghĩ trực diện tới: trừu tượng
(abstraction) với cụ thể
(concrete), cấp cao
với cấp thấp
, và phụ thuộc
. Những thứ ngày mang ta tới một cách xây dựng class: Viết ra class mà sao cho nội dung của nó (bao gồm các field và method) chỉ liên quan tới những thứ như abstract class/interface, và tránh việc sử dụng trực tiếp concrete class.
Để minh họa điều này, cùng quay lại bài toán mẫu bên trên với cách triển khai Transaction Script. Trong đó đề cập tới thói quen khai báo lớp Service:

ShipmentController
sử dụng (phụ thuộc) vào interface chứ không vào ServiceImpl.Hình 1 cho thấy cách triển khai lớp service điển hình. Khi xây dựng tầng nghiệp vụ, các bước sau thường được thực hiện:
- Tạo một interface đại diện cho service, ở đây là
ShipmentService
. - Tạo một class thực thi interface trên, đặt tên là
ShipmentServiceImpl
. - Tạo một class controller nằm ở tầng Presentation,
ShipmentController
, trong đó khai báoShipmentService
interface.ShipmentServiceImpl
sẽ được "tiêm" vào khi khởi chạy ứng dụng.
Chúng ta sẽ đi tới phần nhận xét sau, giờ đi tiếp tới Service, đây mới là trọng tâm của ứng dụng vì nó chính là nơi nghiệp vụ được thể hiện.
ShipmentServiceImpl
cần giao tiếp với database để save/read các đối tượng Shipment
, Driver
. Do đó, ta khai báo hai repository - là các interface:

ShipmentServiceImpl
phụ thuộc vào các repository interface và các class domain.ShipmentServiceImpl
không chỉ phụ thuộc vào Shipment
entity và Driver
entity, mà còn vào cả hai repository: ShipmentRepository
và DriverRepository
.
Tới lúc này, có thể thấy thói quen này là tốt và áp dụng đúng DIP.
Ta đi tới một số nhận xét:
- Chiếu theo DIP, ta thấy các class sau áp dụng đúng:
ShipmentController
: vì class này chỉ phụ thuộc vàoShipmentService
interface. Interface này là trừu tượng và nó ở cấp cao hơn so với controller. Trong kiến trúc phân thành các lớp với các đường tròn đồng tâm, lớp bên trong sẽ có cấp cao hơn lớp bên dưới.ShipmentServiceImpl
: vì class này chỉ phụ thuộc vào các repository, là trừu tượng, và các domain entity (Shipment
,Driver
), là class có cấp cao hơn, nên cũng đạt chuẩn DIP.
- Ta áp dụng như vậy vì mục đích gì? Tôi thường gặp câu trả lời như sau:
- Vì
ShipmentService
khai báo dưới dạng interface nên sau này có thể có những Service Impl kiểu khác thực thi được. Có vẻ cũng hợp lý. Nhưng đã bao giờ bạn bổ sung class như vậy chưa hay bạn chỉnh sửa trực tiếpShipmentServiceImpl
? - Còn một lý do khác, API của một module nên là interface. Nhưng lại một lần nữa tôi thấy, gần như chưa bao giờ lớp logic business được đóng gói thành file module (file JAR trong Java).
- Vì hai lẽ trên, bạn hoàn toàn có thể sử dụng
ShipmentService
dưới dạng class trực tiếp trongShipmentController
mà không cần thông qua một interface (sự thật phũ phàng :D) - Trong
ShipmentServiceImpl
, những gì mà ta đang theo DIP chưa mang lại giá trị lớn doShipment
vàDriver
chỉ thuần túy là các data class (xem trong Transaction Script). Nghiệp vụ thay đổi nhiều nhất được "phơi bày" ra bởi pattern này. Do đó, giá trị DIP mong ta đạt được thì ta không với tới được do ta đã chọn đi một con đường khác.
- Vì
Để hiểu rõ hơn DIP và áp dụng được nhiều hơn, ta cần hiểu từng khía cạnh mà nguyên lý này đề cập.
Phụ thuộc là gì?
Thế nào là phụ thuộc? Một cách ngắn gọn: Code của ta đang sử dụng class, interface nào thì code đó phụ thuộc vào class, interface đó.
Xem xét đoạn code sau:
public class ShipmentServiceImpl implements ShipmentService {
private final ShipmentRepository shipmentRepository;
...
}
Trong đoạn code trên, ShipmentServiceImpl
phụ thuộc vào ShipmentService
interface và ShipmentRepository
interface. Nhưng ShipmentServiceImpl
không phụ thuộc vào class cụ thể thực thi ShipmentRepository
. Đây cũng chính là điểm mấu chốt của DIP. Cùng xem lại dưới dạng sơ đồ phụ thuộc để thấy rõ hơn:

Trong hình 3, ShipmentServiceImpl, ở trung tâm, phụ thuộc trực tiếp vào các class màu đỏ, không phụ thuộc vào các class màu xanh.
Nếu code của mình không nhắc tới thành phần nào thì mình không phụ thuộc trực tiếp vào thành phần đó.
Cấp cao, cấp thấp là gì?
Như đã đề cập trong phần thói quen bên trên, ta thường nghĩ tới abstract class và interface. Vậy những thành phần nào càng ở đầu của cây kế thừa thì càng ở cấp cao hơn. Điều này là đúng nhưng chưa đủ.
Hãy xem lại bài viết nhỏ về chính sách phần mềm. Trong đó đề cập rằng, phần mềm là tập hợp các tuyên bố về chính sách. Chính sách ở mức cao sẽ xa input/output hơn chính sách cấp thấp. Cùng làm rõ hơn, vì tôi biết bạn đang thấy khó hiểu :)
Ứng dụng của bạn sẽ thường chia thành 3 lớp:
- Presentation Layer: là nơi chìa ra API tiếp nhận yêu cầu người dùng; hiểu nôm na thì nó tương đương với package
controller
trong web service của bạn. - Business Layer: là nơi chứa logic nghiệp vụ, là trung tâm của ứng dụng. Nó tương đương với package
service
. Ngoài ra, Business cần lấy về các Domain Entity thông qua các Repository. Và, ta có packagerepository
chứa các interface này. Có thể bạn thấy lạ, nhưng với Repository Pattern thì các class này thuộc về Business Layer chứ không thuộc lớp Data Access layer bên dưới. Cụ thể hơn ta sẽ đề cập ở bài viết riêng cho nó. - Data Access Layer: là nơi giao tiếp với database, giúp in/out dữ liệu cho phần Business Layer. Tương đương với các class thực thi các Repository Interface. Các class này được Spring framework tự động tạo cho ta theo mặc định.
Như vậy, mô hình hóa các lớp này lên bạn sẽ thấy:

Trong hình 4 bạn có thể thấy một điều mà bình thường có thể bạn đã làm rất nhiều lần: Thay vì mũi tên phụ thuộc một chiều từ trên xuống (Presentation -> Business -> Data Access) thì Business Layer trở thành trái tim của ứng dụng. Sơ đồ này là hình ảnh rút gọn của kiến trúc Hexagonal, hay có tên khác là Ports And Adapters. Trong kiến trúc này, Business trở thành nơi đỉnh cao nhất, là nơi mà các lớp khác phải phụ thuộc vào. Kiến trúc đề cao vai trò trung tâm của nghiệp vụ.
Hình 4 cũng cho thấy Business Layer có cấp cao nhất vì nó xa người dùng và database hơn hai thành phần còn lại. Presentation gần người dùng nhất, còn Data Access layer gần database nhất nên hai thành phần này có thể coi có cùng cấp với nhau, và đều thấp hơn Business Layer.
Vậy DIP đã áp dụng đúng chưa? Ta thấy hoàn toàn đúng: Business Layer là cấp cao nhất không phụ thuộc vào 2 lớp thấp hơn, còn hai lớp thấp hơn đều phụ thuộc vào cấp cao hơn, là Business Layer.
Trong này cũng cho thấy, các class cấp cao cũng không hẳn là các abstract class hay interface. Shipment, Driver là ví dụ. Chúng là hai Domain Entity, có thể là concrete class và chúng thuộc Business Layer nên chúng có cấp cao. Thực tế thì chúng còn là những thành phần có cấp cao nhất.
(Do nội dung đã tương đối dài và nếu tiếp tục có thể gây khó hiểu nên tôi ngắt sang phần 2 ở đây)