Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> C#

Liskov Substitution Principle (LSP) คืออะไร และการนำไปใช้ใน C#

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)

  1. Contravariance of Parameters: Parameter Type ใน Derived ต้องเป็น Base Type หรือ Supertype (กว้างขึ้น)
  2. Covariance of Return Types: Return Type ใน Derived ต้องเป็น Subtype (แคบลง)
  3. Exceptions: Derived ไม่ควรโยน Exception ใหม่ที่ Base ไม่โยน
  4. Immutable Properties: Property ที่ Read-only ใน Base ควร Read-only ใน Derived

สรุป

LSP หมายถึง: "Is-A Relationship ต้องเป็น Behavioral Subtyping ไม่ใช่แค่ Structural" หาก Derived Class เปลี่ยนพฤติกรรมที่ Client คาดหวังจาก Base ให้ใช้ Composition แทน Inheritance