Menu
Coddy logo textTech

デコレーターパターン

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

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

コーヒーショップを想像してみてください。まずは普通のコーヒーから始め、それからミルク、砂糖、ホイップクリームなどのトッピングを追加します。

各追加要素は元のコーヒーをラップし、その説明と価格を加算します。重要なポイントは、ベースとなるコーヒーとデコレータの両方が同じインターフェースを共有している点です。

<?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;
    }
}

デコレータは同じインターフェースを実装し、別の 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

各デコレータは前のオブジェクトをラップし、チェーンを形成します。このパターンにより、考えられるすべての組み合わせに対してクラスを作成することなく、任意の順序と量で振る舞いを組み合わせることができます。ミルクを2倍にする必要がありますか?その場合は、単に2回ラップするだけです。

challenge icon

チャレンジ

簡単

デコレーターパターン(Decorator Pattern)を使用して、ピザ注文システムを構築しましょう。基本のピザから始めて、トッピングを動的に追加していきます。レッスンのコーヒーの例と同じように、各トッピングがピザをラップし、説明と価格を追加します。

コードは4つのファイルに分けて構成します:

  • Pizza.php — すべてのピザとトッピングの契約を定める Pizza インターフェースを定義します。getDescription(): stringgetPrice(): float の2つのメソッドを持つ必要があります。
  • BasePizza.php — Pizza インターフェースを読み込み、それを実装する MargheritaPizza クラスを作成します。これは基本となるピザで、説明は "Margherita"、価格は 8.00 です。
  • Toppings.php — Pizza インターフェースを読み込み、それを実装する3つのデコレータークラスを作成します。各デコレーターは Pizza オブジェクトをラップし(コンストラクタで受け取ります)、説明と価格の両方を強化します:
    • CheeseTopping — 説明に ", Extra Cheese" を追加し、価格に 1.50 を追加します
    • PepperoniTopping — 説明に ", Pepperoni" を追加し、価格に 2.00 を追加します
    • MushroomTopping — 説明に ", Mushrooms" を追加し、価格に 1.25 を追加します
  • main.php — BasePizza と Toppings ファイルを読み込みます。1つの入力を受け取ります。それは追加するトッピングのカンマ区切りリストです(例:"cheese,pepperoni""mushroom,cheese,cheese")。

    MargheritaPizza から開始します。入力された各トッピングに対して、適切なデコレーターでピザをラップします:

    • "cheese"CheeseTopping でラップします
    • "pepperoni"PepperoniTopping でラップします
    • "mushroom"MushroomTopping でラップします

    すべてのトッピングを適用した後、最終的なピザの説明と価格を以下の形式で出力してください:

    [description] = $[price]

    価格は小数点以下2桁までフォーマットしてください。

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

チートシート

Decorator パターンは、実行時にオブジェクトをデコレータオブジェクトでラップすることにより、オブジェクトに振る舞いを動的に追加する構造デザインパターンです。

ベースオブジェクトとデコレータの両方が同じインターフェースを実装します:

<?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;
    }
}

デコレータは、同じインターフェース型の別のオブジェクトをラップします:

<?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

デコレータは、あらゆる組み合わせに対してクラスを作成することなく、任意の順序で何度でも適用できます。

自分で試してみよう

<?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腕試し

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

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