Bài thứ 2 và cũng là bài cuối cùng trong series bài giới thiệu sơ sơ về SOLID (xem lại bài 1)

1.   LSP – Liskov subsitituion principle

Phát biểu EN: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

Phát biểu VN: có thể hiểu nôm na là “trong các method có trỏ đến instance của 1 base class thì cũng có thể sử dụng các instance kế thừa từ base class mà không cần biết gì về child class”.

Ví dụ như một người nào đó cần lập gia phả trong dòng họ Nguyễn thì chỉ cần biết ông C là con của ông Nguyễn A thì có thể đưa vào gia phải, vậy thôi, không cần biết ông C này là do bà nào sinh ra hết. Trong ví dụ trên thì ta có method là add vào cây gia phả, và parameter là “một người dòng máu họ Nguyễn”. Vậy thì khi gọi hàm ta có thể truyền tham số là ông A, hoặc ông C đều được.

Ví dụ khác trong C#, ta có cấu trúc kế thừa như sau:

clip_image002

Implement của class Enquiry như sau:

clip_image004

Và ta có một method thực hiện công việc add customer vào db:

clip_image006

Khi chạy đoạn chương trình trên thì ta sẽ bị báo exception:

clip_image008

Như vậy tức là muốn chương trình chạy thành công thì người lập trình phải biết là có được phép gọi method Add của subclass kế thừa từ class Customer hay không, gọi method Add đó có bị raise exception hay không => trái với nguyên tắc Liskov.

Để fix lại chương trình trên tuân thủ LSP, ta implement như sau:

clip_image010

Define hai interface, một cho discount item (nhưng không add được vào db) và một cho item có thể add được vào db.

Implement class Enquiry kế thừa interface IDiscount theo đúng mục đích ban đầu của nó:

clip_image012

Còn class Customer thì implement cả hai interface IDiscount lần IDatabase vì ta cần thêm khách hàng vào DB để làm CRM (đại loại vậy, nêu CRM ra khè thiên hạ thôi ^_^) và cũng có thể discount tùy vào type của khách hàng:

clip_image014

Đoạn chương trình để add customer được viết lại như sau:

clip_image016

Như ta thấy trong hình thì compiler báo lỗi ở dòng Add (new Enquiry()) vì Enquiry nó không phải là instance có thể add vào DB, tuân theo LSP.

2.   ISP – Interface Segregation Principle

Phát biểu EN: “Clients should not be forced to implement interfaces they don’t use” hay "Many client specific interfaces are better than one general purpose interface.”

Phát biểu VN: hiểu nôm na là “một đối tượng chỉ nên thực hiện chức năng mà nó được thiết kế để thực hiện mà thôi, không cần biết về các chức năng khác”.

Ví dụ đại khái là trong công ty thì ông giám đốc được biết thông tin về lương hướng của các nhân viên nhưng thằng nhân viên thì không được biết lương hướng của những thằng nhân viên khác nhằm tránh sự đố kỵ, GATO😀.

Ví dụ cụ thể trong C#, ta có các interface có chức năng generate report như sau:

clip_image018

Và các concrete class implement các interface tương ứng:

clip_image020

Ví dụ ở trên đã tuân theo ISP, vậy các bạn suy luận tình huống ngược lại của ví dụ trên với trường hợp không tuân theo ISP là như thế nào? Đó là khi ta chỉ có một interface IReportBAL định nghĩa tất cả các action xuất ra report cho tất cả các role. Nói chung nguyên tắc này khác dễ hiểu nên mình không ví dụ nhiều😀

3.   DIP – Dependency Inversion Principle

Phát biểu EN: "High level modules should not depend upon low level modules. Rather, both should depend upon abstractions."

Phát biểu VN: đại loại là những thằng level cao thì không nên phụ thuộc vào những thằng có level thấp hơn mà thay vào đó nên nhờ vả những thằng đại diện cho thằng level thấp. Giả sử như trong đội bóng có Cristiano Ronaldo, anh này hiển nhiên là level cao rồi, anh này muốn mở cửa hàng bán sĩ condom, mà chườm cái mặt ra thì cũng hơi bị mất mặt nên thông qua người đại diện của anh này, làm việc với người đại diện của chủ hãng condom, như vậy là không ai biết mặt ai đứng sau giật dây hết, lỡ sau này ông đại diện của CR7 có làm không được việc, đổi người thì shop condom của anh CR7 vẫn làm ăn tốt, và ngược lại😀.

Ví dụ trong C#: mình có 1 shop bán máy tính, laptop blah blah, tạm thời ban đầu vốn ít bán Laptop, Tablet và Desktop thôi, mình viết một website hiển thị description của từng loại device như sau:

clip_image022

Đoạn chương trình trên vi phạm Open Closed Principle vì khi shop bán loại device khác, ví dụ như smartphone chẳng hạn, ta sẽ phải modify đoạn code, tức là không “closed for modification”, và rõ ràng nó cũng không “open for expansion”. Đoạn chương trình cũng vi phạm Dependency Inversion vì ở đây Laptop, Desktop… là low level module, trong khi ComputerShop là high level module. Để cho đúng thì ComputerShop sẽ chỉ nói chuyện với thằng nào làm “đại diện chung” cho cả Laptop, Desktop và Tablet thôi. Như vậy ta sẽ edit lại như sau:

clip_image024

Tah dah, giờ có muốn thêm bao nhiêu loại device mới vào cũng được, chỉ cần kế thừa interface IComputer là được.

4.   KẾT LUẬN

Trong lập trình thì việc thay đổi là không thể tránh khỏi. Thứ tốt nhất có thể làm đó là thiết kế kiến trúc phần mềm sao cho nó dễ dàng mở rộng, đáp ứng được yêu cầu nhất.

  • Khi khởi tạo một class, method hay bất cứ module nào (kể cả SQL Stored Procedure hay Function) nào, luôn nhớ đến nguyên tắc SRP, việc này giúp cho code của chúng ta dễ hiểu hơn, gọn gàng hơn và dễ test hơn
  • Trong thực tế thì không phải lúc nào cũng có thể tuân theo DIP, đôi khi ta vẫn phải có sự phụ thuộc vào các concrete class. Thứ duy nhất ta có thể làm tốt đó là hiểu rõ hệ thống, requirement cũng như environment, từ đó nhận thấy lúc nào là thích hơn để áp dụng DIP
  • Khi đã follow DIP và SRP thì gần như chúng ta cũng đã follow OCP luôn
  • Nhớ tạo các interface cho các trường hợp phức tạp hay có thể gây khó hiểu cho developer làm công việc implement bằng cách tuân theo ISP
  • Khi áp dụng tính kế thừa, nhớ lưu ý đến LSP

pic6