Pattern: 裝飾者模式
Class Diagram: 漢堡點餐系統
需求一:客戶想要一個漢堡點餐系統
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\DecoratorPattern\Burger;
class Program { public function makeBigMac() { return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、起司、生菜、沙拉、麵包'; }
public function makeDoubleCheeseBurger() { 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
| <?php
namespace App\DecoratorPattern\Burger;
class Program { protected $cheese = 'normal';
public function customize($demand) { $this->cheese = $demand['cheese']; }
public function makeBigMac() { if ($this->cheese == 'double') { return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、兩倍起司、生菜、沙拉、麵包'; }
return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、起司、生菜、沙拉、麵包'; } }
|
在完成後,卻發現了一些問題:
- 每當有配料客製化需求時,我們必須改變大麥克的實作,違反開放封閉原則。
- 不同的漢堡種類,客製化的過程會違反 DRY 原則。
- 新增漢堡種類時,我們可能要實作目前所有客製化的選項。
藉由以上幾點,我們知道漢堡的實作與配料客製化是兩個不同的職責。
我們試著改用裝飾者模式實作。
1 2 3 4 5 6 7 8
| <?php
namespace App\DecoratorPattern\Burger\Contracts;
interface Food { public function getDescription(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\DecoratorPattern\Burger\ConcreteComponent;
use App\DecoratorPattern\Burger\Contracts\Food;
abstract class Burger implements Food { protected $name = '未知品項';
public function getDescription() { return $this->name . ':'; } }
|
1 2 3 4 5 6 7 8 9 10 11
| <?php
namespace App\DecoratorPattern\Burger\ConcreteComponent;
use App\DecoratorPattern\Burger\ConcreteComponent\Burger;
class BigMac extends Burger { protected $name = '大麥克'; }
|
漢堡在裝飾者模式中屬於裝飾物件類別,也就是被裝飾者。
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
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Contracts\Food;
abstract class Ingredient implements Food {
protected $food;
protected $name = '未知配料';
public function __construct(Food $food) { $this->food = $food; }
public function getDescription() { return $this->food->getDescription() . $this->name . '、'; } }
|
注意:Food包括漢堡類(被裝飾者)和配料類(裝飾者)。
而這邊__construct()與getDescription()方法的實作,
晚點會透過它們來實現裝飾。
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Bread extends Ingredient { protected $name = '麵包'; }
|
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Beef extends Ingredient { protected $name = '牛肉'; }
|
1 2 3 4 5 6 7 8 9 10 11
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Lettuce extends Ingredient { protected $name = '生菜'; }
|
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Salad extends Ingredient { protected $name = '沙拉'; }
|
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Cheese extends Ingredient { protected $name = '起司'; }
|
配料在裝飾者模式中屬於裝飾者類別,也就是裝飾者。
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
| <?php
namespace App\DecoratorPattern\Burger;
use App\DecoratorPattern\Burger\ConcreteComponent\BigMac; use App\DecoratorPattern\Burger\Decorator\Bread; use App\DecoratorPattern\Burger\Decorator\Beef; use App\DecoratorPattern\Burger\Decorator\Lettuce; use App\DecoratorPattern\Burger\Decorator\Cheese; use App\DecoratorPattern\Burger\Decorator\Salad; use App\DecoratorPattern\Burger\Decorator\Pickle; use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Program { public function makeBigMac() { $bigMac = new BigMac(); $topBread = new Bread($bigMac); $firstBeef = new Beef($topBread); $firstLettuce = new Lettuce($firstBeef); $firstSalad = new Salad($firstLettuce); $middleBread = new Bread($firstSalad); $secondBeef = new Beef($middleBread); $cheese = new Cheese($secondBeef); $secondLettuce = new Lettuce($cheese); $secondSalad = new Salad($secondLettuce); $bottomBread = new Bread($secondSalad);
return $this->getBurgerDescription($bottomBread); }
private function getBurgerDescription(Ingredient $burger) { $result = $burger->getDescription(); return $this->subLastPunctuation($result); }
private function subLastPunctuation($string) { return mb_substr($string, 0, mb_strlen($string, 'UTF-8') - 1, 'UTF-8'); } }
|
有用subLastPunctuation方法作文字修飾,可忽略。
透過Ingredient抽象類別的**__construct()包裝先前的類**,
當我們使用getDescription()時便能夠一層一層地往內部呼叫,
直到所有類別都被呼叫後,才動態產生所需的結果。
讓我們回到需求二:兩倍起司的客製化需求
- 我們先來改寫Ingredient抽象類別
新增customize()與changeDefaultIfDemanded()來作客製化的需求
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
| <?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Contracts\Food; use ReflectionClass;
abstract class Ingredient implements Food {
protected $food;
protected $name = '未知配料';
public function __construct(Food $food) { $this->food = $food; }
public function getDescription() { return $this->food->getDescription() . $this->name . '、'; }
public function customize($demand) { $this->changeDefaultIfDemanded($demand);
if ($this->food instanceof Ingredient) { $this->food->customize($demand); }
return $this; }
protected function changeDefaultIfDemanded($demand) { $ingredientName = $this->getIngredientName();
if (isset($demand[$ingredientName])) { $this->$ingredientName = $demand[$ingredientName]; } }
private function getIngredientName() { $reflectionClass = new ReflectionClass($this); return strtolower($reflectionClass->getShortName()); } }
|
注意:Food包括漢堡類(被裝飾者)和配料類(裝飾者)。
customize()方法遇到Food為配料時,會往內部呼叫,直到所有配料都被呼叫。
changeDefaultIfDemanded(),可理解成demand有提到的配料才會作客製化,可忽略。
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\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Cheese extends Ingredient { protected $name = '起司';
protected $cheese = 'normal';
public function getDescription() { if ($this->cheese == 'double') { return $this->food->getDescription() . '兩倍' . $this->name . '、'; }
return parent::getDescription(); } }
|
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
| <?php
namespace App\DecoratorPattern\Burger;
use App\DecoratorPattern\Burger\ConcreteComponent\BigMac; use App\DecoratorPattern\Burger\Decorator\Bread; use App\DecoratorPattern\Burger\Decorator\Beef; use App\DecoratorPattern\Burger\Decorator\Lettuce; use App\DecoratorPattern\Burger\Decorator\Cheese; use App\DecoratorPattern\Burger\Decorator\Salad; use App\DecoratorPattern\Burger\Decorator\Pickle; use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Program {
protected $demand = [];
public function setDemand($demand) { $this->demand = $demand; }
public function makeBigMac() { $bigMac = new BigMac(); $topBread = new Bread($bigMac); $firstBeef = new Beef($topBread); $firstLettuce = new Lettuce($firstBeef); $firstSalad = new Salad($firstLettuce); $middleBread = new Bread($firstSalad); $secondBeef = new Beef($middleBread); $cheese = new Cheese($secondBeef); $secondLettuce = new Lettuce($cheese); $secondSalad = new Salad($secondLettuce); $bottomBread = new Bread($secondSalad);
return $this->getBurgerDescription($bottomBread); }
private function subLastPunctuation($string) { return mb_substr($string, 0, mb_strlen($string, 'UTF-8') - 1, 'UTF-8'); }
private function getBurgerDescription(Ingredient $ingredient) { $result = $ingredient->customize($this->demand)->getDescription(); return $this->subLastPunctuation($result); } }
|
[單一職責原則]
我們將漢堡的實作與配料客製化視作兩種不同的職責。
[開放封閉原則]
無論是新增漢堡種類、新增配料或客製化,我們都能夠僅改到小部分的程式碼。
[裡氏替換原則]
遇到客製化需求時,我們可能會改寫配料中的getDescription()方法。
[介面隔離原則]
食物介面 - 使每個食物能透過getDescription()方法組裝。
漢堡抽象類別 - 被裝飾者,主要是為了與配料類(裝飾者)職責切割。
配料抽象類別 - 裝飾者,擁有一些客製化的方法。
[依賴反轉原則]
許多方法都依賴在食物介面、漢堡抽象類別、配料抽象類別。
ʕ •ᴥ•ʔ:這個Example有作testing ,被我refactor無數次,
是目前為止最喜歡的範例!