範例:漢堡點餐系統(裝飾者模式)

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';

/**
* @param array $demand
*/
public function customize($demand)
{
$this->cheese = $demand['cheese'];
}

public function makeBigMac()
{
if ($this->cheese == 'double') {
return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、兩倍起司、生菜、沙拉、麵包';
}

return '大麥克:麵包、牛肉、生菜、沙拉、麵包、牛肉、起司、生菜、沙拉、麵包';
}
}

在完成後,卻發現了一些問題:

  1. 每當有配料客製化需求時,我們必須改變大麥克的實作,違反開放封閉原則
  2. 不同的漢堡種類,客製化的過程會違反 DRY 原則
  3. 新增漢堡種類時,我們可能要實作目前所有客製化的選項。

藉由以上幾點,我們知道漢堡的實作配料客製化是兩個不同的職責。
我們試著改用裝飾者模式實作。


  • 首先定義食物介面
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
{
/**
* @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()方法的實作,
晚點會透過它們來實現裝飾。

  • 接著實作各式配料
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);
}

/**
* @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()時便能夠一層一層地往內部呼叫,
直到所有類別都被呼叫後,才動態產生所需的結果。


讓我們回到需求二:兩倍起司的客製化需求

  • 我們先來改寫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
{
/**
* @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有提到的配料才會作客製化,可忽略。


  • 接著改寫 Cheese 類別
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();
}
}

  • 最後改寫 Program 的實作
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
{
/**
* @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()方法組裝。
漢堡抽象類別 - 被裝飾者,主要是為了與配料類(裝飾者)職責切割。
配料抽象類別 - 裝飾者,擁有一些客製化的方法。

[依賴反轉原則]
許多方法都依賴在食物介面漢堡抽象類別配料抽象類別

ʕ •ᴥ•ʔ:這個Example有作testing ,被我refactor無數次,
是目前為止最喜歡的範例!