Design pattern: SOLID Design Principles 3

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.

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)

SOLID - 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 printscan. 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#.

SOLID ISP

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

solid - dependency injection

Ư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

1 Comment

Leave a Reply