Decorator Pattern (裝飾者模式) 是一種結構型設計模式,它用於在不改變原有對象的情況下,為對象添加新的功能。這種模式通常會使用一個裝飾者類別,用來包裝原有的類實例。
本文使用一個範例,展示未使用和使用 Decorator 的差異,藉以了解 Decorator 的優點與實際用法。
Table of Contents
Coffee 範例(未使用 Decorator Pattern)
這段 PHP 代碼展示了如何使用多個不同的類來代表不同種類的咖啡,而非使用Decorator 模式。我將分步驟解釋每個部分。
STEP 1:SimpleCoffee 類別
這個類別定義了最基本的咖啡,包括其成本和描述。
class SimpleCoffee {
public function getCost() {
return 10;
}
public function getDescription() {
return 'Simple coffee';
}
}
STEP 2:MilkCoffee 類別
這個類別代表了加了牛奶的咖啡,也有其自己的成本和描述。
class MilkCoffee {
public function getCost() {
return 12;
}
public function getDescription() {
return 'Simple coffee, milk';
}
}
STEP 3:ChocolateCoffee 類別
這個類別代表了加了牛奶和巧克力的咖啡。注意,這裡不是在 MilkCoffee
基礎上添加巧克力,而是直接創建了一個全新的類別。
class ChocolateCoffee {
public function getCost() {
return 17;
}
public function getDescription() {
return 'Simple coffee, chocolate';
}
}
STEP 4:ChocolateMilkCoffee 類別
這個類別代表了加了牛奶和兩倍巧克力的咖啡。它也有自己獨立的成本和描述。
class ChocolateMilkCoffee {
public function getCost() {
return 30;
}
public function getDescription() {
return 'Simple coffee, milk, chocolate';
}
}
STEP 5:使用各種咖啡類別
最後,代碼示範了如何使用這些咖啡類別來獲得成本和描述。
// 使用
$coffee = new SimpleCoffee();
echo $coffee->getCost(); // 輸出: 10
echo $coffee->getDescription(); // 輸出: Simple coffee
$milkCoffee = new MilkCoffee();
echo $milkCoffee->getCost(); // 輸出: 12
echo $milkCoffee->getDescription(); // 輸出: Simple coffee, milk
$chocolateCoffee = new ChocolateCoffee();
echo $chocolateCoffee->getCost(); // 輸出: 17
echo $chocolateCoffee->getDescription(); // 輸出: Simple coffee, milk, chocolate
$chocolateMilkCoffee = new ChocolateMilkCoffee();
echo $chocolateMilkCoffee->getCost(); // 輸出: 30
echo $chocolateMilkCoffee->getDescription(); // 輸出: Simple coffee, milk, chocolate
這樣的設計有一些明顯的缺點~
代碼重複: 每一種不同的咖啡(如有牛奶、有糖等)都需要一個全新的類。這會導致大量重複的代碼。
不易擴展: 如果你想為咖啡添加新的修飾(例如糖、冰塊等),你將需要創建更多新的類。這樣會讓系統變得非常冗長和難以維護。
違反開放/封閉原則: 每次添加新的修飾,你都需要修改現有的代碼,這違反了軟件應該對擴展開放,對修改封閉的開放/封閉原則。
維護困難: 如同上面所述,一旦基本咖啡的價格或其他特性需要更改,你可能需要在多個類中進行修改。
改善作法 (使用 Decorator Pattern)
STEP 1:定義基本介面
首先,我們需要一個共同的介面,以便所有的咖啡和咖啡的裝飾者都可以實現它。這個介面包括 getCost()
和 getDescription()
這兩個方法。
<?php
interface Coffee {
public function getCost();
public function getDescription();
}
STEP 2:建立基本咖啡類
接著,建立一個簡單咖啡(SimpleCoffee)類別,這個類別會實現上面定義的 Coffee
介面。
class SimpleCoffee implements Coffee {
public function getCost() {
return 10;
}
public function getDescription() {
return 'Simple coffee';
}
}
這個簡單咖啡類代表了最基本的咖啡,它有固定的價格和描述。
STEP 3:建立牛奶裝飾
現在,開始進入 Decorator 模式的核心部分。首先,建立一個加牛奶的咖啡(MilkCoffee)裝飾者。
class MilkCoffee implements Coffee {
protected $coffee;
public function __construct(Coffee $coffee) {
$this->coffee = $coffee;
}
public function getCost() {
return $this->coffee->getCost() + 2;
}
public function getDescription() {
return $this->coffee->getDescription() . ', milk';
}
}
STEP 4:實際使用
最後,我們可以創建一個咖啡物件並用裝飾者來裝飾它。
// 創建一個簡單咖啡物件
$coffee = new SimpleCoffee();
// 用加牛奶的咖啡裝飾它
$coffee = new MilkCoffee($coffee);
// 輸出結果
echo $coffee->getCost(); // 輸出: 12
echo $coffee->getDescription(); // 輸出: Simple coffee, milk
STEP 5:擴展 ChocolateCoffee 裝飾者
現在可以很容易地擴展 Decorator 模式,加入一個新的 ChocolateCoffee
裝飾者。
class ChocolateCoffee implements Coffee {
protected $coffee;
public function __construct(Coffee $coffee) {
$this->coffee = $coffee;
}
public function getCost() {
return $this->coffee->getCost() + 5;
}
public function getDescription() {
return $this->coffee->getDescription() . ', chocolate';
}
}
STEP 6:實際使用 ChocolateCoffee
接著,再次使用裝飾者來裝飾咖啡物件。
// 創建一個簡單咖啡物件
$coffee = new SimpleCoffee();
// 用牛奶咖啡裝飾它
$coffee = new MilkCoffee($coffee);
// 再用巧克力咖啡裝飾它
$coffee = new ChocolateCoffee($coffee);
// 輸出結果
echo $coffee->getCost(); // 輸出: 17
echo $coffee->getDescription(); // 輸出: Simple coffee, milk, chocolate
如您所見,添加 ChocolateCoffee
裝飾者變得非常容易和靈活。我們只需要創建一個新的裝飾者類,並在其中實現我們想要添加或修改的功能。這樣做不會影響到其他部分的代碼,使得整個系統更容易擴展和維護。
這個模式允許我們以很高的靈活性來組合不同的功能,而不需要創建大量的新類別來表示每一種可能的組合。
發揮 Decorator 的極限
使用目前已有的 Decorator(MilkCoffee
和 ChocolateCoffee
),就可以創建各種不同的咖啡組合。以下是一些範例:
1. 單純的咖啡
$coffee = new SimpleCoffee();
echo $coffee->getCost(); // 輸出: 10
echo $coffee->getDescription(); // 輸出: Simple coffee
2. 加牛奶的咖啡
$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
echo $coffee->getCost(); // 輸出: 12
echo $coffee->getDescription(); // 輸出: Simple coffee, milk
3. 加巧克力的咖啡
$coffee = new SimpleCoffee();
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 15
echo $coffee->getDescription(); // 輸出: Simple coffee, chocolate
4. 加牛奶和巧克力的咖啡
$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 17
echo $coffee->getDescription(); // 輸出: Simple coffee, milk, chocolate
5. 雙倍牛奶的咖啡
$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
$coffee = new MilkCoffee($coffee);
echo $coffee->getCost(); // 輸出: 14
echo $coffee->getDescription(); // 輸出: Simple coffee, milk, milk
6. 雙倍巧克力的咖啡
$coffee = new SimpleCoffee();
$coffee = new ChocolateCoffee($coffee);
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 20
echo $coffee->getDescription(); // 輸出: Simple coffee, chocolate, chocolate
7. 雙倍牛奶和雙倍巧克力的咖啡
$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
$coffee = new MilkCoffee($coffee);
$coffee = new ChocolateCoffee($coffee);
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 22
echo $coffee->getDescription(); // 輸出: Simple coffee, milk, milk, chocolate, chocolate
這樣,我們就創建了 7 種不同的咖啡,而只需要使用 1 個基礎類別和 2 個裝飾者。這展示了使用 Decorator 模式的高靈活性和易於擴展的特點。