デコレーターパターン
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 98/104。
デコレーターパターンは、デコレーターと呼ばれる特別なオブジェクトでラップすることによって、オブジェクトに新しい振る舞いを動的に追加することを可能にします。コンパイル時に振る舞いを追加する継承とは異なり、デコレーターを使用すると、元のクラスを変更することなく実行時に機能を拡張できます。
このパターンは、元のオブジェクトとデコレータの両方が同じインターフェースを実装することによって機能します。各デコレータはコンポーネントへの参照を保持し、ラップされたオブジェクトに委譲する前または後に独自の振る舞いを追加します:
#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を使用します。
チャレンジ
簡単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)を読み取ります:- チーズを追加しますか?
- ペパロニを追加しますか?
- マッシュルームを追加しますか?
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 パターンの素晴らしい点です。
チートシート
Decoratorパターン(装飾者パターン)は、オブジェクトをデコレータと呼ばれる特殊なオブジェクトでラップすることにより、動的に新しい振る舞いを追加することを可能にします。コンパイル時に振る舞いを追加する継承とは異なり、デコレータを使用すると、元のクラスを変更することなく実行時に機能を拡張できます。
このパターンは、元のオブジェクトとデコレータの両方に同じインターフェースを実装させることで機能します。各デコレータはコンポーネントへの参照を保持し、ラップされたオブジェクトに処理を委譲する前後に独自の振る舞いを追加します。
構造
Componentインターフェース: 具体的なコンポーネントとデコレータの両方に共通のインターフェースを定義します。
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};Concrete component(具体的なコンポーネント): デコレーション(装飾)が可能なベースとなるオブジェクトです。
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};Base decorator(ベースデコレータ): コンポーネントへの参照を保持し、同じインターフェースを実装します。
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};Concrete decorators(具体的なデコレータ): ラップされたオブジェクトに処理を委譲し、独自の機能を追加することで、特定の振る舞いを追加します。
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; }
};デコレータの積み重ね
デコレータは積み重ねることができます。それぞれが前のデコレータをラップし、機能を層状に構築していきます。
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パターンを使用します。
自分で試してみよう
#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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス