ソフトウェア開発では既存のクラスに新しい機能を追加したいという場面がよくあります。 しかし、単純に継承を重ねていくとクラスが増えすぎてしまい、保守性が低下するという問題に直面します。
そんなときに役立つのがデコレータパターンです。
デコレータパターンを使えば、オブジェクトをラップすることで柔軟に機能を追加でき、拡張性と保守性を両立できます。
本記事ではゲーム風のコードを用いて、コンソールでできるデコレータパターンのチュートリアルを紹介します。
前提知識
-
C++の基礎文法を理解している
デコレータパターンとは
デコレータパターンとは、既存のオブジェクトをラップして機能を追加するデザインパターンです。
継承を使わずに柔軟に拡張できることが長所です。
その一方で、単に継承で機能を追加するよりもオブジェクト生成のロジックが煩雑になり、可読性が低下する可能性があるといったデメリットがあります。
デコレータパターンの構成と用語
デコレータパターンは、以下のようクラスで構成されています。
-
Component(コンポーネント)
- 抽象的な「インターフェース」や「基底クラス」
-
ConcreteComponent(具象コンポーネント)
- そのインターフェースを 実際に実装した具体的なクラス
-
Decorator(抽象デコレータ)
- 機能追加のためのラップ構造を定義する抽象クラス
-
ConcreteDecorator(具象デコレータ)
- 実際に
Componentに効果などを追加する 具体的な拡張クラス
- 実際に
デザインパターンの文脈で、Concreteという言葉は、具体的な実装を持つクラス という意味で使用される言葉です。
Decoratorは、Componentをラップして機能を追加する仕組みと理解してもらえるとよいです。
ここでいうComponentはUnityのComponentのような部品という意味ではなく「ラップされる対象の抽象型」という意味合いなので注意
構造の図解
コード例
ラーメンにトッピングを乗せていくコード
//-------------------------------------------------------------
//! @file IRamen.h
//! @brief ラーメンの基本インターフェースを定義するヘッダファイル
//! @author つきの
//-------------------------------------------------------------
#pragma once
//-------------------------------------------------------------
//! @class ラーメンの基本インターフェース
//! @note DecoratorパターンのComponent(コンポーネント)の役割を担うクラス
//! @author つきの
//-------------------------------------------------------------
class IRamen {
public:
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//! @note デザインパターンの文脈でいう「operation」関数に相当する
//-------------------------------------------------------------
virtual void check() = 0;
//-------------------------------------------------------------
//! @brief 仮想デストラクタ
//-------------------------------------------------------------
virtual ~IRamen() = default;
};
//-------------------------------------------------------------
//! @file RamenBase.h
//! @brief 基本ラーメンのクラス定義
//! @author つきの
//-------------------------------------------------------------
#pragma once
#include "IRamen.h"
//-------------------------------------------------------------
//! @class 基本ラーメンのクラス
//! @note DecoratorパターンのConcreteComponent(具象コンポーネント)の役割を担うクラス
//! @author つきの
//-------------------------------------------------------------
class RamenBase : public IRamen {
public:
//-------------------------------------------------------------
// ラーメンを確認する処理
//-------------------------------------------------------------
void check() override;
};
//-------------------------------------------------------------
//! @file RamenBase.cpp
//! @brief 基本ラーメンのクラス実装
//! @author つきの
//-------------------------------------------------------------
#include "RamenBase.h"
#include <iostream>
//-------------------------------------------------------------
//! @brief 基本内容のラーメンを確認する処理の
//-------------------------------------------------------------
void RamenBase::check() {
// ラーメンの基本内容を表示
std::cout << "現在のラーメン : ラーメン + スープ" << std::flush;
}
//-------------------------------------------------------------
//! @file ToppingDecorator.h
//! @brief トッピングデコレータのクラス定義
//! @author つきの
//-------------------------------------------------------------
#pragma once
#include "IRamen.h"
#include <memory>
//-------------------------------------------------------------
//! @class トッピングデコレータのクラス
//! @note DecoratorパターンのDecorator(抽象デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class ToppingDecorator : public IRamen {
protected:
std::shared_ptr<IRamen> inner_ramen_; // ラップ対象のラーメン
public:
//-------------------------------------------------------------
// コンストラクタ
//! @param ramen [in] ラップ対象のラーメン
//-------------------------------------------------------------
ToppingDecorator(std::shared_ptr<IRamen> ramen);
//-------------------------------------------------------------
// ラーメンを確認する処理
//! @note ラップして対象のラーメンから確認処理を委譲する
//-------------------------------------------------------------
void check() override;
};
//-------------------------------------------------------------
//! @file ToppingDecorator.cpp
//! @brief トッピングデコレータのクラス実装
//! @author つきの
//-------------------------------------------------------------
#include "ToppingDecorator.h"
//-------------------------------------------------------------
//! @brief コンストラクタ
//-------------------------------------------------------------
ToppingDecorator::ToppingDecorator(std::shared_ptr<IRamen> ramen)
: inner_ramen_(ramen) { // ラップ対象のラーメンを保存
}
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void ToppingDecorator::check() {
inner_ramen_->check(); // ラップして対象のラーメンから確認処理を委譲
}
//-------------------------------------------------------------
//! @file EggDecorator.h
//! @brief 味玉追加デコレータのクラス定義
//! @author つきの
//-------------------------------------------------------------
#pragma once
#include "ToppingDecorator.h"
//-------------------------------------------------------------
//! @class 味玉追加デコレータのクラス
//! @note DecoratorパターンのConcreteDecorator(具体的デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class EggDecorator : public ToppingDecorator {
public:
//-------------------------------------------------------------
// コンストラクタ
//! @param ramen [in] ラップ対象のラーメン
//-------------------------------------------------------------
EggDecorator(std::shared_ptr<IRamen> ramen);
//-------------------------------------------------------------
// ラーメンを確認する処理
//! @note ラップ対象のラーメンに味玉を追加して確認
//-------------------------------------------------------------
void check() override;
};
//-------------------------------------------------------------
//! @file EggDecorator.cpp
//! @brief 味玉追加デコレータのクラス実装
//! @author つきの
//-------------------------------------------------------------
#include "EggDecorator.h"
#include <iostream>
//-------------------------------------------------------------
//! @brief コンストラクタ
//-------------------------------------------------------------
EggDecorator::EggDecorator(std::shared_ptr<IRamen> ramen)
: ToppingDecorator(ramen) { // 抽象デコレータクラスのコンストラクタを呼び出す
}
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void EggDecorator::check() {
inner_ramen_->check(); // ラップした対象のラーメンから確認処理を委譲
std::cout << " + 味玉" << std::flush; // 味玉を追加表示
}
//-------------------------------------------------------------
//! @file RoastPorkDecorator.h
//! @brief チャーシュー追加デコレータのクラス定義
//! @author つきの
//-------------------------------------------------------------
#pragma once
#include "ToppingDecorator.h"
//-------------------------------------------------------------
//! @class チャーシュー追加デコレータのクラス
//! @note DecoratorパターンのConcreteDecorator(具体的デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class RoastPorkDecorator : public ToppingDecorator {
public:
//-------------------------------------------------------------
// コンストラクタ
//! @param ramen [in] ラップ対象のラーメン
//-------------------------------------------------------------
RoastPorkDecorator(std::shared_ptr<IRamen> ramen);
//-------------------------------------------------------------
// ラーメンを確認する処理
//! @note ラップ対象のラーメンにチャーシューを追加して確認
//-------------------------------------------------------------
void check() override;
};
//-------------------------------------------------------------
//! @file RoastPorkDecorator.cpp
//! @brief チャーシュー追加デコレータのクラス実装
//! @author つきの
//-------------------------------------------------------------
#include "RoastPorkDecorator.h"
#include <iostream>
//-------------------------------------------------------------
//! @brief コンストラクタ
//-------------------------------------------------------------
RoastPorkDecorator::RoastPorkDecorator(std::shared_ptr<IRamen> ramen)
: ToppingDecorator(ramen) { // 抽象デコレータクラスのコンストラクタを呼び出す
}
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void RoastPorkDecorator::check() {
inner_ramen_->check(); // ラップした対象のラーメンから確認処理を委譲
std::cout << " + チャーシュー" << std::flush; // チャーシューを追加表示
}
//-------------------------------------------------------------
//! @file main.cpp
//! @brief デコレータパターンによるラーメン確認処理の実装例
//! @author つきの
//-------------------------------------------------------------
#include "IRamen.h"
#include "RamenBase.h"
#include "EggDecorator.h"
#include "RoastPorkDecorator.h"
#include <memory>
#include <iostream>
//エントリポイント
int main() {
//-------------------------------------------------------------
// 基本ラーメンを作成して確認
//-------------------------------------------------------------
std::shared_ptr<IRamen> ramen = std::make_shared<RamenBase>();
std::cout << "--------------------------------------------" << std::endl;
std::cout << "スープと麺だけのラーメンが完成" << std::endl;
std::cout << "--------------------------------------------" << std::endl;
ramen->check(); // 現在のラーメンを確認
std::cout << std::endl;
//-------------------------------------------------------------
// 味玉デコレータでラップ
//-------------------------------------------------------------
ramen = std::make_shared<EggDecorator>(ramen);
std::cout << "--------------------------------------------" << std::endl;
std::cout << "味玉を追加" << std::endl;
std::cout << "--------------------------------------------" << std::endl;
ramen->check(); // 現在のラーメンを確認
std::cout << std::endl;
//-------------------------------------------------------------
// チャーシューデコレータでラップ
//-------------------------------------------------------------
ramen = std::make_shared<RoastPorkDecorator>(ramen);
std::cout << "--------------------------------------------" << std::endl;
std::cout << "チャーシューを追加" << std::endl;
std::cout << "--------------------------------------------" << std::endl;
ramen->check(); // 現在のラーメンを確認
std::cout << std::endl;
//終了
return 0;
}
--------------------------------------------
スープと麺だけのラーメンが完成
--------------------------------------------
現在のラーメン : ラーメン + スープ
--------------------------------------------
味玉を追加
--------------------------------------------
現在のラーメン : ラーメン + スープ + 味玉
--------------------------------------------
チャーシューを追加
--------------------------------------------
現在のラーメン : ラーメン + スープ + 味玉 + チャーシュー
ラーメンにトッピングを追加していくことに成功しました。
クラス図
operation()
IRamenにあるcheck()はデザインパターンの文脈でよくoperation()とよばれる関数にあたり、ラップしたオブジェクトの同名の関数を呼び出してから処理を追加します。
この仕組みによって実行中に柔軟に機能を追加でき、継承を使わずに拡張性を保つことができます。
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//! @note デザインパターンの文脈でいう「operation」関数に相当する
//-------------------------------------------------------------
virtual void check() = 0;
//-------------------------------------------------------------
//! @brief 基本内容のラーメンを確認する処理の
//-------------------------------------------------------------
void RamenBase::check() {
// ラーメンの基本内容を表示
std::cout << "現在のラーメン : ラーメン + スープ" << std::flush;
}
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void ToppingDecorator::check() {
inner_ramen_->check(); // ラップして対象のラーメンから確認処理を委譲
}
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void EggDecorator::check() {
inner_ramen_->check(); // ラップした対象のラーメンから確認処理を委譲
std::cout << " + 味玉" << std::flush; // 味玉を追加表示
}
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void RoastPorkDecorator::check() {
inner_ramen_->check(); // ラップした対象のラーメンから確認処理を委譲
std::cout << " + チャーシュー" << std::flush; // チャーシューを追加表示
}
Decorator/デコレータ
Decoratorパターンでは、デコレータにもConcreteComponent/具象コンポーネントが継承している共通インターフェース/Componentを継承することで、アップキャストを使用して具象デコレータからoperation()を呼べる仕組みとなっています。
//-------------------------------------------------------------
//! @class 基本ラーメンのクラス
//! @note DecoratorパターンのConcreteComponent(具象コンポーネント)の役割を担うクラス
//! @author つきの
//-------------------------------------------------------------
class RamenBase : public IRamen {
public:
// 中略
};
//-------------------------------------------------------------
//! @class トッピングデコレータのクラス
//! @note DecoratorパターンのDecorator(抽象デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class ToppingDecorator : public IRamen {
// 中略
};
//-------------------------------------------------------------
//! @class 味玉追加デコレータのクラス
//! @note DecoratorパターンのConcreteDecorator(具体的デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class EggDecorator : public ToppingDecorator {
// 中略
};
//-------------------------------------------------------------
//! @class チャーシュー追加デコレータのクラス
//! @note DecoratorパターンのConcreteDecorator(具体的デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class RoastPorkDecorator : public ToppingDecorator {
// 中略
};
//-------------------------------------------------------------
// 基本ラーメンを作成して確認
//-------------------------------------------------------------
std::shared_ptr<IRamen> ramen = std::make_shared<RamenBase>();
std::cout << "--------------------------------------------" << std::endl;
std::cout << "スープと麺だけのラーメンが完成" << std::endl;
std::cout << "--------------------------------------------" << std::endl;
ramen->check(); // 現在のラーメンを確認
std::cout << std::endl;
//-------------------------------------------------------------
// 味玉デコレータでラップ
//-------------------------------------------------------------
ramen = std::make_shared<EggDecorator>(ramen);
std::cout << "--------------------------------------------" << std::endl;
std::cout << "味玉を追加" << std::endl;
std::cout << "--------------------------------------------" << std::endl;
ramen->check(); // 現在のラーメンを確認
std::cout << std::endl;
//-------------------------------------------------------------
// チャーシューデコレータでラップ
//-------------------------------------------------------------
ramen = std::make_shared<RoastPorkDecorator>(ramen);
std::cout << "--------------------------------------------" << std::endl;
std::cout << "チャーシューを追加" << std::endl;
std::cout << "--------------------------------------------" << std::endl;
ramen->check(); // 現在のラーメンを確認
std::cout << std::endl;
総括
-
Decoratorパターンを使用することで、実行中に柔軟に機能を追加することが可能となる - ラップしたオブジェクトの
operation()に機能を追加していくといったイメージで運用するのが適切