ソフトウェア開発では、処理中のアルゴリズムを状況によって切り替えたいという場面があります。
しかし、単純にif-elseやswitchで分岐を増やしていくと、コードはすぐに複雑になり、拡張や保守が難しくなります。
ストラテジーパターンを使用することで、アルゴリズムをクラスとして独立させ、実行時に柔軟に切り替えられる仕組みを作ることが出来ます。
本記事では、簡単なサンプルコードを用いてC++によるストラテジーパターンの基本的な使い方を紹介します。
前提知識
-
C++の基礎文法を理解している
ストラテジーパターンとは
ストラテジーパターン/StrategyPatternとは、「アルゴリズムをクラスとして独立させ、実行時に柔軟に切り替えられるようにする」デザインパターンです。
「if文やswitchによる分岐を減らせるために更新処理が簡潔になる」「新しいアルゴリズムを追加しても既存コードを修正せずに拡張可能」といったメリットがあります。
その反面で、小規模な処理ではオーバーエンジニアリングになりやすいといったデメリットも存在します。
ストラテジーパターンの構成と用語
ストラテジーパターンは以下のようなクラスで構成されています。
-
Strategy(抽象戦略)
- アルゴリズムの共通インターフェース
-
ConcreteStrategy(具体戦略)
- Strategyインターフェースを実装し、実際のアルゴリズムを提供する
-
Context(利用側クラス)
-
Strategyを保持し、必要に応じて呼び出す - アルゴリズムの詳細を知らずに利用できる
-
構造の図解
コード例
Strategyでプレイヤーの攻撃方法を変えるコード
このコードは小規模なのでファイル分けなどを行っていませんがご容赦ください。
//--------------------------------------------------------
//! @file main.cpp
//! @brief ストラテジーパターンのサンプルコード
//! @author つきの
//--------------------------------------------------------
#include <iostream>
#include <memory>
//--------------------------------------------------------
//! @class 攻撃方法の抽象クラス
//! @note ストラテジーパターンのStrategy(抽象戦略)の役割
//--------------------------------------------------------
class AttackStrategy {
public:
//--------------------------------------------------------
//! @brief デストラクタ
//--------------------------------------------------------
virtual ~AttackStrategy() = default;
//--------------------------------------------------------
//! @brief 攻撃方法を実行する純粋仮想関数
//! @note 派生クラスで具体的な攻撃方法を実装
//--------------------------------------------------------
virtual void attack() const = 0;
};
//--------------------------------------------------------
//! @class 具体的な攻撃方法のクラス : 剣
//! @note ストラテジーパターンのConcreteStrategy(具体戦略)の役割
//--------------------------------------------------------
class SwordAttack : public AttackStrategy {
public:
//--------------------------------------------------------
//! @brief 剣で攻撃する具体的な実装
//--------------------------------------------------------
void attack() const override {
std::cout << "剣で斬りつける!" << std::endl;
}
};
//--------------------------------------------------------
//! @class 具体的な攻撃方法のクラス : 弓
//! @note ストラテジーパターンのConcreteStrategy(具体戦略)の役割
//--------------------------------------------------------
class BowAttack : public AttackStrategy {
public:
//--------------------------------------------------------
//! @brief 弓で攻撃する具体的な実装
//--------------------------------------------------------
void attack() const override {
std::cout << "弓で矢を放つ!" << std::endl;
}
};
//--------------------------------------------------------
//! @class 具体的な攻撃方法のクラス : 魔法
//! @note ストラテジーパターンのConcreteStrategy(具体戦略)の役割
//--------------------------------------------------------
class MagicAttack : public AttackStrategy {
public:
//--------------------------------------------------------
//! @brief 魔法で攻撃する具体的な実装
//--------------------------------------------------------
void attack() const override {
std::cout << "ファイアボールを唱える!" << std::endl;
}
};
//--------------------------------------------------------
//! @class プレイヤークラス
//! @note ストラテジーパターンのContext(コンテキスト)の役割
//--------------------------------------------------------
class Player {
private:
std::unique_ptr<AttackStrategy> attack_strategy_; // 攻撃戦略を保持
public:
//--------------------------------------------------------
//! @brief 攻撃戦略を設定するメソッド
//! @param s 攻撃戦略のポインタ
//--------------------------------------------------------
void setAttackStrategy(std::unique_ptr<AttackStrategy> s) {
attack_strategy_ = std::move(s);
}
//--------------------------------------------------------
//! @brief 攻撃を実行するメソッド
//! @note 現在設定されている攻撃戦略のattack()を呼び出す
//--------------------------------------------------------
void performAttack() const {
if (attack_strategy_) attack_strategy_->attack();
}
};
//エントリポイント
int main() {
// プレイヤーオブジェクトを生成
Player player;
// 剣で攻撃
player.setAttackStrategy(std::make_unique<SwordAttack>()); // 攻撃戦略を剣に設定
player.performAttack(); // 攻撃を実行
// 弓で攻撃
player.setAttackStrategy(std::make_unique<BowAttack>()); // 攻撃戦略を弓に設定
player.performAttack(); // 攻撃を実行
// 魔法で攻撃
player.setAttackStrategy(std::make_unique<MagicAttack>()); // 攻撃戦略を魔法に設定
player.performAttack(); // 攻撃を実行
// 正常終了
return 0;
}
剣で斬りつける!
弓で矢を放つ!
ファイアボールを唱える!
戦略の変更で攻撃方法を切り替えることが出来ました。
クラス図
Context/利用側クラス
Contextでは、ConcreteStrategy/具体戦略をアップキャストして持つことで、アルゴリズムの差し替えを実現しています。
アルゴリズムをConcreteStrategyへ記述することによって、Contextはアルゴリズムの詳細を知らずに利用することが出来ます。
//--------------------------------------------------------
//! @class プレイヤークラス
//! @note ストラテジーパターンのContext(コンテキスト)の役割
//--------------------------------------------------------
class Player {
private:
std::unique_ptr<AttackStrategy> attack_strategy_; // 攻撃戦略を保持
public:
//--------------------------------------------------------
//! @brief 攻撃戦略を設定するメソッド
//! @param s 攻撃戦略のポインタ
//--------------------------------------------------------
void setAttackStrategy(std::unique_ptr<AttackStrategy> s) {
attack_strategy_ = std::move(s);
}
//--------------------------------------------------------
//! @brief 攻撃を実行するメソッド
//! @note 現在設定されている攻撃戦略のattack()を呼び出す
//--------------------------------------------------------
void performAttack() const {
if (attack_strategy_) attack_strategy_->attack();
}
};
総括
-
ストラテジーパターンを使うことで、アルゴリズムをクラスとして独立させ、実行時に柔軟に切り替えることが出来る -
ConcreteStrategy/具体戦略がインターフェースとなるStrategyを継承することで、Context/利用側クラスは戦略の差し替えが可能となる - 条件分岐を減らせることや、拡張性が高いことがメリット
- クラス数が増えてしまうことと、小規模では冗長になってしまうことがデメリット