本文同步更新於blog
需求一:客戶想要一台收銀機
<?php
namespace App\StrategyPattern\CashRegister;
class Program
{
    /**
     * @var int
     */
    private $originalPrice;
    public function __construct($originalPrice)
    {
        $this->originalPrice = $originalPrice;
    }
    public function pay()
    {
        return $this->originalPrice;
    }
}
需求二:客戶想要有一個優惠活動 (打8折)
<?php
namespace App\StrategyPattern\CashRegister;
class Program
{
    /**
     * @var int
     */
    private $originalPrice;
    /**
     * @var string
     */
    private $promotion;
    public function __construct($originalPrice, $promotion)
    {
        $this->originalPrice = $originalPrice;
        $this->promotion = $promotion;
    }
    public function pay()
    {
        if ($this->promotion == '20% off') {
            return $this->originalPrice * 0.8;
        }
        return $this->originalPrice;
    }
}
需求三:客戶想要有另一個優惠 (買300回饋100)
public function pay()
{
    $originalPrice = $this->originalPrice;
    if ($this->promotion == '20% off') {
        return $originalPrice * 0.8;
    }
    if ($this->promotion == 'spend_300_feedback_100') {
        if ($originalPrice >= 300) {
            return $originalPrice - floor($originalPrice / 300) * 100;
        }
    }
    return $originalPrice;
}
這時候功能是完成了,但有沒有覺得哪裡怪怪的?
欸嘿,我們想到之前學過的簡單工廠模式。
可以實作三個類別,分別是正常付費、8折付費、買300回饋100。
讓我們利用簡單工廠改造它。
<?php
namespace App\StrategyPattern\CashRegister\Contracts;
interface Payable
{
    public function pay();
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class NormalPay implements Payable
{
    /**
     * @var int
     */
    private $originalPrice;
    public function __construct($originalPrice)
    {
        $this->originalPrice = $originalPrice;
    }
    public function pay()
    {
        return $this->originalPrice;
    }
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class OffPercentPay implements Payable
{
    /**
     * @var int
     */
    private $originalPrice;
    /**
     * @var double
     */
    private $offPercent;
    public function __construct($originalPrice, $offPercent)
    {
        $this->originalPrice = $originalPrice;
        $this->offPercent = $offPercent;
    }
    public function pay()
    {
        return $this->originalPrice * (1 - $this->offPercent);
    }
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class FeedbackPay implements Payable
{
    /**
     * @var int
     */
    private $originalPrice;
    /**
     * @var int
     */
    private $priceCondition;
    /**
     * @var int
     */
    private $feedback;
    public function __construct($originalPrice, $priceCondition, $feedback)
    {
        $this->originalPrice = $originalPrice;
        $this->priceCondition = $priceCondition;
        $this->feedback = $feedback;
    }
    public function pay()
    {
        $originalPrice = $this->originalPrice;
        $priceCondition = $this->priceCondition;
        $feedback = $this->feedback;
        if ($originalPrice >= $priceCondition) {
            return $originalPrice - floor($originalPrice / $priceCondition) * $feedback;
        }
        return $originalPrice;
    }
}
最後原本程式再搭配工廠即可完成。 (下略)
正當我們洋洋得意的時候,客戶送來第四個需求...
需求四:客戶希望收銀機可以開一般發票或電子發票
不是啊,客戶你要這種發票類型的需求你要先說.. (碎念)
按照簡單工廠模式的思維,
我們必須為這個需求做出6個類別,
分別是(正常付費、打折付費、買多少回饋多少)x(一般發票、電子發票)的排列組合。
不行不行,假設客戶將來又提出要開統編的需求,我們就要寫8個類別了。
而且這樣也違反開放封閉原則。
每次有新需求都會改動所有的程式碼。
在我們研究一下後,發現了一個適合的設計模式:策略模式
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\OffPercentPay;
use App\StrategyPattern\CashRegister\FeedbackPay;
use App\StrategyPattern\CashRegister\NormalPay;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class CashContext
{
    /**
     * @var Payable
     */
    private $discountMethod;
    /**
     * @param int $originalPrice
     * @param string $discountType
     */
    public function __construct($originalPrice, $discountType)
    {
        $this->resolveDiscountMethod($originalPrice, $discountType);
    }
    /**
     * @param int $originalPrice
     * @param string $discountType
     */
    private function resolveDiscountMethod($originalPrice, $discountType)
    {
        switch ($discountType) {
            case '20% off':
                $this->discountMethod = new OffPercentPay($originalPrice, 0.2);
                break;
            case 'spend_300_feedback_100':
                $this->discountMethod = new FeedbackPay($originalPrice, 300, 100);
                break;
            default:
                $this->discountMethod = new NormalPay($originalPrice);
                break;
        }
    }
    public function pay()
    {
        return $this->discountMethod->pay();
    }
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\CashContext;
class Program
{
    /**
     * @var CashContext
     */
    private $cashContext;
    /**
     * @param int $originalPrice
     * @param string $discountType
     */
    public function __construct($originalPrice, $discountType)
    {
        $this->cashContext = new CashContext($originalPrice, $discountType);
    }
    public function pay()
    {
        return $this->cashContext->pay();
    }
}
這樣好像還看不出來有什麼好處,我們繼續實作。
<?php
namespace App\StrategyPattern\CashRegister\Contracts;
interface Receiptable
{
    public function getReceipt();
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class NormalReceipt implements Receiptable
{
    public function getReceipt()
    {
        return '一般發票';
    }
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class ElectronicReceipt implements Receiptable
{
    public function getReceipt()
    {
        return '電子發票';
    }
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\NormalPay;
use App\StrategyPattern\CashRegister\NormalReceipt;
use App\StrategyPattern\CashRegister\Contracts\Payable;
use App\StrategyPattern\CashRegister\ElectronicReceipt;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class CashContext
{
    /**
     * @var Payable
     */
    private $discountMethod;
    /**
     * @var Receiptable
     */
    private $receipt;
    /**
     * @param int $originalPrice
     * @param string $discountType
     * @param string $receiptType
     */
    public function __construct($originalPrice, $discountType, $receiptType)
    {
        $this->resolveDiscountMethod($originalPrice, $discountType);
        $this->resolveReceiptType($receiptType);
    }
    /**
     * @param int $originalPrice
     * @param string $discountType
     */
    private function resolveDiscountMethod($originalPrice, $discountType)
    {
        switch ($discountType) {
            case '20% off':
                $this->discountMethod = new OffPercentPay($originalPrice, 0.2);
                break;
            case 'spend_300_feedback_100':
                $this->discountMethod = new FeedbackPay($originalPrice, 300, 100);
                break;
            default:
                $this->discountMethod = new NormalPay($originalPrice);
                break;
        }
    }
    /**
     * @param string $receiptType
     */
    private function resolveReceiptType($receiptType)
    {
        switch ($receiptType) {
            case 'electronicReceipt':
                $this->receipt = new ElectronicReceipt();
                break;
            default:
                $this->receipt = new NormalReceipt();
                break;
        }
    }
    public function pay()
    {
        return $this->discountMethod->pay();
    }
    public function getReceipt()
    {
        return $this->receipt->getReceipt();
    }
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\CashContext;
class Program
{
    /**
     * @var CashContext
     */
    private $cashContext;
    /**
     * @param int $originalPrice
     * @param string $discountType
     * @param string $receiptType
     */
    public function __construct($originalPrice, $discountType, $receiptType)
    {
        $this->cashContext = new CashContext($originalPrice, $discountType, $receiptType);
    }
    public function pay()
    {
        return $this->cashContext->pay();
    }
    public function getReceipt()
    {
        return $this->cashContext->getReceipt();
    }
}
[單一職責原則]
將類別本身職責跟算法族的職責分離,就是策略模式的精神!
[開放封閉原則]
這下子,我們終於不會在客戶提出一個新需求時,影響到全部的既有程式碼了。
[介面隔離原則]
定義出付錢介面與發票介面,讓兩者不會互相影響。
可以交由各自的算法族,分別實現。
[依賴反轉原則]
消費明細類別依賴抽象的付錢介面與發票介面。
不同的算法族,實現對應的抽象介面。
最後附上類別圖:
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)
ʕ •ᴥ•ʔ:使用策略模式,我們依然會做出許多小類別(算法族/算法),
但因為切分的更細,也就更能因應需求去做變化。