本文同步更新於blog
子類別必須要能替代它的父類別。
目前是以集合關係的角度來理解LSP。
實作上大致有兩個概念:
這樣子類別才有取代父類別的可能。(既有行為正常)
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背後就是集合論啊,數學系對這個原則備感親切。