Chào các bạn, mình lại gặp nhau trong bài viết số 3 nói về nguyên tắc SOLID. Chúng ta đã đi qua được 3 nguyên tắc đầu trong SOLID.
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion
Bạn đã áp dụng được chưa? Nếu chưa thì đọc lại 2 bài viết của mình nhé. Ngoài ra bạn có thể tìm thêm những ví dụ theo các keyword trong bài viết của mình để hiểu rõ về SOLID. Nếu có bất kì khó khăn hay thắc mắc trong quá trình áp dụng, để lại comment bên dưới mỗi bài viết. Mình sẽ cùng nhau giải quyết 😀 . Ở bài viết này mình sẽ giới thiệu nốt 2 nguyên tắc còn lại nhé. Nào cũng bắt đầu thôi
Interface segregation principle (ISP)
Nguyên tắc này nó về bất lợi của một “fat” interface. Phát biểu của nguyên tắc này như sau:
Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể.
Có lẽ phát biểu đã nói rất rõ về nguyên tắc này. Trước khi vào ví dụ chi tiết thì chúng ta tản mạn một tí.
Interface
Interface là một lớp rỗng chỉ chứa khai báo về tên phương thức không có khai báo về thuộc tính hay thứ gì khác và các phương thức này cũng là rỗng. Vì vậy bất kì các class nào kế thừa interface đều bắt buộc phải định nghĩa hết những phương thức đã khai báo trong interface.
Sự khác nhau giữa kế thừa một class và kế thừa interface là: kế thừa class nhằm sử dụng lại những thứ có sẳn từ lớp cơ sở còn những lớp implement interface là “cá nhân hóa” các cài đặt của riêng nó. Về việc đặt tên cho interface và lớp implement interface có 2 trường phái đó là prefix và suffix. Những người theo trường phái prefix sẽ đặt tên như sau: Ví dụ ta có interface Person
thì nó sẽ là IPerson
và lớp implement sẽ là Person
. Còn trường phái suffix thì interface sẽ là Person
và implement sẽ là PersonImp
. Riêng mình thì mình theo trường phái prefix còn bạn thì sao? Quay lại vấn đề nhé.
Ví dụ
Chúng ta có một interface định nghĩa các chứ năng của một máy in như sau:
public interface IMachine { void Print(); void Scan(); void Fax(); }
và một loại máy in hiện đại đáp ứng đầy đủ các tính năng trên gọi là MultipleFunctionPrinter
. Chúng ta sẽ tạo 1 class implement interface đó cho máy in hiện đại đó
public class MultipleFunctionPrinter : IMachine { public void Print() { // print } public void Scan() { // scan } public void Fax() { // fax } }
Sau đó có một loại máy in ít hiện đại hơn chỉ có chức năng print
và scan
. Chúng ta sẽ tạo 1 class loại máy in đó là:
public class OldFashionedPrinter : IMachine { public void Print() { // print } public void Scan() { // scan } public void Fax() { throw new NotImplementedException(); } }
Máy in cũ hơn không hỗ trợ Fax. Đó là vấn đề của chúng ta. Chúng ta không thể nào không implement Fax()
và cũng không thể throw Exception hoặc là show message khi người dùng gọi thực thi phương thức Fax()
. Để giải quyết vấn đề này chúng ta chia nhỏ thành các interface. Tính chất của interface là chúng ta có thể kế thừa từ nhiều interface trong khi đó chỉ được phép kế thừa từ 1 class.
public interface IPrinter { void Print(); } public interface IScanner { void Scan(); } public class Photocopier : IPrinter, IScanner { public void Print() { // print } public void Scan() { // scan } }
Thậm chí chúng ta có thể tạo 1 interface gọi là IMultipleFunctionDevice
kế thừa từ 2 interface IPrinter
, IScanner
. Thêm một ví dụ khác trong thực tế đó là List trong c#.
Tuy nhiên nếu chúng ta chia quá nhỏ các interface sẽ dẫn đến một số lượng lớn interface rất khó để kiểm soát. Vì vậy hãy phân tích thật kỹ càng và thận trọng trước khi chia nhỏ nhé. 🙂
Dependency Inversion principle (DIP)
Đây là nguyên tắc cuối cùng trong SOLID, cũng là nguyên tắc quan trọng và được áp dụng nhiều nhất trong lập trình. Nội dung nguyên tắc như sau:
Các module cấp cao không nên phụ thuộc vào module cấp thấp, Tất cả nên phụ thuộc vào abstractions. (abstractions ở đây có thể hiểu là các interface). Các abstractions không nên phụ thuộc vào chi tiết (implementation) và ngược lại.
Đọc khái niệm thì có vẻ khó hiểu nên mình sẽ đi qua ví dụ bên dưới để giải thích thế nào là module cấp cao, module cấp thấp.
public enum Relationship // enum cho loại quan hệ { Parent, Child, Sibling } // Class chứa thông tin như name, dateOfBirth, gender ...v..v.. public class Person { public string Name { get; set; } // other info public Person(string name) { Name = name; } } // Low-level public class Relationships { private List<(Person, Relationship, Person)> relations = new List<(Person, Relationship, Person)>(); public void AddParentAndChild(Person parent, Person child) { relations.Add((parent, Relationship.Parent, child)); relations.Add((child, Relationship.Child, parent)); } public List<(Person, Relationship, Person)> Relations => relations; } // High-level public class Research { public Research(Relationships relationships) { foreach (var item in relationships.Relations .Where(x => x.Item1.Name.Equals("John") && x.Item2 == Relationship.Parent)) { Console.WriteLine($"John has a child called {item.Item3.Name}"); } } public static void Run() { var parent = new Person("John"); var child1 = new Person("Chris"); var child2 = new Person("Steve"); var relationships = new Relationships(); relationships.AddParentAndChild(parent, child1); relationships.AddParentAndChild(parent, child2); var research = new Research(relationships); } }
Module ở đây là 1 class. Relationships
là một module cấp thấp ( low-level ) và Reseach
là module cấp cao ( high level ) vì Relationships
được sử dụng bên trong Research
. Relationships
có nhiệm vụ là lưu trữ lại danh sách các mối quan hệ và Research
là tìm kiếm tất cả mối quan hệ có cha là John. Hiện tại thì Research
đang bị phụ thuộc vào Relationships
. Nếu có bất kì sự thay đổi nào trong Relationships
thì cũng sẽ phá vỡ cấu trúc của Research
. Áp dụng theo nguyên tắc DIP thì chúng ta sẽ fix như sau:
// Code person và enum bên trên // Khai báo 1 interface với phương thức tìm children public interface IRelationshipBrowser { IEnumerable<Person> FindAllChildrenOf(string name); } // Kế thừa interface IRelationshipBrowser và implement phương thức FindAllChildrenOf public class Relationships : IRelationshipBrowser { private List<(Person, Relationship, Person)> relations = new List<(Person, Relationship, Person)>(); public void AddParentAndChild(Person parent, Person child) { relations.Add((parent, Relationship.Parent, child)); relations.Add((child, Relationship.Child, parent)); } public List<(Person, Relationship, Person)> Relations => relations; // Implement phương thức của interface. public IEnumerable<Person> FindAllChildrenOf(string name) { foreach (var item in relations .Where(x => x.Item1.Name.Equals("John") && x.Item2 == Relationship.Parent)) { yield return item.Item3; } } } public class Research { // Thay vì truyền vào một class cụ thể thì chúng ta sẽ truyền vào 1 interface (abstractions) public Research(IRelationshipBrowser relationshipBrowser) { foreach (var children in relationshipBrowser.FindAllChildrenOf("John")) { Console.WriteLine($"John has a child called {children.Name}"); } } public static void Run() { var parent = new Person("John"); var child1 = new Person("Chris"); var child2 = new Person("Steve"); var relationships = new Relationships(); relationships.AddParentAndChild(parent, child1); relationships.AddParentAndChild(parent, child2); var research = new Research(relationships); } }
Chúng ta sẽ viết một interface với một phương thức tìm kiếm children theo tên parent. Và cho Relationships
implement interface trên. Sau đó thay vì truyền vào class Relationships
cụ thể thì chúng ta truyền vào 1 interface. Kết quả 2 đoạn chương trình như nhau. Ở ví dụ thứ 2. Nếu trường hợp Relationships
có thay đổi trong cách lưu trữ, từ Tuple thành class hay bất kì thứ gì thì module cấp cao hơn sẽ không bị ảnh hưởng. Đó là nguyên tắc Dependency Inversion principle (DIP).
Trên thực tế có một pattern áp dụng nguyên tắc này mà chúng ta luôn gặp đó là Dependency Injection (DI). Việc truyền một abstraction qua contractor của một module cấp cao hơn gọi là injection. Mình sẽ có một bài viết khác để nói rõ hơn về DI, các bạn đón đọc nhé :3
Ưu điểm
Giảm độ kết dính của các module, dễ dàng thay thế hoặc bảo trì.
Linh thoạt thay đổi các implement của abstraction mà không bị ảnh hưởng ở module cấp cao.
Unit test dễ dàng.
Kết Luận
Như vậy là chúng ta đã đi xong 5 nguyên tắc trong SOLID. Hãy cố gắng áp dụng những nguyên tắc này để code của bạn trở nên sạch sẽ, gọn gàng hơn nhé 🙂 Nhưng nói đi cũng phải nói lại, đây chỉ là những nguyên tắc mang tính chất chỉ dẫn, không nên cứng ngắt áp dụng vào tất cả các tình huống. Việc lạm dụng quá nhiều sẽ làm code bạn trở nên phức tạp quá mức. Hãy nhớ là Keep It Simple Stupid (KISS).
Ở những bài viết tiếp theo mình sẽ tập trung vào các pattern trong cuốn Gang of Four và đưa ra các ví dụ thật gần thực tế. Hy vọng các bạn ủng hộ và góp ý cho các bài viết của mình nhé 🙂
Tài liệu tham khảo
https://github.com/levinhtxbt/design-patterns/tree/master/DesignPatterns/SOLID
https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
Permalink
thank a 😀