design pattern
設計模式
策略模式
strategy pattern
php
當需要多個排列組合達成效果時可以使用 避免使用簡單工廠造成多過的class
該範例當中有兩個介面餵食飼料介面
選擇寵物介面
需求一 :客戶想要一台自動餵食寵物機
<?php
namespace Src\Behavioral\StrategyPatternByKen;
/**
* @property string food 寵物的飼料
*/
class Machine
{
public function __construct(string $food)
{
$this->food = $food;
}
public function toEat()
{
return $this->food;
}
}
需求二 客戶想要選擇高級的飼料
<?php
namespace Src\Behavioral\StrategyPatternByKen;
/**
* @property string food 寵物的飼料
* @property string level 飼料等級
*/
class Machine
{
public function __construct(string $food, string $level)
{
$this->food = $food;
$this->level = $level;
}
public function toEat()
{
if ($this->level == 'high') {
return '高級的'. $this->food;
}
return $this->food;
}
}
需求三 客戶有時候沒錢無法支持一般的飼料
<?php
namespace Src\Behavioral\StrategyPatternByKen;
/**
* @property string food 寵物的飼料
* @property string level 飼料等級
*/
class Machine
{
public function __construct(string $food, string $level)
{
$this->food = $food;
$this->level = $level;
}
public function toEat()
{
if ($this->level == 'high') {
return '高級的'. $this->food;
}
if ($this->level == 'low') {
return '貧窮的'. $this->food;
}
return $this->food;
}
}
根據不同情形的對應的型態, 就可以使用簡單工廠模式改變
實作三個類別 分別是正常飼料 高級飼料 以及低級飼料
<?php
namespace Src\Behavioral\StrategyPatternByKen\Contracts;
interface Eatable
{
public function toEat();
}
<?php
namespace Src\Behavioral\StrategyPatternByKen;
use Src\Behavioral\StrategyPatternByKen\Contracts\Eatable;
/**
* @property string food
*/
class NormalFood implements Eatable
{
public function __construct(string $food)
{
$this->food = $food;
}
public function toEat(): string
{
return $this->food;
}
}
<?php
namespace Src\Behavioral\StrategyPatternByKen;
/**
* @property string food
* @property string level
*/
class HighFood implements Contracts\Eatable
{
public function __construct(string $food, string $level)
{
$this->food = $food;
$this->level = $level;
}
public function toEat(): string
{
return $this->level . $this->food;
}
}
<?php
namespace Src\Behavioral\StrategyPatternByKen;
class LowFood implements Contracts\Eatable
{
private string $food;
private string $level;
public function __construct(string $food, string $level)
{
$this->food = $food;
$this->level = $level;
}
public function toEat():string
{
return $this->level . $this->food;
}
}
最後原本程式搭配簡單工廠即可完成
但客服送來第四個需求
希望這個寵物餵食機有切換不同動物的功能
按照簡單工廠的思維 必須要有最少三種以上的類別
分別是(一般飼料 高級飼料 低級飼料) X (動物的種類) 排列組合
而且這樣也違反開放封閉原則
這樣每次有新的動物時就會要改動所有的程式碼
研究一下之後發現了適合的設計模式 本文的標題 策略模式
<?php
namespace Src\Behavioral\StrategyPatternByKen\EatRegister\Contracts;
interface Animalcule
{
public function getAnimal();
}
<?php
namespace Src\Behavioral\StrategyPatternByKen\EatRegister;
use Src\Behavioral\StrategyPatternByKen\EatRegister\Contracts\Animalcule;
class Cat implements Animalcule
{
public function getAnimal(): string
{
return '貓咪';
}
}
<?php
namespace Src\Behavioral\StrategyPatternByKen\EatRegister;
class Dog implements Contracts\Animalcule
{
public function getAnimal(): string
{
return '柴犬';
}
}
<?php
namespace Src\Behavioral\StrategyPatternByKen\EatRegister;
use http\Exception\RuntimeException;
use LogicException;
use Src\Behavioral\StrategyPatternByKen\Contracts\Eatable;
use Src\Behavioral\StrategyPatternByKen\EatRegister\Contracts\Animalcule;
use Src\Behavioral\StrategyPatternByKen\HighFood;
use Src\Behavioral\StrategyPatternByKen\LowFood;
use Src\Behavioral\StrategyPatternByKen\NormalFood;
use function PHPUnit\Framework\throwException;
/**
* @property Eatable discountMethod
* @property Animalcule discountAnimal
*/
class AnimalEatContext
{
public function __construct(string $food, string $level, string $animal)
{
$this->resolveDiscountMethod($food, $level);
$this->resolveDiscountAnimal($animal);
}
private function resolveDiscountMethod(string $food, string $level)
{
$result = [
'high' => new HighFood($food, '高級的'),
'low' => new LowFood($food, '低級的'),
'normal' => new NormalFood($food),
];
if (!isset($result[$level])) {
throw new LogicException('沒有這種類別的等級');
}
$this->discountMethod = $result[$level];
}
public function toEat(): string
{
return $this->discountMethod->toEat();
}
private function resolveDiscountAnimal(string $animal)
{
$result = [
'dog' => new Dog(),
'cat' => new Cat()
];
if (!isset($result[$animal])) {
throw new LogicException('沒有這種動物');
}
$this->discountAnimal = $result[$animal];
}
public function getAnimal():string
{
return $this->discountAnimal->getAnimal();
}
}
在這邊我與原作者不同的點在於 我用陣列的方式儲存 要返回的 動物類別以及 不同等級的飼料 若出現了非陣列中的值 則直接丟出例外錯誤
再來修改machine 呼叫這個餵食飼料類別
<?php
namespace Src\Behavioral\StrategyPatternByKen;
use Src\Behavioral\StrategyPatternByKen\EatRegister\AnimalEatContext;
/**
* @property AnimalEatContext animalcule
*/
class Machine
{
public function __construct(string $food, string $level, string $animal)
{
$this->animalcule = new AnimalEatContext($food, $level, $animal);
}
public function toEat():string
{
return $this->animalcule->toEat();
}
public function getAnimal():string
{
return $this->animalcule->getAnimal();
}
}
[單一職責原則
]
將類別本身職責跟飼料等級的職責分離 就是策略模式的精神
[開放封閉原則
]
不會在客戶提出一個新的需求時 影響到全部的程式碼了.
[介面隔離原則
]
定義出動物介面
與飼料等級介面
讓兩者不會互相影響
可以交由各自的算法 分別實現
[依賴反轉原則
]
機器只需要依賴抽象的動物
與飼料
介面
輸入不同的參數就能獲得對應的算法 實現相對應的抽象介面
[測試
]
<?php
namespace Test\Behavioral\StrategyPatternByKen;
use Src\Behavioral\StrategyPatternByKen\Machine;
use PHPUnit\Framework\TestCase;
/**
* @property Machine machine
*/
class MachineTest extends TestCase
{
public function test_dog_normal_food()
{
$this->machine = new Machine('乾糧', 'normal', 'dog');
$this->assertEquals('柴犬', $this->machine->getAnimal());
$this->assertEquals('乾糧', $this->machine->toEat());
}
public function test_cat_normal_food()
{
$this->machine = new Machine('乾糧', 'normal', 'cat');
$this->assertEquals('貓咪', $this->machine->getAnimal());
$this->assertEquals('乾糧', $this->machine->toEat());
}
public function test_dog_low_food()
{
$this->machine = new Machine('乾糧', 'low', 'dog');
$this->assertEquals('柴犬', $this->machine->getAnimal());
$this->assertEquals('低級的乾糧', $this->machine->toEat());
}
public function test_cat_low_food()
{
$this->machine = new Machine('乾糧', 'low', 'cat');
$this->assertEquals('貓咪', $this->machine->getAnimal());
$this->assertEquals('低級的乾糧', $this->machine->toEat());
}
public function test_dog_high_food()
{
$this->machine = new Machine('乾糧', 'high', 'dog');
$this->assertEquals('柴犬', $this->machine->getAnimal());
$this->assertEquals('高級的乾糧', $this->machine->toEat());
}
public function test_cat_high_food()
{
$this->machine = new Machine('乾糧', 'high', 'cat');
$this->assertEquals('貓咪', $this->machine->getAnimal());
$this->assertEquals('高級的乾糧', $this->machine->toEat());
}
public function test_error_normal_food()
{
//輸入錯誤的動物
$this->expectException(\LogicException::class);
$this->machine = new Machine('乾糧', 'normal', 'mouse');
}
public function test_normal_error_level_food()
{
//輸入錯誤的等級
$this->expectException(\LogicException::class);
$this->machine = new Machine('乾糧', 'aaa', 'dog');
}
}
結果
參考來源:
it鐵人賽-YNCBearz