Liskov Substitution Principle (LSP) ระบุว่า "Derived types must be completely substitutable for their base types" (Subtype ต้องแทนที่ Base Type ได้โดยไม่ทำให้โปรแกรมผิดพลาด)
ง่ายๆ คือ: ถ้าโค้ดทำงานถูกต้องกับ Base Class แล้ว ต้องทำงานถูกต้องกับ Derived Class ทุกตัวได้เช่นกัน (Behavioral Subtyping)
ตัวอย่าง: ละเมิด LSP (Violation)
public class Ellipse
{
public double MajorAxis { get; set; }
public double MinorAxis { get; set; }
public virtual void SetMajorAxis(double majorAxis) => MajorAxis = majorAxis;
public virtual void SetMinorAxis(double minorAxis) => MinorAxis = minorAxis;
public virtual double Area() => MajorAxis * MinorAxis * Math.PI;
}
public class Circle : Ellipse
{
// ปัญหา: Circle มี Axis เดียว (Radius) แต่สืบทอด Ellipse ที่มี 2 Axis
public override void SetMajorAxis(double majorAxis)
{
base.SetMajorAxis(majorAxis);
MinorAxis = majorAxis; // บังคับให้ Minor = Major
}
public override void SetMinorAxis(double minorAxis)
{
base.SetMinorAxis(minorAxis);
MajorAxis = minorAxis; // บังคับให้ Major = Minor
}
}
// Client Code
public class AreaCalculator
{
public void Calculate(Ellipse ellipse)
{
ellipse.SetMajorAxis(5);
ellipse.SetMinorAxis(4); // คาดหวัง Area = 5*4*PI = 20PI
var area = ellipse.Area();
// ถ้าเป็น Circle: SetMinorAxis(4) ทำให้ MajorAxis กลายเป็น 4 -> Area = 4*4*PI = 16PI (ผิด!)
Console.WriteLine($"Area: {area}");
}
}
ปัญหา: Circle แทน Ellipse ไม่ได้ เพราะพฤติกรรม SetMajorAxis/SetMinorAxis ขัดแย้งกับ Invariant ของ Circle (Radius ต้องเท่ากัน)
ตัว例: ปฏิบัติตาม LSP (Compliant)
วิธี 1: แยก Hierarchy (Composition over Inheritance)
// Base แบบไม่มี Setter ที่ขัดแย้ง
public abstract class Shape
{
public abstract double Area();
}
public class Ellipse : Shape
{
public double MajorAxis { get; set; }
public double MinorAxis { get; set; }
public override double Area() => MajorAxis * MinorAxis * Math.PI;
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Radius * Radius * Math.PI;
}
// Client ใช้ Shape (Base Abstraction)
public class AreaCalculator
{
public void Calculate(Shape shape) => Console.WriteLine($"Area: {shape.Area()}");
}
วิธี 2: ทำให้ Derived Class รักษา Contract ของ Base (Design by Contract)
- Preconditions: Derived ต้องอ่อนโยนกว่า (Weaker) หรือเท่ากัน
- Postconditions: Derived ต้องเข้มงวดกว่า (Stronger) หรือเท่ากัน
- Invariants: Derived ต้องรักษา Invariant ของ Base
กฎ LSP สำคัญ (Behavioral Subtyping)
- Contravariance of Parameters: Parameter Type ใน Derived ต้องเป็น Base Type หรือ Supertype (กว้างขึ้น)
- Covariance of Return Types: Return Type ใน Derived ต้องเป็น Subtype (แคบลง)
- Exceptions: Derived ไม่ควรโยน Exception ใหม่ที่ Base ไม่โยน
- Immutable Properties: Property ที่ Read-only ใน Base ควร Read-only ใน Derived
สรุป
LSP หมายถึง: "Is-A Relationship ต้องเป็น Behavioral Subtyping ไม่ใช่แค่ Structural" หาก Derived Class เปลี่ยนพฤติกรรมที่ Client คาดหวังจาก Base ให้ใช้ Composition แทน Inheritance