---
# System prepended metadata

title: 自動餵食機 (策略模式)
tags: [php, design pattern, 設計模式, strategy pattern, 策略模式]

---

# 自動餵食機 (策略模式)

###### tags: `design pattern` `設計模式` `策略模式` `strategy pattern` `php`
> 當需要多個排列組合達成效果時可以使用 避免使用簡單工廠造成多過的class
> 該範例當中有兩個介面 `餵食飼料介面` `選擇寵物介面`

> 需求一 ：客戶想要一台自動餵食寵物機

```php=
<?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=
<?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=
<?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=
<?php


namespace Src\Behavioral\StrategyPatternByKen\Contracts;


interface Eatable
{
    public function toEat();
}
```

* 定義正常飼料類別
```php=
<?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=
<?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=
<?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=
<?php

namespace Src\Behavioral\StrategyPatternByKen\EatRegister\Contracts;

interface Animalcule
{
    public function getAnimal();
}
```
* 實作貓咪的介面
```php=
<?php

namespace Src\Behavioral\StrategyPatternByKen\EatRegister;

use Src\Behavioral\StrategyPatternByKen\EatRegister\Contracts\Animalcule;

class Cat implements Animalcule
{

    public function getAnimal(): string
    {
        return '貓咪';
    }
}
```
* 實作狗狗的介面
```php=
<?php

namespace Src\Behavioral\StrategyPatternByKen\EatRegister;

class Dog implements Contracts\Animalcule
{

    public function getAnimal(): string
    {
        return '柴犬';
    }
}
```

* 製作一個餵食飼料類別 他擁有所有的動物吃飯模式

```php=
<?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=
<?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=
<?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');
    }
}

```

結果
![](https://i.imgur.com/nokCZN4.png)

參考來源:
[it鐵人賽-YNCBearz](https://ithelp.ithome.com.tw/articles/10243000)
