Menu
Coddy logo textTech

デコレーターパターン

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

Decoratorパターンは、オブジェクトをデコレータと呼ばれる特殊なオブジェクトでラップすることにより、オブジェクトに新しい振る舞いを動的に追加することを可能にします。コンパイル時に振る舞いを追加する継承とは異なり、デコレータを使用すると、元のクラスを修正することなく、実行時に機能を拡張できます。

このパターンは、元のオブジェクトとデコレータの両方に同じインターフェースを実装させることで機能します。各デコレータはコンポーネントへの参照を保持し、ラップされたオブジェクトに処理を委譲する前または後に、独自の振る舞いを追加します:

#include <iostream>
#include <memory>

// コンポーネントのインターフェース
class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

// 具体的なコンポーネント
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

// ベースデコレータ
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

// 具体的なデコレータ
class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Sugar";
    }
    double getCost() const override { return coffee->getCost() + 0.2; }
};

デコレータは積み重ねることができます。各デコレータが前のデコレータをラップし、機能を一層ずつ積み上げていきます。

int main() {
    std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
    order = std::make_unique<MilkDecorator>(std::move(order));
    order = std::make_unique<SugarDecorator>(std::move(order));
    
    std::cout << order->getDescription() << ": $" << order->getCost();
    // 出力: Coffee, Milk, Sugar: $2.7
}

他のオブジェクトに影響を与えることなく、動的にオブジェクトに責務を追加する必要がある場合や、サブクラス化による機能拡張を行うとクラス数が爆発的に増えてしまう場合に、Decoratorを使用します。

challenge icon

チャレンジ

簡単

Decorator パターンを使用して、ピザ注文システムを構築しましょう。顧客がベースとなるピザから始めて、さまざまなトッピングを追加できる柔軟なシステムを作成します。各トッピングは前の注文をラップし、独自の価格と説明を追加します。これはデコレータの完璧なユースケースです。サブクラスを大量に作成することなく、トッピングを任意の組み合わせで積み重ねることができます。

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

  • Pizza.h: コンポーネントインターフェースとベースとなるピザクラスを定義します。

    純粋仮想関数 getDescription()(文字列を返す)と getCost()(doubleを返す)、および仮想デストラクタを持つ抽象クラス Pizza を作成します。

    次に、ベースとなるピザを表す具体的な PlainPizza クラスを作成します。説明として "Pizza" を返し、基本料金を 8.0 とします。

  • ToppingDecorator.h: デコレータのベースクラスと具体的なトッピングデコレータを構築します。

    Pizza を継承し、ラップされたコンポーネントへの std::unique_ptr<Pizza> を保持する ToppingDecorator クラスを作成します。これがすべてのトッピングデコレータのベースとなります。

    以下の3つの具体的なトッピングデコレータを実装してください:

    • CheeseTopping — 説明に ", Cheese" を追加し、価格に 1.5 を追加します
    • PepperoniTopping — 説明に ", Pepperoni" を追加し、価格に 2.0 を追加します
    • MushroomTopping — 説明に ", Mushrooms" を追加し、価格に 1.0 を追加します

    各デコレータは、ラップされたピザに処理を委譲し、その後に自身の情報を追加する必要があります。

  • main.cpp: ユーザー入力に基づいてカスタマイズされたピザの注文を作成します。

    3つの入力(それぞれ yes または no)を読み取ります:

    1. チーズを追加しますか?
    2. ペパロニを追加しますか?
    3. マッシュルームを追加しますか?

    PlainPizza から開始し、入力に基づいて適切なトッピングデコレータでラップします(順序は:チーズ、ペパロニ、マッシュルーム)。最後に、注文の詳細を以下の形式で出力します:

    Order: [description]
    Total: $[cost]

例えば、入力が yes, yes, no の場合:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

入力が yes, no, yes の場合:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

入力が no, no, no の場合:

Order: Pizza
Total: $8

入力が yes, yes, yes の場合:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

各デコレータが前のデコレータをラップし、説明と価格の両方を層状に積み上げていく様子に注目してください。既存のピザやトッピングのコードを変更することなく、新しいデコレータクラスを作成するだけで簡単に新しいトッピングを追加できます。これが Decorator パターンの素晴らしい点です。

自分で試してみよう

#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"

using namespace std;

int main() {
    // 3つの入力(yesまたはno)を読み込む
    string addCheese, addPepperoni, addMushrooms;
    cin >> addCheese;
    cin >> addPepperoni;
    cin >> addMushrooms;
    
    // TODO: std::unique_ptrを使用してPlainPizzaから開始する
    
    // TODO: addCheeseが "yes" の場合、ピザをCheeseToppingでラップする
    
    // TODO: addPepperoniが "yes" の場合、ピザをPepperoniToppingでラップする
    
    // TODO: addMushroomsが "yes" の場合、ピザをMushroomToppingでラップする
    
    // TODO: 次の形式で注文を出力する:
    // Order: [description]
    // Total: $[cost]
    
    return 0;
}
quiz icon腕試し

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

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