Pattern: 策略模式
Class Diagram: 收銀機
需求一:客戶想要一台收銀機
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
namespace App\StrategyPattern\CashRegister;
class Program {
private $originalPrice;
public function __construct($originalPrice) { $this->originalPrice = $originalPrice; }
public function pay() { return $this->originalPrice; } }
|
需求二:客戶想要有一個優惠活動 (打8折)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?php
namespace App\StrategyPattern\CashRegister;
class Program {
private $originalPrice;
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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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。
讓我們利用簡單工廠改造它。
1 2 3 4 5 6 7 8
| <?php
namespace App\StrategyPattern\CashRegister\Contracts;
interface Payable { public function pay(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class NormalPay implements Payable {
private $originalPrice;
public function __construct($originalPrice) { $this->originalPrice = $originalPrice; }
public function pay() { return $this->originalPrice; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class OffPercentPay implements Payable {
private $originalPrice;
private $offPercent;
public function __construct($originalPrice, $offPercent) { $this->originalPrice = $originalPrice; $this->offPercent = $offPercent; }
public function pay() { return $this->originalPrice * (1 - $this->offPercent); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class FeedbackPay implements Payable {
private $originalPrice;
private $priceCondition;
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個類別了。
而且這樣也違反開放封閉原則。
每次有新需求都會改動所有的程式碼。
在我們研究一下後,發現了一個適合的設計模式:策略模式
- 我們首先製作一個消費明細類別,它擁有所有的優惠方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <?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 {
private $discountMethod;
public function __construct($originalPrice, $discountType) { $this->resolveDiscountMethod($originalPrice, $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(); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\CashContext;
class Program {
private $cashContext;
public function __construct($originalPrice, $discountType) { $this->cashContext = new CashContext($originalPrice, $discountType); }
public function pay() { return $this->cashContext->pay(); } }
|
這樣好像還看不出來有什麼好處,我們繼續實作。
1 2 3 4 5 6 7 8
| <?php
namespace App\StrategyPattern\CashRegister\Contracts;
interface Receiptable { public function getReceipt(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class NormalReceipt implements Receiptable { public function getReceipt() { return '一般發票'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class ElectronicReceipt implements Receiptable { public function getReceipt() { return '電子發票'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <?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 {
private $discountMethod;
private $receipt;
public function __construct($originalPrice, $discountType, $receiptType) { $this->resolveDiscountMethod($originalPrice, $discountType); $this->resolveReceiptType($receiptType); }
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; } }
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(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\CashContext;
class Program {
private $cashContext;
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(); } }
|
[單一職責原則]
將類別本身職責跟算法族的職責分離,就是策略模式的精神!
[開放封閉原則]
這下子,我們終於不會在客戶提出一個新需求時,影響到全部的既有程式碼了。
[介面隔離原則]
定義出付錢介面與發票介面,讓兩者不會互相影響。
可以交由各自的算法族,分別實現。
[依賴反轉原則]
消費明細類別依賴抽象的付錢介面與發票介面。
不同的算法族,實現對應的抽象介面。
ʕ •ᴥ•ʔ:使用策略模式,我們依然會做出許多小類別(算法族/算法),
但因為切分的更細,也就更能因應需求去做變化。