Menu
Coddy logo textTech

Decoratorパターン

CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 77/91。

Decorator Patternは、デコレーターと呼ばれる特別なラッパーオブジェクトでオブジェクトをラップすることにより、新しい振る舞いを動的に追加できる構造設計パターンです。コンパイル時に振る舞いを追加する継承とは異なり、デコレーターは実行時に振る舞いを追加します。

コーヒーショップを想像してみてください。まず基本のコーヒーから始め、次に Milk、砂糖、ホイップクリームなどの追加を加えます。

それぞれの追加要素は元のcoffeeをラップし、そのdescriptionとコストにaddsします。重要な洞察は、base coffeeとデコレーターの両方が同じinterfaceを共有していることです。

<?php
interface Coffee {
    public function getDescription(): string;
    public function getCost(): float;
}

class SimpleCoffee implements Coffee {
    public function getDescription(): string {
        return "Simple Coffee";
    }
    
    public function getCost(): float {
        return 2.00;
    }
}

Decoratorは同じinterfaceを実装し、別のCoffeeオブジェクトをラップします。

<?php
class MilkDecorator implements Coffee {
    public function __construct(private Coffee $coffee) {}
    
    public function getDescription(): string {
        return $this->coffee->getDescription() . ", Milk";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 0.50;
    }
}

class SugarDecorator implements Coffee {
    public function __construct(private Coffee $coffee) {}
    
    public function getDescription(): string {
        return $this->coffee->getDescription() . ", Sugar";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 0.25;
    }
}

$coffee = new SimpleCoffee();
$coffee = new MilkDecorator($coffee);
$coffee = new SugarDecorator($coffee);

echo $coffee->getDescription() . " = $" . $coffee->getCost();

出力:

Simple Coffee, Milk, Sugar = $2.75

each decorator は前の object をラップし、チェーンを形成します。このパターンを使うと、考えられるすべての組み合わせごとに class を作成することなく、動作を任意の順序と数量で組み合わせられます。Milk を2倍にしたいですか? そのまま2回ラップしてください。

challenge icon

チャレンジ

簡単

デコレータパターンを使って、ピザの注文システムを構築しましょう。まず基本的なピザから始め、トッピングを動的に追加します。各トッピングはピザをラップし、その説明と価格に追加します。これはレッスンの coffee の例と同じです。

コードを4つのファイルに分けて整理します。

  • Pizza.php:すべてのピザとトッピングの契約を定義する Pizza interface を Define します。次の2つの methods を持たせます:getDescription(): stringgetPrice(): float
  • BasePizza.php:Pizza interface を読み込み、それを implements する MargheritaPizza class を Create します。これは、description が "Margherita"、price が 8.00 の base pizza です。
  • Toppings.php:Pizza interface を読み込み、それを implements する3つの decorator class を Create します。各 decorator は Pizza object をラップし(constructor で受け取ります)、description と price の両方を拡張します。
    • CheeseTopping:description に ", Extra Cheese" を、price に 1.50 を adds
    • PepperoniTopping:description に ", Pepperoni" を、price に 2.00 を adds
    • MushroomTopping:description に ", Mushrooms" を、price に 1.25 を adds
  • main.php:BasePizza ファイルと Toppings ファイルを読み込みます。追加するトッピングの comma 区切りの list を1つ input として受け取ります(例:"cheese,pepperoni" または "mushroom,cheese,cheese")。

    MargheritaPizza から始めます。input 内の each トッピングについて、appropriate decorator でピザをラップします。

    • "cheese" の場合は CheeseTopping でラップします
    • "pepperoni" の場合は PepperoniTopping でラップします
    • "mushroom" の場合は MushroomTopping でラップします

    すべてのトッピングを適用した後、final pizza の description と price を次の format で出力します。

    [description] = $[price]

    price は小数点以下2桁に Format してください。

同じトッピングを複数回適用できることに注目してください。ダブルチーズを注文する場合は、単に CheeseTopping で2回ラップします。この柔軟性こそが、デコレータパターンの力です。

自分で試してみよう

<?php
require_once 'BasePizza.php';
require_once 'Toppings.php';

// 入力を読み取る - トッピングのカンマ区切りリスト
$input = trim(fgets(STDIN));
$toppings = explode(',', $input);

// ベースのMargheritaPizzaから始める
$pizza = new MargheritaPizza();

// TODO: 各トッピングをループし、適切なデコレータでピザをラップする
// - "cheese" -> CheeseToppingでラップする
// - "pepperoni" -> PepperoniToppingでラップする
// - "mushroom" -> MushroomToppingでラップする

// TODO: 結果を次の形式で出力する: [description] = $[price]
// number_format()を使って価格を小数点以下2桁にフォーマットする

?>
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: PHPオンラインコンパイラ