0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C++】デコレータパターンで拡張性のある設計を

Posted at

ソフトウェア開発では既存のクラスに新しい機能を追加したいという場面がよくあります。 しかし、単純に継承を重ねていくとクラスが増えすぎてしまい、保守性が低下するという問題に直面します。
そんなときに役立つのがデコレータパターンです。
デコレータパターンを使えば、オブジェクトをラップすることで柔軟に機能を追加でき、拡張性と保守性を両立できます。
本記事ではゲーム風のコードを用いて、コンソールでできるデコレータパターンのチュートリアルを紹介します。

前提知識

  • C++の基礎文法を理解している

デコレータパターンとは

デコレータパターンとは、既存のオブジェクトをラップして機能を追加するデザインパターンです。
継承を使わずに柔軟に拡張できることが長所です。
その一方で、単に継承で機能を追加するよりもオブジェクト生成のロジックが煩雑になり、可読性が低下する可能性があるといったデメリットがあります。

デコレータパターンの構成と用語

デコレータパターンは、以下のようクラスで構成されています。

  • Component(コンポーネント)
    • 抽象的な「インターフェース」や「基底クラス」
  • ConcreteComponent(具象コンポーネント)
    • そのインターフェースを 実際に実装した具体的なクラス
  • Decorator(抽象デコレータ)
    • 機能追加のためのラップ構造を定義する抽象クラス
  • ConcreteDecorator(具象デコレータ)
    • 実際にComponentに効果などを追加する 具体的な拡張クラス

デザインパターンの文脈で、Concreteという言葉は、具体的な実装を持つクラス という意味で使用される言葉です。
Decoratorは、Componentをラップして機能を追加する仕組みと理解してもらえるとよいです。

ここでいうComponentUnityComponentのような部品という意味ではなく「ラップされる対象の抽象型」という意味合いなので注意

構造の図解

コード例

ラーメンにトッピングを乗せていくコード

IRamen.h
//-------------------------------------------------------------
//! @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;
};
RamenBase.h
//-------------------------------------------------------------
//! @file	RamenBase.h
//! @brief  基本ラーメンのクラス定義
//! @author つきの
//-------------------------------------------------------------
#pragma once
#include "IRamen.h"
//-------------------------------------------------------------
//! @class  基本ラーメンのクラス
//! @note   DecoratorパターンのConcreteComponent(具象コンポーネント)の役割を担うクラス
//! @author つきの
//-------------------------------------------------------------
class RamenBase : public IRamen {
public:
	//-------------------------------------------------------------
	// ラーメンを確認する処理
	//-------------------------------------------------------------
	void check() override;
};
RamenBase.cpp
//-------------------------------------------------------------
//! @file	RamenBase.cpp
//! @brief  基本ラーメンのクラス実装
//! @author つきの
//-------------------------------------------------------------
#include "RamenBase.h"
#include <iostream>
//-------------------------------------------------------------
//! @brief  基本内容のラーメンを確認する処理の
//-------------------------------------------------------------
void RamenBase::check() {
	// ラーメンの基本内容を表示
	std::cout << "現在のラーメン : ラーメン + スープ" << std::flush;
}
ToppingDecorator.h
//-------------------------------------------------------------
//! @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;
};
ToppingDecorator.cpp
//-------------------------------------------------------------
//! @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();	// ラップして対象のラーメンから確認処理を委譲
}
EggDecorator.h
//-------------------------------------------------------------
//! @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;
};
EggDecorator.cpp
//-------------------------------------------------------------
//! @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;	// 味玉を追加表示
}
RoastPorkDecorator.h
//-------------------------------------------------------------
//! @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;
};
RoastPorkDecorator.cpp
//-------------------------------------------------------------
//! @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;	// チャーシューを追加表示
}
main.cpp
//-------------------------------------------------------------
//! @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;
}
result
--------------------------------------------
スープと麺だけのラーメンが完成
--------------------------------------------
現在のラーメン : ラーメン + スープ
--------------------------------------------
味玉を追加
--------------------------------------------
現在のラーメン : ラーメン + スープ + 味玉
--------------------------------------------
チャーシューを追加
--------------------------------------------
現在のラーメン : ラーメン + スープ + 味玉 + チャーシュー

ラーメンにトッピングを追加していくことに成功しました。

クラス図

operation()

IRamenにあるcheck()デザインパターンの文脈でよくoperation()とよばれる関数にあたり、ラップしたオブジェクトの同名の関数を呼び出してから処理を追加します。
この仕組みによって実行中に柔軟に機能を追加でき、継承を使わずに拡張性を保つことができます。

IRamen.h
	//-------------------------------------------------------------
	//! @brief ラーメンを確認する処理
	//! @note  デザインパターンの文脈でいう「operation」関数に相当する
	//-------------------------------------------------------------
	virtual void check() = 0;
RamenBase.cpp
//-------------------------------------------------------------
//! @brief  基本内容のラーメンを確認する処理の
//-------------------------------------------------------------
void RamenBase::check() {
	// ラーメンの基本内容を表示
	std::cout << "現在のラーメン : ラーメン + スープ" << std::flush;
}
ToppingDecorator.cpp
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void ToppingDecorator::check() {
	inner_ramen_->check();	// ラップして対象のラーメンから確認処理を委譲
}
EggDecorator.cpp
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void EggDecorator::check() {
	inner_ramen_->check();					// ラップした対象のラーメンから確認処理を委譲
	std::cout << " + 味玉" << std::flush;	// 味玉を追加表示
}
RoastPorkDecorator.cpp
//-------------------------------------------------------------
//! @brief ラーメンを確認する処理
//-------------------------------------------------------------
void RoastPorkDecorator::check() {
	inner_ramen_->check();							// ラップした対象のラーメンから確認処理を委譲
	std::cout << " + チャーシュー" << std::flush;	// チャーシューを追加表示
}

Decorator/デコレータ

Decoratorパターンでは、デコレータにもConcreteComponent/具象コンポーネント継承している共通インターフェース/Component継承することで、アップキャストを使用して具象デコレータからoperation()を呼べる仕組みとなっています。

RamenBase.h
//-------------------------------------------------------------
//! @class  基本ラーメンのクラス
//! @note   DecoratorパターンのConcreteComponent(具象コンポーネント)の役割を担うクラス
//! @author つきの
//-------------------------------------------------------------
class RamenBase : public IRamen {
public:
	// 中略
};
ToppingDecorator.h
//-------------------------------------------------------------
//! @class トッピングデコレータのクラス
//! @note  DecoratorパターンのDecorator(抽象デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class ToppingDecorator : public IRamen {
    // 中略
};
EggDecorator.cpp
//-------------------------------------------------------------
//! @class 味玉追加デコレータのクラス
//! @note  DecoratorパターンのConcreteDecorator(具体的デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class EggDecorator : public ToppingDecorator {
    // 中略
};
RoastPorkDecorator.h
//-------------------------------------------------------------
//! @class チャーシュー追加デコレータのクラス
//! @note  DecoratorパターンのConcreteDecorator(具体的デコレータ)の役割を担うクラス
//-------------------------------------------------------------
class RoastPorkDecorator : public ToppingDecorator {
    // 中略
};
main.cpp
	//-------------------------------------------------------------
	// 基本ラーメンを作成して確認
	//-------------------------------------------------------------
	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()に機能を追加していくといったイメージで運用するのが適切
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?