Specification Pattern,PHP 規格模式

closeup photo of book pages

簡介

在這篇文章中,將會探討在 PHP 中實現 Specification Pattern 的方法。

Specification Pattern 是一種行為設計模式,它允許我們將業務規則封裝到一個單一的類別中,並使其可組合。這樣可以創建更靈活和可維護的系統。

我們將通過一個以咖啡為主題的範例來展示如何使用這個模式,並比較在使用和不使用 Specification Pattern 的情況下的代碼結構和可維護性。

什麼是 Specification Pattern

在 Specification Pattern 這個模式中,每個規則或規範都是一個單獨的類別,這些類別可以通過邏輯操作(如 AND、OR 和 NOT)組合在一起,形成更複雜的規範。

specification pattern class diagram
規格模式類別圖

想像一下你在經營一家咖啡店,你有各種不同類型的咖啡豆,每種咖啡豆都有不同的特性,比如產地、烘焙程度和風味。現在,你想要創建一個系統來幫助你選擇適合特定顧客口味的咖啡豆。你可以使用 Specification Pattern 來實現這個功能。

你可以為每種特性創建一個規範,比如 OriginSpecificationRoastLevelSpecificationFlavorProfileSpecification。然後,根據顧客的需求將這些規範組合在一起,創建一個複合規範來找到最適合的咖啡豆。

什麼狀況適合使用 The Specification Pattern

Specification Pattern 非常適合於需要根據一組動態條件過濾或選擇對象的情況。它提供了一種將業務規則從業務對象中分離出來的方法,使得業務規則更容易被理解和維護。這在需要處理複雜業務邏輯或規則的系統中尤其有用,比如電子商務平台中的產品過濾、信貸審批系統中的風險評估等場景。

範例教學

接下來的部分,將通過一個具體的範例來展示如何在 PHP 中實現和使用 Specification Pattern。

原始範例,未使用 Specification Pattern

假設有一個咖啡選擇應用程式,它根據客戶的喜好來選擇咖啡。以下是未使用 Specification Pattern 的 PHP 程式碼:

<?php
// 定義一個 CoffeeSelection 類別來處理咖啡選擇的邏輯
class CoffeeSelection {
    // selectCoffee 方法接受咖啡的類型、大小、是否加烈和是否冷飲作為參數
    public function selectCoffee($type, $size, $extraShot, $cold) {
        // 檢查咖啡的類型是否為 Espresso,大小是否為 Large,是否加烈,並且不是冷飲
        if ($type === 'Espresso' && $size === 'Large' && $extraShot && !$cold) {
            // 如果條件匹配,則返回描述這種咖啡的字串
            return 'Large Espresso with extra shot';
        // 檢查咖啡的類型是否為 Latte,大小是否為 Medium,不加烈,並且是冷飲
        } elseif ($type === 'Latte' && $size === 'Medium' && !$extraShot && $cold) {
            // 如果條件匹配,則返回描述這種咖啡的字串
            return 'Medium Iced Latte';
        }
        // 如果有更多的咖啡選擇條件,可以繼續添加更多的 elseif 分支來處理

        // 如果沒有任何條件匹配,則默認返回 null
        // (這裡沒有明確的 return null,但在 PHP 中,如果一個函數不返回任何值,則默認返回 null)
    }
}

// 創建一個 CoffeeSelection 物件實例
$coffeeSelection = new CoffeeSelection();
// 調用 selectCoffee 方法,傳入咖啡的類型、大小、是否加烈和是否冷飲作為參數
// 並且將返回的結果輸出
echo $coffeeSelection->selectCoffee('Espresso', 'Large', true, false);
// 輸出將會是 'Large Espresso with extra shot',因為這是匹配的條件

上段程式碼展示了一個簡單的咖啡選擇類別,它根據提供的參數返回一個描述咖啡的字串。這是一個基於條件判斷的簡單實現,但在條件增多時可能會變得難以維護。

如果需要處理更多的咖啡選擇條件,可能會需要一個更靈活和可擴展的解決方案,例如使用 Specification Pattern。

改善範例,使用 Specification Pattern

接下來將重構上面的範例,使用 Specification Pattern 來實現相同的功能。使我們的代碼更加清晰、靈活和易於維護。

<?php
// 定義一個咖啡規格的介面,所有的規格類別都必須實現這個介面
interface CoffeeSpecification {
    public function isSatisfiedBy($coffee);
}

// 實現咖啡類型規格的類別
class TypeSpecification implements CoffeeSpecification {
    private $type;

    // 建構函數接收一個咖啡類型
    public function __construct($type) {
        $this->type = $type;
    }

    // 檢查咖啡是否符合這個類型
    public function isSatisfiedBy($coffee) {
        return $coffee->type === $this->type;
    }
}

// 實現咖啡大小規格的類別
class SizeSpecification implements CoffeeSpecification {
    private $size;

    // 建構函數接收一個咖啡大小
    public function __construct($size) {
        $this->size = $size;
    }

    // 檢查咖啡是否符合這個大小
    public function isSatisfiedBy($coffee) {
        return $coffee->size === $this->size;
    }
}

// 實現額外加烈咖啡規格的類別
class ExtraShotSpecification implements CoffeeSpecification {
    private $extraShot;

    // 建構函數接收一個布林值,表示是否需要額外加烈
    public function __construct($extraShot) {
        $this->extraShot = $extraShot;
    }

    // 檢查咖啡是否符合額外加烈的要求
    public function isSatisfiedBy($coffee) {
        return $coffee->extraShot === $this->extraShot;
    }
}

// 實現冷飲咖啡規格的類別
class ColdSpecification implements CoffeeSpecification {
    private $cold;

    // 建構函數接收一個布林值,表示咖啡是否為冷飲
    public function __construct($cold) {
        $this->cold = $cold;
    }

    // 檢查咖啡是否為冷飲
    public function isSatisfiedBy($coffee) {
        return $coffee->cold === $this->cold;
    }
}

// 咖啡選擇器類別,負責根據提供的規格選擇咖啡
class CoffeeSelector {
    // 根據一組規格和一組咖啡選項來選擇咖啡
    public function selectCoffee($specifications, $coffees) {
        foreach ($coffees as $coffee) {
            $satisfied = true;
            // 檢查所有規格是否都滿足
            foreach ($specifications as $specification) {
                if (!$specification->isSatisfiedBy($coffee)) {
                    $satisfied = false;
                    break;
                }
            }
            // 如果所有規格都滿足,則返回這個咖啡
            if ($satisfied) {
                return $coffee;
            }
        }
        // 如果沒有咖啡滿足所有規格,則返回 null
        return null;
    }
}

// 咖啡選項的陣列,每個咖啡選項都是一個物件
$coffees = [
    (object)['type' => 'Espresso', 'size' => 'Large', 'extraShot' => true, 'cold' => false],
    (object)['type' => 'Latte', 'size' => 'Medium', 'extraShot' => false, 'cold' => true],
    // 更多咖啡選項...
];

// 建立一組規格
$specifications = [
    new TypeSpecification('Espresso'),
    new SizeSpecification('Large'),
    new ExtraShotSpecification(true),
    new ColdSpecification(false)
];

// 創建咖啡選擇器實例
$coffeeSelector = new CoffeeSelector();
// 使用規格和咖啡選項來選擇咖啡
$selectedCoffee = $coffeeSelector->selectCoffee($specifications, $coffees);
// 輸出選擇的咖啡,如果沒有符合的咖啡,則輸出 'No coffee found'
echo $selectedCoffee ? $selectedCoffee->type : 'No coffee found';

這個範例的優點是,每個規格都是獨立的,我們可以很容易地新增或移除規格,而不需要修改 CoffeeSelector 類別。這使得程式碼更容易維護和擴展。

把規格都封裝在自己的類別中,就可以獨立地測試和重用。CoffeeSelector 類別則負責根據這些規格來選擇合適的咖啡。這種設計使得程式碼更加清晰,並且當需要新增或修改規格時,只需要修改相應的規格類別,而不是整個選擇邏輯。

其他類似的 Design Pattern

Specification Pattern 是一種行為設計模式,它與其他行為設計模式如 Strategy Pattern、Command Pattern 和 Observer Pattern 有相似之處。這些模式都涉及到將算法或行為從其使用的上下文中分離出來,以提高系統的靈活性和可維護性。

然而,Specification Pattern 的獨特之處在於其對規則或條件的封裝,並提供了一種將這些規則或條件組合在一起的機制。

設計模式描述使用案例
規範模式
(Specification Pattern)
將業務規則封裝成可組合的單元。需要基於動態條件進行複雜的篩選和選擇。
策略模式
(Strategy Pattern)
定義一系列算法,封裝每一個算法,並使它們可互換。當你想定義一個類別,它將擁有與其他類別相似的行為時。
命令模式
(Command Pattern)
將請求封裝為一個物件,從而使客戶端可以用隊列、請求和操作來參數化。當你需要用操作來參數化物件時。
觀察者模式
(Observer Pattern)
定義物件間一對多的依賴關係,以便當一個物件改變狀態時,所有依賴它的物件都會得到通知並自動更新。當一個物件的變更需要改變其他物件,而你不知道需要改變多少物件時。

參考資料

  1. Specification Pattern – Martin Fowler
  2. PHP Design Patterns – GitHub

發佈留言

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