Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> C#

Nguyên tắc Liskov Substitution Principle (LSP) và triển khai trong C#

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ắcGiải thích
PreconditionsSubtype không thể 강화 (strengthen) precondition (yêu cầu ít hơn hoặc bằng).
PostconditionsSubtype không thể yếu (weaken) postcondition (cam kết nhiều hơn hoặc bằng).
InvariantsSubtype phải bảo toàn invariants của base type.
History ConstraintSubtype 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ụ: Square kế thừa Rectangle vớ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.