本文同步更新於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皆為抽象。
所以最新理解是:跟裝飾者模式的特性無關。
我之前應該想錯了。
ʕ •ᴥ•ʔ:希望你能看得到這則留言。