Nguyên tắc cơ bản trong thiết kế OOP: Bài mở đầu Generalization và Specification
SOLID - Bài 1: Single Responsibility Principle (SRP)
SOLID - Bài 2: Open Closed Principle (OCP)
SOLID - Bài 3: Liskov's Substitution Principle (LSP)
SOLID - Bài 4: Interface Segregation Principle (ISP)
SOLID - Bài 5: Dependency Inversion Principle (DIP)
Bây giờ, chúng ta sẽ tìm hiểu nguyên tắc cuối cùng trong 5 nguyên tắc SOLID - Dependency Inversion Principle (DIP). Tôi dịch là "Hoán đổi sự phụ thuộc".
Nội dung của nguyên tắc được phát biểu chia làm 2 mệnh đề như sau:
"Những module tầng cao (high-level module) không nên phụ thuộc vào những module tầng thấp (low-level module). Cả hai đều chỉ nên phụ thuộc vào định nghĩa trừu tượng của chúng. (1)
Những định nghĩa trừu tượng không nên phụ thuộc vào chi tiết cụ thể mà ngược lại, chi tiết nên phụ thuộc vào định nghĩa trừu tượng. (2)"
Khá khó hiểu ... nhưng không sao, bạn đừng lo vì chúng ta sẽ mổ xẻ phát biểu này từng phần một. Tôi xin lấy phần lớn giải thích và các ví dụ từ chính nguồn của tác giả Robert C. Martin về nguyên tắc này trong bài viết của ông. Lý do là vì sau khi tham khảo rất nhiều nguồn trên internet, tôi nhận ra chẳng có giải thích nào tốt hơn bài viết của ông cả.
Đầu tiên ta sẽ tìm hiểu thế nào là các tầng của phần mềm. Theo Grady Booch(1): "... tất cả những kiến trúc hướng đối tượng tốt được chia làm nhiều tầng rõ ràng, mỗi tầng cung cấp một tập hợp các services chuyên biệt." . Thông thường, một phần mềm được cấu trúc gồm 3 tầng chính:
Tầng trên cùng là Presentation layer, nơi trực tiếp giao tiếp với người dùng bao gồm nhận và hiển thị dữ liệu. Tầng thứ nhì, business layer, là tầng xử lý các nghiệp vụ trên dữ liệu. Tầng cuối cùng, data layer, là tầng giao tiếp với database để thực hiện truy xuất và lưu trữ dữ liệu. Và đó chính là ý nghĩa tầng cao tầng thấp của module.
Trong lược đồ trên, tầng trên cùng Presentation sẽ dùng service cung cấp ở tầng Business tiếp theo Business dùng service ở tầng Data thấp nhất. Trông có vẻ ổn, nhưng thực tế mô hình này có điểm bất lợi vì sự phụ thuộc bị trải dài từ trên xuống dưới trên cả 3 tầng. Xin lấy ví dụ về chương trình Copy của tác giả:
Giả sử có một chương trình dùng để liên tục copy các chữ nhập vào từ bàn phím rồi gửi cho máy in như sau:
Mô hình | Code |
---|---|
Giả sử nếu một lúc nào đó bạn muốn chương trình của bạn có thêm khả năng in ra file. Phương thức Copy này không thể sử dụng lại được. Do đó bạn có thể phải sửa lại phương thức Copy như sau:
Điều này sẽ trở nên rất phiền toái mỗi khi bạn muốn thêm vào một chức năng mới. Hơn nữa, việc sửa code làm bạn vi phạm nguyên tắc 2 Open/Closed. Để ý rằng nguyên nhân chính là do phương thức Copy phụ thuộc quá nhiều vào 2 phương thức của module tầng dưới: WriteToPrinter và ReadKeyBoard. Để giải quyết điều này, ta có thể tạo ra 2 lớp trừu tượng và Copy sẽ dùng 2 lớp này thay vì dùng trực tiếp.
Mô hình | Code |
---|---|
Rõ ràng theo lược đồ trên, bạn đã thấy sự phụ thuộc không còn từ tầng cao xuống tầng thấp mà đã được giải quyết bằng cách tạo ra tầng trung gian abstract. Sau đó tầng sự phụ thuộc bị đổi ngược bằng cách tầng dưới phải dựa theo định nghĩa ở tầng trên mà implement. Đó chính là tại sao nguyên tắc này gọi là Dependency Inversion (hoán đổi phụ thuộc).
Cuối cùng, đây là một nguyên tắc khó nhất để hiện thực và tuân thủ. Nếu như Open/Closed là nguyên tắc khái quát quan trọng nhất thì đây chính là nguyên tắc chi tiết quan trọng nhất trong 5 nguyên tắc đã nêu. Trong thực tế, để hiện thực nguyên tắc này một cách triệt để, đôi khi bạn phải dùng khá nhiều bước design "cồng kềnh", thậm chí dùng đến những library để hiện thực nó.
Enjoy your coding!
References
(1) Object Solutions, Grady Booch, Addision Wesley 1996, p54
(2) The Dependency Inversion Principle, Robert C. Martin, C++ Report, May 1996
References
(1) Object Solutions, Grady Booch, Addision Wesley 1996, p54
(2) The Dependency Inversion Principle, Robert C. Martin, C++ Report, May 1996