Menu
Coddy logo textTech

デコレーターパターン

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を使用します。

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 パターンの素晴らしい点です。

チートシート

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;
}
quiz icon腕試し

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

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