Series SOLID cho thanh niên code cứng: Liskov Substitution Principle (P3)

Giới thiệu

Đây là đây là bài viết thứ 3 trong series “SOLID cho thanh niên code cứng”. Ở bài viết này, mình sẽ nói về Liskov Substitution Principle – Nguyên lý Thay Thế Lít Kốp (LSP).
  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle
Nội dung nguyên lý:
Trong một chương trình, các object của class con có thể thay thế class cha mà không
làm thay đổi tính đúng đắn của chương trình

Giải thích nguyên lý

Xin cảnh báo trước một chút là nguyên lý này hơi trừu tượng và khó hiểu (các bác developer nước ngoài cũng tranh cãi khá nhiều về nó), do đó mình sẽ cố gắng giải thích một cách đơn giản nhất có thể. Nếu đọc lần đầu không hiểu, các bạn cố gắng đọc kĩ lại vài lần nhé, nếu vẫn không hiểu thì… chịu khó google tìm bài khác vậy.
Để giữ tính đúng đắn của chương trình, class con phải thay thế được class cha. Nói dễ hiểu là thế này: Ngày xửa ngày xưa, hẳn bạn nào cũng có 1 cái Individual Portable Brick Game, tên tiếng Việt là máy chơi game xếp hình “thần thánh”. Thuở ấy, mỗi lần máy hết pin, mình lại xin mấy nghìn ra ngoài hàng mua pin con ó gắn vào chơi tiếp. Một lần nọ, hết pin mà không có tiền, mình đi lượm mấy cục pin con heo gắn vào chơi tạm. Không ngờ gắn pin vào xong, vừa hí hửng bật máy lên thì máy bị cháy vì điện thế của pin hơn bình thường. Thế là đi tong luôn cái máy chỉ vì… tiếc tiền mua pin.

Theo lý thuyết, class PinConHeo là con của class Pin, khi ta dùng PinConHeo gắn vào làm Pin thì máy phải chạy bình thường. Tuy nhiên trong trường hợp của mình, class Pin Con Heo đã vi phạm LSP vì đã gây lỗi khi dùng thay cho class Pin (Sau này, mình gặp một trải nghiệm tương tự với class PhimConH.., nhưng mà thôi để lần khác kể vậy).

Ví dụ minh họa

Mình sẽ đưa ra 2 ví dụ thường gặp về việc vi phạm LSP:

Ví dụ thứ nhất, class con quăng exception khi gọi hàm

Giả sử, ta muốn viết một chương trình để mô tả các loài chim bay. Đại bàng, chim sẻ, vịt bay được, nhưng chim cánh cụt không bay được. Do chim cánh cụt cũng là chim, ta cho nó kế thừa class Bird. Tuy nhiên, vì cánh cụt không biết bay, khi gọi hàm bay của chim cánh cụt, ta sẽ quăng NoFlyException.
public class Bird {
  public virtual void Fly() { Console.Write("Fly"); }
}
public class Eagle : Bird {
  public override void Fly() { Console.Write("Eagle Fly"); }
}
public class Duck : Bird {
  public override void Fly() { Console.Write("Duck Fly"); }
}
public class Penguin : Bird {
  public override void Fly() { throw new NoFlyException(); }
}

var birds = new List { new Bird(), new Eagle(), new Duck(), new Penguin() };
foreach(var bird in birds) bird.Fly(); 
// Tới pengiun thì lỗi vì cánh cụt quăng Exception
Ta tạo 1 mảng chứa các loài chim rồi duyệt các phần tử. Khi gọi hàm Flycủa class Penguin, hàm này sẽ quăng lỗi. Class Penguin gây lỗi khi chạy, không thay thế được class cha của nó là Bird, do đó nó đã vi phạm LSP.

Ví dụ thứ 2, class con thay đổi hành vi class cha

Đây là ví dụ kinh điển về hình vuông và hình chữ nhật mà mọi người thường dùng để giải thích LSP, mình chỉ viết và giải thích lại đôi chút.
Đầu tiên, hãy cùng đọc đoạn code dưới đây. Ta có 2 class cho hình vuông và hình chữ nhật. Ai cũng biết hình vuông là hình chữ nhật có 2 cạnh bằng nhau, do đó ta có thể cho class Square kế thừa classRectangle để tái sử dụng code.
public class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }
    
    public virtual void SetHeight(int height)
    {
        this.Height = height;
    }
    public virtual void SetWidth(int width)
    {
        this.Width = width;
    }
    public virtual int CalculateArea()
    {
        return this.Height * this.Width;
    }
}

public class Square : Rectangle
{
    public override void SetHeight(int height)
    {
        this.Height = height;
        this.Width = height;
    }
    
    public override void SetWidth(int width)
    {
        this.Height = width;
        this.Width = width;
    }
}
Do hình vuông có 2 cạnh bằng nhau, mỗi khi set độ dài 1 cạnh thì ta set luôn độ dài của cạnh còn lại. Tuy nhiên, khi chạy thử, hành động này đã thay đổi hành vi của của class Rectangle, dẫn đến vi phạm LSP.
// code from http://prasadhonrao.com/solid-principles-liskov-substitution-principle-lsp/
Rectangle rect = new Rectangle();
rect.SetHeight(10);
rect.SetWidth(5);
System.Console.WriteLine(rect.CalculateArea()); // Kết quả là 5 * 10

// Below instantiation can be returned by some factory method
Rectangle rect1 = new Square(); 
rect1.SetHeight(10);
rect1.SetWidth(5);
System.Console.WriteLine(rect1.CalculateArea()); 
// Kết quả là 5 x 5. Nếu đúng phải là 10x5, vì diện tích 1 hình chữ nhật là dài x rộng
// Class Square sửa hành vi của class cha Rectangle, set cả dài và rộng về 5 
Trong trường hợp này, để code không vi phạm LSP, ta phải tạo 1 class cha là class Shape, sau đó cho Square và Rectangle kế thừa class Shapenày.

Lưu ý và kết luận

Đây là nguyên lý… dễ bị vi phạm nhất, nguyên nhân chủ yếu là do sự thiếu kinh nghiệm khi thiết kế class. Thông thường, design các class dựa theo đời thật: hình vuông là hình chữ nhật, chim cánh cụt là chim. Tuy nhiên, không thể bê nguyên văn mối quan hệ này vào code. Hãy nhớ 1 điều:
Trong đời sống, A là B (hình vuông là hình chữ nhật, chim cánh cụt là chim) không có nghĩa là class A nên kế thừa class B. Chỉ cho class A kế thừa class B khi class A thay thế được cho class B.
Pin con heo là pin nhưng không thay thế được cho pin, chim cánh cụt là chim nhưng không thay thế được cho chim, do đó 2 ví dụ này vi phạm LSP.
Nguyên lý này ẩn giấu trong hầu hết mọi đoạn code, giúp cho code linh hoạt và ổn định mà ta không hề hay biết. Ví dụ như trong C#, ta có thể chạy hàm foreach với List, ArrayList, LinkedList bởi vì chúng cùng kế thừa interface IEnumerable.  Các class List, ArrayList, .. đã được thiết kế đúng LSP, chúng có thể thay thế cho IEnumerable mà không làm hỏng tính đúng đắn của chương trình.
Một số tài liệu để tham khảo thêm:

No comments:

Post a Comment

The Ultimate XP Project

  (Bài chia sẻ của tác giả  Ryo Amano ) Trong  bài viết  số này, tôi muốn viết về dự án phát triển phần mềm có áp dụng nguyên tắc phát triển...