本文同步更新於blog
需求一:客戶想要一個漢堡點餐系統
<?php
namespace App\DecoratorPattern\Burger;
class Program
{
public function makeBigMac()
{
return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、起司、生菜、沙拉、麵包';
}
public function makeDoubleCheeseBurger()
{
return '雙層牛肉吉事堡:麵包、酸菜、起司、牛肉、起司、牛肉、麵包';
}
}
需求二:客戶想要點餐能夠客製化 (比如說:兩倍起司)
<?php
namespace App\DecoratorPattern\Burger;
class Program
{
protected $cheese = 'normal';
/**
* @param array $demand
*/
public function customize($demand)
{
$this->cheese = $demand['cheese'];
}
public function makeBigMac()
{
if ($this->cheese == 'double') {
return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、兩倍起司、生菜、沙拉、麵包';
}
return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、起司、生菜、沙拉、麵包';
}
}
在完成後,卻發現了一些問題:
藉由以上幾點,我們知道漢堡的實作與配料客製化是兩個不同的職責。
我們試著改用裝飾者模式實作。
<?php
namespace App\DecoratorPattern\Burger\Contracts;
interface Food
{
public function getDescription();
}
<?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 . ':';
}
}
<?php
namespace App\DecoratorPattern\Burger\ConcreteComponent;
use App\DecoratorPattern\Burger\ConcreteComponent\Burger;
class BigMac extends Burger
{
protected $name = '大麥克';
}
漢堡在裝飾者模式中屬於裝飾物件類別,也就是被裝飾者。
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Contracts\Food;
abstract class Ingredient implements Food
{
/**
* @var 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()方法的實作,
晚點會透過它們來實現裝飾。
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Bread extends Ingredient
{
protected $name = '麵包';
}
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Beef extends Ingredient
{
protected $name = '牛肉';
}
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Lettuce extends Ingredient
{
protected $name = '生菜';
}
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Salad extends Ingredient
{
protected $name = '沙拉';
}
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Decorator\Ingredient;
class Cheese extends Ingredient
{
protected $name = '起司';
}
配料在裝飾者模式中屬於裝飾者類別,也就是裝飾者。
<?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);
}
/**
* @param Ingredient $burger
* @return string
*/
private function getBurgerDescription(Ingredient $burger)
{
$result = $burger->getDescription();
return $this->subLastPunctuation($result);
}
/**
* 去除最後一個標點符號
*
* @param string $string
* @return string
*/
private function subLastPunctuation($string)
{
return mb_substr($string, 0, mb_strlen($string, 'UTF-8') - 1, 'UTF-8');
}
}
有用subLastPunctuation方法作文字修飾,可忽略。
透過Ingredient抽象類別的 __construct()包裝先前的類,
當我們使用getDescription()時便能夠一層一層地往內部呼叫,
直到所有類別都被呼叫後,才動態產生所需的結果。
讓我們回到需求二:兩倍起司的客製化需求
<?php
namespace App\DecoratorPattern\Burger\Decorator;
use App\DecoratorPattern\Burger\Contracts\Food;
use App\DecoratorPattern\Burger\ConcreteComponent\Burger;
use ReflectionClass;
abstract class Ingredient implements Food
{
/**
* @var Food
*/
protected $food;
protected $name = '配料';
public function __construct(Food $food)
{
$this->food = $food;
}
public function getDescription()
{
return $this->food->getDescription() . $this->name . '、';
}
/**
* 讓最後一個裝飾者客製化自己外,也能客製化先前的裝飾者
*
* @param array $demand
* @return food
*/
public function customize($demand)
{
$this->changeDefaultIfDemanded($demand);
if ($this->food instanceof Ingredient) {
$this->food->customize($demand);
}
return $this;
}
/**
* 我們會利用該配料名稱,當作客製化的設定
*
* @param array $demand
*/
protected function changeDefaultIfDemanded($demand)
{
$ingredientName = $this->getIngredientName();
if (isset($demand[$ingredientName])) {
$this->$ingredientName = $demand[$ingredientName];
}
}
/**
* @return string
*/
private function getIngredientName()
{
$reflectionClass = new ReflectionClass($this);
return strtolower($reflectionClass->getShortName());
}
}
注意:Food包括漢堡類(被裝飾者)和配料類(裝飾者)。
customize()方法遇到Food為配料時,會往內部呼叫,直到所有配料都被呼叫。
changeDefaultIfDemanded(),可理解成demand有提到的配料才會作客製化,可忽略。
<?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();
}
}
<?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
{
/**
* @var array
*/
protected $demand = [];
/**
* @param array $demand
*/
public function setDemand($demand)
{
$this->demand = $demand;
}
/**
* @return string
*/
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);
}
/**
* 去除最後一個標點符號
*
* @param string $string
* @return string
*/
private function subLastPunctuation($string)
{
return mb_substr($string, 0, mb_strlen($string, 'UTF-8') - 1, 'UTF-8');
}
/**
* @param Ingredient $ingredient
* @return string
*/
private function getBurgerDescription(Ingredient $ingredient)
{
$result = $ingredient->customize($this->demand)->getDescription();
return $this->subLastPunctuation($result);
}
}
[單一職責原則]
我們將漢堡的實作與配料客製化視作兩種不同的職責。
[開放封閉原則]
無論是新增漢堡種類、新增配料或客製化,我們都能夠僅改到小部分的程式碼。
[裡氏替換原則]
遇到客製化需求時,我們可能會改寫配料中的getDescription()方法。
[介面隔離原則]
食物介面 - 使每個食物能透過getDescription()方法組裝。
漢堡抽象類別 - 被裝飾者,主要是為了與配料類(裝飾者)職責切割。
配料抽象類別 - 裝飾者,擁有一些客製化的方法。
[依賴反轉原則]
許多方法都依賴在食物介面、漢堡抽象類別、配料抽象類別。
最後附上類別圖:
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)
ʕ •ᴥ•ʔ:這個Example有作testing ,被我refactor無數次,
是目前為止最喜歡的範例!
筆者您好:
很喜歡您簡明扼要的解釋設計模式,受惠良多,十分感謝!
想冒昧請教這邊 Food 介面和 Ingredient 抽象類別您是如何判斷有聚合關係的呢?
嗨你好,謝謝你喜歡這個系列。
關於UML的部分,稍微翻了下手頭的書,
這個地方的箭頭在兩本書有不同的詮釋。
書名 | 關係 |
---|---|
大話設計模式 | 聚合關係 |
深入淺出系列 | 關聯關係 |
我自己的觀點會是,因為裝飾者模式的特殊性,
會像俄羅斯娃娃一樣,一個包著一個,
所以Food中通常會包含多個Ingredient。
順道一提,中午還沒查閱書時,發現這邊或許可以用關聯關係。
看起來會相對簡單一些。
也歡迎提出你的想法!
ʕ •ᴥ•ʔ:噢,是本系列的第一個留言~
最近寫橋接模式的範例時,發現有一樣的聚合關係判斷。
兩者相似處應該是Food與Ingredient皆為抽象。
所以最新理解是:跟裝飾者模式的特性無關。
我之前應該想錯了。
ʕ •ᴥ•ʔ:希望你能看得到這則留言。