Menu
Coddy logo textTech

Decoratorパターン

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

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

このパターンは、元のオブジェクトとdecoratorsの両方が同じinterfaceをImplementすることで機能します。各decoratorはcomponentへの参照を保持し、ラップされたオブジェクトに委譲する前または後に独自の動作を追加します。

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

Decorator は積み重ねることができます。それぞれが前のものをラップし、機能を層ごとに構築していきます。

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

チャレンジ

簡単

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

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

  • Pizza.h:component interfaceと基本のピザクラスをdefineします。

    純粋仮想メソッドgetDescription()(stringを返す)とgetCost()(doubleを返す)、およびvirtual destructorを持つabstractなPizzaクラスを作成します。

    次に、基本のピザを表すconcreteなPlainPizzaクラスを作成します。説明として"Pizza"を返し、基本コストは8.0にします。

  • ToppingDecorator.h:decoratorの基本クラスとconcreteなトッピングdecoratorを構築します。

    Pizzaを継承し、ラップされたcomponentへのstd::unique_ptr<Pizza>を保持するToppingDecoratorクラスを作成します。これはすべてのトッピングdecoratorのBaseとなります。

    3つのconcreteなトッピングdecoratorをImplementします。

    • CheeseTopping:説明に", Cheese"を、コストに1.5をaddします
    • PepperoniTopping:説明に", Pepperoni"を、コストに2.0をaddします
    • MushroomTopping:説明に", Mushrooms"を、コストに1.0をaddします

    各decoratorはラップされたpizzaに処理を委譲し、その後、自身の分をaddする必要があります。

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

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

    1. Cheeseをaddしますか?
    2. Pepperoniをaddしますか?
    3. Mushroomsをaddしますか?

    PlainPizzaから始め、入力に基づいて適切なトッピングdecoratorでラップします(順番はCheese、Pepperoni、Mushrooms)。最後に、次のformatでorderの詳細を出力します。

    Order: [description]
    Total: $[cost]

たとえば、入力がyesyesnoの場合:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

入力がyesnoyesの場合:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

入力がnononoの場合:

Order: Pizza
Total: $8

入力がyesyesyesの場合:

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

各decoratorが直前のdecoratorをラップし、説明とコストの両方をレイヤーごとに構築していることに注目してください。既存のpizzaやtoppingのコードを変更せずに、新しいdecoratorクラスを作成することで新しいトッピングを簡単に追加できます。これが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腕試し

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

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

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