Liskov Substitution Principle (LSP): Đối tượng của lớp con (derived) phải có thể thay thế đối tượng của lớp cha (base) mà không làm thay đổi tính đúng đắn của chương trình. - Barbara Liskov.
Nghĩa là: Nếu S là subtype của T, thì mọi chỗ dùng T đều có thể dùng S mà không bị lỗi logic.
Vi phạm LSP: Circle kế thừa Ellipse
// TRƯỚC: Vi phạm LSP
public class Ellipse
{
public double MajorAxis { get; set; }
public double MinorAxis { get; set; }
public virtual void SetMajorAxis(double major) => MajorAxis = major;
public virtual void SetMinorAxis(double minor) => MinorAxis = minor;
public virtual double Area() => MajorAxis * MinorAxis * Math.PI;
}
public class Circle : Ellipse
{
public override void SetMajorAxis(double major)
{
base.SetMajorAxis(major);
MinorAxis = major; // Circle: cả hai trục bằng nhau
}
}
// Client code
public void Resize(Ellipse e)
{
e.SetMajorAxis(5);
e.SetMinorAxis(4); // Mong đợi 5*4=20
Console.WriteLine($"Area: {e.Area()}"); // Circle: 5*5=25 -> SAI LOGIC
}
Circle không thể thay thế Ellipse vì thay đổi hành vi của SetMinorAxis (khi set Minor thì Major cũng thay đổi).
Tuân thủ LSP: Thiết kế lại hierarchy
Cách 1: Không kế thừa, dùng Composition
public interface IShape { double Area(); }
public class Ellipse : IShape
{
public double MajorAxis { get; set; }
public double MinorAxis { get; set; }
public double Area() => MajorAxis * MinorAxis * Math.PI;
}
public class Circle : IShape
{
public double Radius { get; set; }
public double Area() => Radius * Radius * Math.PI;
}
// Circle và Ellipse đều implement IShape, không kế thừa nhau.
Cách 2: Bất biến (Immutable) - loại bỏ setter
public abstract class Shape
{
public abstract double Area();
}
public class Ellipse : Shape
{
public double MajorAxis { get; }
public double MinorAxis { get; }
public Ellipse(double major, double minor) { MajorAxis = major; MinorAxis = minor; }
public override double Area() => MajorAxis * MinorAxis * Math.PI;
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) { Radius = radius; }
public override double Area() => Radius * Radius * Math.PI;
}
// Không có setter -> không thể vi phạm LSP qua mutation.
Quy tắc thiết kế tuân thủ LSP (Design by Contract)
| Quy tắc | Giải thích |
|---|---|
| Preconditions | Subtype không thể 강화 (strengthen) precondition (yêu cầu ít hơn hoặc bằng). |
| Postconditions | Subtype không thể yếu (weaken) postcondition (cam kết nhiều hơn hoặc bằng). |
| Invariants | Subtype phải bảo toàn invariants của base type. |
| History Constraint | Subtype không thể thay đổi state mà base type không cho phép. |
Dấu hiệu vi phạm LSP thường gặp
- Override method ném
NotImplementedException. - Override method trả về kiểu khác (covariant return OK, contravariant không).
- Subclass bỏ qua logic validation của base class.
- Subclass thay đổi hành vi cơ bản (ví dụ:
Squarekế thừaRectanglevới Width/Height độc lập).
Tóm lại: Hãy dùng kế thừa khi có quan hệ IS-A đúng nghĩa (behavioral subtyping), không chỉ cấu trúc dữ liệu. Ưu tiên Composition over Inheritance.