iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 4
2

本文同步更新於blog

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types.

子類別必須要能替代它的父類別。


目前是以集合關係的角度來理解LSP。

實作上大致有兩個概念:

  • 子類別的Input型態可以比父類別更寬鬆。
  • 子類別的Output型態則需比父類別更為嚴謹。

這樣子類別才有取代父類別的可能。(既有行為正常)


Def. 餐廳:顧客可以用金流換取餐點的地方

舉個實際例子,假設你要繼承老爸的餐廳:

[Input篇]
原本顧客習慣的付款方式是現金
你可以另外提供信用卡來付款。
這樣原本使用現金的顧客,依然能在你的店消費。

[Output篇]
而顧客來到餐廳的目的是餐點
你可以提供漢堡、炸牛排、陽春麵等等。
但必須要是餐點


以下提供範例程式碼:

情境:老爸開了一間速食餐廳

  • 首先定義餐廳的產出:餐點介面
<?php

namespace App\SOLID\LSP\Restaurant\Contracts;

interface Eatable
{
    public function beEaten();
}

  • 接著供應的餐點類型:漢堡、炸雞、雞塊,它們都必須實作餐點介面
<?php

namespace App\SOLID\LSP\Restaurant\Food;

use App\SOLID\LSP\Restaurant\Contracts\Eatable;

class Burger implements Eatable
{
    public function beEaten()
    {
        return '招牌漢堡被吃了';
    }
}

<?php

namespace App\SOLID\LSP\Restaurant\Food;

use App\SOLID\LSP\Restaurant\Contracts\Eatable;

class FriedChicken implements Eatable
{
    public function beEaten()
    {
        return '招牌炸雞被吃了';
    }
}

<?php

namespace App\SOLID\LSP\Restaurant\Food;

use App\SOLID\LSP\Restaurant\Contracts\Eatable;

class ChickenNuggets implements Eatable
{
    public function beEaten()
    {
        return '招牌雞塊被吃了';
    }
}

  • 最後是老爸的餐廳
<?php

namespace App\SOLID\LSP\Restaurant;

use App\SOLID\LSP\Restaurant\Contracts\Eatable;
use App\SOLID\LSP\Restaurant\Food\Burger;
use Exception;
use App\SOLID\LSP\Restaurant\Food\ChickenNuggets;
use App\SOLID\LSP\Restaurant\Food\FriedChicken;

class DadRestaurant
{
    public function getFood($money): Eatable
    {
        if (!is_int($money)) {
            throw new Exception('我們只收現金');
        }

        $randomNumber = rand(1, 3);

        switch ($randomNumber) {
            case 1:
                return new Burger();
                break;

            case 2:
                return new FriedChicken();
                break;

            case 3:
                return new ChickenNuggets();
                break;
        }
    }
}

這邊可以發現,老爸餐廳的出餐是很隨意的,
客人用現金,可以換取餐點

但餐點可能是漢堡、炸雞或是雞塊其中之一。


時光匆匆,過了幾年,兒子決定繼承老爸餐廳

現在有些客人會使用信用卡等其他的付款方式。
也不希望餐廳餐點總是如此隨意,無法預料。

新開的餐廳決定只供應漢堡這種餐點,並提供其他付款方式。
(即對Input型態更寬鬆,而Output型態更嚴謹)

  • 兒子新開的餐廳
<?php

namespace App\SOLID\LSP\Restaurant;

use App\SOLID\LSP\Restaurant\DadRestaurant;
use App\SOLID\LSP\Restaurant\Food\Burger;
use App\SOLID\LSP\Restaurant\Contracts\Eatable;

class SonRestaurant extends DadRestaurant
{
    public function getFood($goldFlow): Eatable
    {
        return new Burger();
    }
}


測試客人消費的情況

<?php

namespace Tests\Unit\SOLID\LSP;

use PHPUnit\Framework\TestCase;
use App\SOLID\LSP\Restaurant\Program;
use App\SOLID\LSP\Restaurant\Contracts\Eatable;

class ProgramTest extends TestCase
{
    /**
     * @var Program
     */
    protected $sut;

    public function setUp(): void
    {
        $this->sut = new Program();
    }

    public function testUseMoneyInDadRestaurant()
    {
        $expected = Eatable::class;
        $money = 100;
        $actual = $this->sut->getFoodInDadRestaurant($money);
        $this->assertInstanceOf($expected, $actual);
    }

    public function testUseCardInDadRestaurant()
    {
        $card = '信用卡';
        $this->expectExceptionMessage('我們只收現金');
        $this->sut->getFoodInDadRestaurant($card);
    }

    public function testUseMoneyInSonRestaurant()
    {
        $expected = Eatable::class;
        $money = 100;
        $actual = $this->sut->getFoodInSonRestaurant($money);
        $this->assertInstanceOf($expected, $actual);
    }

    public function testUseCardInSonRestaurant()
    {
        $expected = Eatable::class;
        $card = '信用卡';
        $actual = $this->sut->getFoodInSonRestaurant($card);
        $this->assertInstanceOf($expected, $actual);
    }
}

透過測試,我們可以發現:

顧客類型 付款方式 老爸的餐廳 兒子的餐廳
老顧客 現金 可以換取餐點 可以換取餐點 (但只有漢堡)
新顧客 信用卡 無法換取餐點 可以換取餐點

符合了裡氏替換原則的精神。

ʕ •ᴥ•ʔ:LSP背後就是集合論啊,數學系對這個原則備感親切。


上一篇
Day3. 開放封閉原則
下一篇
Day5. 介面隔離原則
系列文
你終究都要學設計模式的,那為什麼不一開始就學呢?57
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言