Decorator Pattern,PHP 裝飾者模式

macbook pro on white table

Decorator Pattern (裝飾者模式) 是一種結構型設計模式,它用於在不改變原有對象的情況下,為對象添加新的功能。這種模式通常會使用一個裝飾者類別,用來包裝原有的類實例。

本文使用一個範例,展示未使用和使用 Decorator 的差異,藉以了解 Decorator 的優點與實際用法。

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(MilkCoffeeChocolateCoffee),就可以創建各種不同的咖啡組合。以下是一些範例:

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 模式的高靈活性和易於擴展的特點。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *