> For the complete documentation index, see [llms.txt](https://www.saladpuk.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.saladpuk.com/basic/solid/lsp.md).

# Liskov Substitution Principle

## 👑 หัวใจหลักของ Liskov Substitution Principle (LSP)

> Let *Φ(x)* be a property provable about objects *x* of type *T*. Then *Φ(y)\_should be true for objects \_y* of type *S* where *S* is a subtype of *T*.

นี่มันสูตรสมการอะไรกันฟร๊าเนี่ย (เป็นนิยามของคนเสนอเรื่องนี้จริงๆในปี 1998 เลยนะจากแม่ใหญ่ **Barbara Liskov**) มันเลยมีนิยามอีกตัวที่ทำให้เข้าใจง่ายขึ้นออกมาว่า

> Subtypes **must** be substitutable for their base types.

**"คลาสลูกทุกตัวจะต้องสามารถทำหน้าที่แทนคลาสแม่ได้"** แปลออกมาแล้วก็มึนๆเหมือนเดิม เดี๋ยวไปดูตัวอย่างให้เข้าใจก่อนว่ามันเป็นยังไง

### 😵 ตัวอย่างคลาสลูกที่ไม่สามารถแทนคลาสแม่ได้

สมมุติว่าเราต้องจัดการกับของที่เป็น **สี่เหลี่ยมผืนผ้า** ซักตัว เราก็เลยเขียนคลาส **Rectangle** ขึ้นมาโดยเก็บ *ความกว้าง* กับ *ความสูง* และ สามารถคำนวณพื้นที่ได้ด้วย ตามโค้ดด้านล่างนี้

```csharp
public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double GetArea()
    {
        return Width * Height;
    }
}
```

อยู่มาวันนึง เราต้องการทำงานกับของที่เป็น **สี่เหลี่ยมจัตุรัส** บ้างล่ะ เราจะทำยังไง ? ถ้ามองแบบเผินๆ สี่เหลี่ยมจัตุรัส ก็มี *ความกว้าง* กับ *ความสูง* เหมือนกันนิน่า ดังนั้นเราก็แค่ทำ inheritance ซะก็จบแล้วนิ ตามรูปเลย

![](/files/-LnHSbhc1b5dF0NUNAFX)

ซึ่งจากที่ทำมาก็ดูเหมือนว่ามันจะไม่มีปัญหาอะไรนะ จนกระทั่งเรามานึกขึ้นได้ว่า **สี่เหลี่ยมจัตุรัส ขนาดของ** ***ความกว้าง*** **กับ** ***ความสูง*** **มันต้องเท่ากันนิหว่า!** ทำให้เราต้องไปแก้โค้ดของ Square โดยพลัน เพื่อปรับ ขนาดให้มันเท่ากันเสมอตามโค้ดด้านล่าง

```csharp
public class Square : Rectangle
{
    private double width;
    public new double Width
    {
        get { return width; }
        set
        {
            width = value;
            height = value;
        }
    }

    private double height;
    public new double Height
    {
        get { return height; }
        set
        {
            width = value;
            height = value;
        }
    }
}
```

นี่แหละคือตัวอย่างที่ **คลาสลูกไม่สามารถแทนคลาสแม่ได้อย่างสมบูรณ์**

## ❓ ทำไมคลาสลูกต้องสามารถแทนคลาสแม่ได้ล่ะ?

ความสัมพันธ์ของการทำ Inheritance นั้นเราเรียกว่า **IS-A relationship** นั่นหมายความว่า เวลาที่เราใช้คลาสที่สืบทอดกันมาเราก็จะคาดหวังว่าการทำงานในหลายๆส่วนจะมีพฤติกรรมที่เหมือนกับคลาสแม่ของมันด้วย เช่น มีคลาส **Dog** กับ **Cat** ที่สืบทอดจากคลาส **Animal** และ**สัตว์ทุกตัวสามารถส่งเสียงร้องได้** ตามรูปด้านล่างนี้

![](/files/-M1VIPUwLMo6ot_BbIax)

ซึ่งปรกติเวลาเราสั่งให้สัตว์มันส่งเสียงร้องจาก method **Goes()** เราก็จะ**คาดหวังให้มันส่งเสียงร้อง**ใช่ไหม? เช่น หมาก็จะร้องโฮ่งๆ แมวก็ร้องเหมี๊ยวๆไรงี้ แต่ถ้าคลาสลูกไม่ได้ทำงานเปรียบเสมือนคลาสแม่ล่ะ ลองคิดดูว่าถ้าเราเรียกให้แมวมันร้องผ่าน method **Goes()** ไปปุ๊ป แต่สิ่งที่แมวมันทำคือ มันตีลังกาเต้น 3 ตลบ เราจะแปลกใจแค่ไหน ว่าทำไมเอ็งไม่ร้องฟร๊าาาา

ดังนั้นจากที่ว่ามา พอกลับไปที่ตัวอย่าง **สี่เหลี่ยมผืนผ้า** กับ **สี่เหลี่ยมจัตุรัส** ด้านบนบ้าง มันจะเกิดอะไรขึ้นถ้า method เราเขียนโค้ดตรวจสอบพื้นที่ไว้แบบนี้

```csharp
public void TestCalculationAreaOfShape(Rectangle rec)
{
   rec.Width = 10;
   rec.Height = 5;
   if(rec.GetArea() != 50)
   {
      throw new Exception("การคำนวณพื้นที่ไม่ถูกต้อง")
   }
}
```

ถ้าเราส่ง **Rectangle** ไป มันจะทำงานได้ถูกต้องไม่มีปัญหาอะไร แต่พอเราส่ง **Square** ลงไปเท่านั้นแหละมันจะทำงานผิดทันที ทั้งๆที่การคำนวณพื้นที่มันถูกแล้วนะ แต่สิ่งที่มันทำให้โค้ดส่วนนี้ทำงานไม่ถูกคือ **คลาสลูกไม่สามารถแทนคลาสแม่ได้**นั่นเอง

{% hint style="success" %}
**สรุปสั้นๆจากการละเมิดกฎ LSP**\
เราจะไม่สามารถเชื่อใจได้ว่าคลาสลูกจะนำมาใช้งานแทนคลาสแม่ได้เสมอไป !!!
{% endhint %}

{% hint style="danger" %}
การละเมิดกฎ **LSP** ก็เป็นการละเมิดกฎ **OCP** ด้วยเช่นกัน เพราะเราไม่สามารถใช้ abstraction มาทำงานเพื่อต่อยอดความสามารถได้ทุกครั้งไป *(เรื่อง OCP ไปอ่านในบนก่อนหน้าเด้อ - ดูใน side menu เอา)*
{% endhint %}

## 😕 แล้วจะออกแบบยังไงให้คลาสลูกแทนคลาสแม่ได้

เราลองมองกลับไปที่ **Rectangle** กับ **Square** กันก่อนว่าทำไมมันถึงมีปัญหากันล่ะ? . . . คำตอบก็คือ การทำ Inheritance นั้นเป็นความสัมพันธ์ในรูปแบบ **IS-A** ยังไงล่ะ

{% hint style="success" %}
**IS-A relationship**\
ความสัมพันธ์ในรูปแบบนี้จะเกิดขึ้นได้ก็ต่อเมื่อ**ของทั้งสองอย่างนั้นต้องมีพฤติกรรม (behavior) ที่เหมือนกัน**นั่นเอง ถึงควรจะมีความสัมพันธ์กับแบบ IS-A
{% endhint %}

**ถ้าเรามองในฐานะคนเขียนโปรแกรม**\
เราจะเห็นแค่ว่า **Rectangle** กับ **Square มันมีตัวแปรที่ใช้ร่วมกันได้** เราก็เลยใช้ Inheritance ในจุดนั้น ซึ่งมันก็ไม่ได้ผิดในมุมของการ reuse code

**ถ้าเรามองในหลักการออกแบบ**\
เราจะพบว่า **Rectangle** กับ **Square มันไม่ได้มีพฤติกรรมเหมือนกัน** เพราะ Rectangle นั้นความกว้างกับความสูงมันไม่เท่ากันก็ได้ แต่ในขณะที่ Square ความกว้างกับความสูงต้องเท่ากันเสมอ ดังนั้นถ้ามองจากหลักการออกแบบแล้วจะพบว่า Square มันไม่ใช่กลุ่มเดียวกับ Rectangle ดังนั้นไม่ควรใช้ Inheritance นั่นเอง

{% hint style="success" %}
**Object-Oriented Design (OOD)**\
มุ้งเน้นไปที่เรื่องการออกแบบโดยมองจากมุมพฤติกรรม behavior เป็นหลัก ดังนั้นถ้าเราปฏิบัติตาม LSP โค้ดเราก็จะได้หลัก OOD ไปด้วยในตัว
{% endhint %}

พออ่านมาถึงตรงนี้หลายๆคนก็เริ่มมึนๆแล้วว่า แล้วจะรู้ได้ยังไงว่าของแต่ละอย่างมันจะมีพฤติกรรมยังไง ต้องไปนั่งวิเคราะห์มันแต่ละตัวก่อนอีกเหรอ? คำตอบคือ(วิเคราะห์ก่อนก็ดีนะ) หรือเราสามารถใช้เทคนิคที่เรียกว่า **Design by Contract** หรือ **Factoring** ก็ได้

### 😄 Design by Contract (DBC)

เป็นการออกแบบโดยใช้ **Code Contract** เข้ามาช่วย วิธีทำคือเวลาที่เราอยากจะเขียนการทำงานซักอย่างนึงขึ้นมา เราก็จะไปกำหนดพฤติกรรมว่าของที่เราอยากได้คืออะไรทิ้งเอาไว้ ส่วนเมื่อไหร่ที่จะเขียนโค้ดการทำงานจริงๆก็ต้องเขียนโค้ดให้ตรงกับพฤติกรรมที่เรากำหนดไว้ในตอนแรกทั้งหมดด้วย

Code Contract สามารถเขียน **Preconditions & Postconditions** ได้ ้เพื่อใช้ตรวจสอบพฤติกรรมของโค้ดที่เขียนว่ามันเป็นไปตามที่เราต้องการหรือเปล่า

{% hint style="info" %}
**Code Contract**\
อ่านเรื่องนี้เต็มๆได้จากลิงค์นี้ [Microsoft: Code Contract](https://docs.microsoft.com/en-us/dotnet/framework/debug-trace-profile/code-contracts)
{% endhint %}

{% hint style="info" %}
**Unit testing**\
เราสามารถใช้ unit tests เข้ามาช่วยตรวจสอบ **Preconditions & Postconditions** ของ Code Contract ได้ด้วยนะ เพื่อเอาไว้ตรวจว่าโค้ดที่เขียนขึ้นมาตรงตาม contract ที่ระบุไว้หรือไม่
{% endhint %}

### 😄 Factoring code

เป็นเทคนิกในการแยกของต่างๆที่ซ้ำกันออกมา แล้วเขียนเฉพาะของที่มีลักษณะเฉพาะตัวให้กับคลาสตัวนั้นๆ

จากตัวอย่างของ Rectangle กับ Square เราก็แค่แยกของที่ซ้ำกันออกมานั่นก็คือ การคำนวณพื้นที่ **GetArea()** ส่วนเรื่อง *ความกว้าง* กับ *ความสูง* ของคลาสทั้งสองตัวมันเป็นลักษณะเฉพาะของมัน ดังนั้นเราก็ควรแยกไปเป็นของใครของมันเลย ดังนั้นเราก็จะได้ออกมาเป็นโค้ดด้านล่างนี้

![](/files/-LqwNOcCtSjBNiY5toxy)

```csharp
public abstract class Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Shape(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }
}

public class Rectangle : Shape
{
    public Rectangle(double width, double height)
        : base(width, height)
    {
    }
}

public class Square : Shape
{
    public Square(double widthOrHight)
        : base(widthOrHight, widthOrHight)
    {
    }
}
```

## 🎯 บทสรุป

ถ้าคลาสลูกไม่สามารถทำงานแทนคลาสแม่ได้อย่างสมบูรณ์แล้ว นั่นหมายความว่าเราจะใช้งานคลาสลูกแบบหวั่นๆ เพราะต้องคอยจำว่าคลาสลูกตัวนี้ใช้ได้ถ้าเป็นแบบนี้ คลาสลูกตัวนี้ใช้ไม่ได้ถ้าเป็นแบบนั้น และเราไม่มีทางไปจำเงื่อนไขทั้งหมดได้ตลอดไปหรอก **ทำให้โค้ดเราขาดคุณสมบัติของ OCP ไปในตัว**
