CleanMyMac

Decorator Pattern,PHP 裝飾者模式

Feature image for Decorator Pattern,PHP 裝飾者模式

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

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

Coffee 範例(未使用 Decorator Pattern)

這段 PHP 程式碼展示了如何使用多個不同的類別來代表不同種類的咖啡,而非使用 Decorator Pattern。我將分步驟解釋每個部分。

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, 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 Pattern 的核心部分。首先,建立一個加牛奶的咖啡(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),就可以創建各種不同的咖啡組合。以下是一些範例:

單純的咖啡

$coffee = new SimpleCoffee();
echo $coffee->getCost(); // 輸出: 10
echo $coffee->getDescription(); // 輸出: Simple coffee

加牛奶的咖啡

$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
echo $coffee->getCost(); // 輸出: 12
echo $coffee->getDescription(); // 輸出: Simple coffee, milk

加巧克力的咖啡

$coffee = new SimpleCoffee();
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 15
echo $coffee->getDescription(); // 輸出: Simple coffee, chocolate

加牛奶和巧克力的咖啡

$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 17
echo $coffee->getDescription(); // 輸出: Simple coffee, milk, chocolate

雙倍牛奶的咖啡

$coffee = new SimpleCoffee();
$coffee = new MilkCoffee($coffee);
$coffee = new MilkCoffee($coffee);
echo $coffee->getCost(); // 輸出: 14
echo $coffee->getDescription(); // 輸出: Simple coffee, milk, milk

雙倍巧克力的咖啡

$coffee = new SimpleCoffee();
$coffee = new ChocolateCoffee($coffee);
$coffee = new ChocolateCoffee($coffee);
echo $coffee->getCost(); // 輸出: 20
echo $coffee->getDescription(); // 輸出: Simple coffee, chocolate, chocolate

雙倍牛奶和雙倍巧克力的咖啡

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