はじめに
この記事で得られること
- 巨大なswitch文をステートパターンで置き換える方法
- 組込み系デバイスの状態管理を
std::unique_ptrで実装する具体例 - 組込み環境でステートパターンを適用する際の注意点
対象読者
- 状態遷移図・状態遷移表を使った開発経験がある組込み系エンジニア
- C++(C++11以降)の基本的な文法を理解している方
- switch文による状態管理に限界を感じている方
前提知識
- C++のクラス、継承、仮想関数の基本
-
std::unique_ptrの基本的な使い方
switch文による状態管理の問題
組込み系の開発では、デバイスの状態をenumで定義し、switch文で分岐するコードをよく見かけます。
switch (dev.state) {
case DeviceState::Idle:
std::cout << " [無視] 既に停止しています\n";
break;
case DeviceState::Initializing:
std::cout << " 初期化を中断して停止します\n";
dev.sensorReady = false;
dev.state = DeviceState::Idle;
break;
case DeviceState::Running:
std::cout << " デバイスを停止します\n";
if (dev.networkConnected) {
std::cout << " サーバーに停止を通知...\n";
}
dev.state = DeviceState::Idle;
break;
case DeviceState::Paused:
std::cout << " 一時停止中のデバイスを停止します\n";
dev.state = DeviceState::Idle;
break;
case DeviceState::Error:
if (dev.lastError == "過熱状態での再開拒否") {
std::cout << " 過熱エラーのため冷却シーケンスを実行...\n";
dev.temperature = 25;
}
std::cout << " エラー状態からの停止\n";
dev.errorCount = 0;
dev.state = DeviceState::Idle;
break;
case DeviceState::Shutdown:
std::cout << " [無視] 既にシャットダウン済みです\n";
break;
}
このコードは一見わかりやすいですが、実務で規模が大きくなると以下の問題が顕在化します。
1. 状態追加時の修正箇所が散在する
新しい状態(例:Calibrating)を追加すると、start()、stop()、pause()など全てのswitch文にcaseを追加する必要があります。修正漏れがあってもコンパイルエラーにならず、実行時まで気づけません。
2. 状態固有のロジックが分散する
ある状態に関するコードがstart()、stop()、pause()の各関数に散らばるため、「Running状態ではどんな操作が可能か」を把握するには複数の関数を横断的に読む必要があります。
3. テストが困難
特定の状態の振る舞いだけを単体テストしたい場合でも、デバイス全体のコンテキストが必要になります。
ステートパターンとは
ステートパターンは、GoF(Gang of Four)デザインパターンの一つで、状態を独立したオブジェクトとして切り出すことで上記の問題を解決します。
状態遷移図
今回の実装例で扱う状態遷移は以下の通りです。
[Idle] --start--> [Running] --pause--> [Paused]
[Running] --stop--> [Idle]
[Paused] --resume--> [Running]
[Paused] --stop--> [Idle]
構成要素
| 役割 | クラス | 説明 |
|---|---|---|
| Context | Device |
状態オブジェクトを保持し、操作を委譲する |
| State(インターフェース) | State |
全状態に共通の操作を定義する基底クラス |
| ConcreteState |
IdleState, RunningState, PausedState
|
各状態固有の振る舞いを実装する |
実装例
State.h — 状態インターフェース(基底クラス)
#pragma once
#include <string>
class Device; // 前方宣言(循環インクルード防止)
// State インターフェース
// 各状態クラスはこのクラスを継承し、対応する操作をオーバーライドする
class State {
public:
virtual ~State() = default;
virtual std::string name() const = 0;
// デフォルト実装では「無効な操作」として処理する
virtual void start(Device& device);
virtual void stop(Device& device);
virtual void pause(Device& device);
virtual void resume(Device& device);
};
State.cpp — デフォルト実装(無効な操作の共通処理)
#include "State.h"
#include <iostream>
using namespace std;
void State::start(Device& /*device*/) {
cout << " [無視] " << name() << " 状態では start できません\n";
}
void State::stop(Device& /*device*/) {
cout << " [無視] " << name() << " 状態では stop できません\n";
}
void State::pause(Device& /*device*/) {
cout << " [無視] " << name() << " 状態では pause できません\n";
}
void State::resume(Device& /*device*/) {
cout << " [無視] " << name() << " 状態では resume できません\n";
}
Device.h — Context(デバイス本体)
#pragma once
#include <memory>
#include "State.h"
// Context: 現在の状態オブジェクトを保持し、操作を委譲する
class Device {
public:
explicit Device(std::unique_ptr<State> initial);
void setState(std::unique_ptr<State> newState);
std::string currentState() const;
void start();
void stop();
void pause();
void resume();
private:
std::unique_ptr<State> state_;
};
Device.cpp
#include "Device.h"
#include <iostream>
using namespace std;
Device::Device(unique_ptr<State> initial)
: state_(move(initial)) {
}
void Device::setState(unique_ptr<State> newState) {
cout << " [遷移] " << state_->name()
<< " -> " << newState->name() << "\n";
state_ = move(newState);
}
string Device::currentState() const {
return state_->name();
}
// 各操作は現在の状態オブジェクトに委譲する
void Device::start() { state_->start(*this); }
void Device::stop() { state_->stop(*this); }
void Device::pause() { state_->pause(*this); }
void Device::resume() { state_->resume(*this); }
IdleState.h / IdleState.cpp — Idle状態
// IdleState.h
#pragma once
#include "State.h"
class IdleState : public State {
public:
std::string name() const override;
void start(Device& device) override; // Idle → Running のみ許可
};
// IdleState.cpp
#include "IdleState.h"
#include "RunningState.h"
#include "Device.h"
#include <iostream>
using namespace std;
string IdleState::name() const {
return "Idle";
}
void IdleState::start(Device& device) {
cout << " デバイスを起動します\n";
device.setState(make_unique<RunningState>());
}
RunningState.h / RunningState.cpp — Running状態
// RunningState.h
#pragma once
#include "State.h"
class RunningState : public State {
public:
std::string name() const override;
void stop(Device& device) override; // Running → Idle
void pause(Device& device) override; // Running → Paused
};
// RunningState.cpp
#include "RunningState.h"
#include "IdleState.h"
#include "PausedState.h"
#include "Device.h"
#include <iostream>
using namespace std;
string RunningState::name() const {
return "Running";
}
void RunningState::stop(Device& device) {
cout << " デバイスを停止します\n";
device.setState(make_unique<IdleState>());
}
void RunningState::pause(Device& device) {
cout << " デバイスを一時停止します\n";
device.setState(make_unique<PausedState>());
}
PausedState.h / PausedState.cpp — Paused状態
// PausedState.h
#pragma once
#include "State.h"
class PausedState : public State {
public:
std::string name() const override;
void stop(Device& device) override; // Paused → Idle
void resume(Device& device) override; // Paused → Running
};
// PausedState.cpp
#include "PausedState.h"
#include "IdleState.h"
#include "RunningState.h"
#include "Device.h"
#include <iostream>
using namespace std;
string PausedState::name() const {
return "Paused";
}
void PausedState::stop(Device& device) {
cout << " 一時停止中のデバイスを停止します\n";
device.setState(make_unique<IdleState>());
}
void PausedState::resume(Device& device) {
cout << " デバイスを再開します\n";
device.setState(make_unique<RunningState>());
}
main.cpp — 動作確認
#include "IdleState.h"
#include "Device.h"
#include <windows.h>
#include <iostream>
using namespace std;
int main() {
SetConsoleOutputCP(CP_UTF8);
cout << "=== State Pattern デモ ===\n\n";
Device device(make_unique<IdleState>());
cout << "初期状態: " << device.currentState() << "\n\n";
// 通常の遷移シーケンス
cout << ">> start\n";
device.start();
cout << " 結果: " << device.currentState() << "\n\n";
cout << ">> pause\n";
device.pause();
cout << " 結果: " << device.currentState() << "\n\n";
cout << ">> resume\n";
device.resume();
cout << " 結果: " << device.currentState() << "\n\n";
cout << ">> stop\n";
device.stop();
cout << " 結果: " << device.currentState() << "\n\n";
// 不正な操作の例
cout << "--- 不正な操作のテスト ---\n\n";
cout << ">> stop (Idle 状態で)\n";
device.stop();
cout << " 結果: " << device.currentState() << "\n\n";
cout << ">> pause (Idle 状態で)\n";
device.pause();
cout << " 結果: " << device.currentState() << "\n\n";
return 0;
}
ソースコードはGitHubにも登録しています。
https://github.com/freelike-system/StatePattern.git
Before / After 比較
| 観点 | switch文(Before) | ステートパターン(After) |
|---|---|---|
| 状態の追加 | 全てのswitch文にcase追加が必要 | 新しいStateクラスを1つ追加するだけ |
| 状態固有ロジックの所在 | 複数関数に分散 | 1つのクラスに集約 |
| 開放閉鎖原則(OCP) | 既存コードの修正が必須 | 既存コードを変更せず拡張可能 |
| 単体テスト | デバイス全体のセットアップが必要 | 状態クラス単体でテスト可能 |
| コード量 | 少ない(状態が少ない場合) | クラス数が増える |
状態が2〜3個程度ならswitch文の方がシンプルです。状態が増える見込みがある場合や、状態ごとの振る舞いが複雑な場合にステートパターンの効果が発揮されます。
組込み環境での注意点
ヒープ確保について
本記事の実装ではstd::make_uniqueによるヒープ確保を行っています。動的メモリ確保が許容されるWindows組込み環境やLinux組込み環境では問題ありませんが、リアルタイムOSなどヒープ確保ができない環境もあります。
ヒープ確保ができない環境の場合は、以下のような代替手法があります。
- 各状態をstaticなシングルトンとして保持し、静的領域を指すポインタで切り替える
-
std::variant(C++17)を使って状態オブジェクトをスタック上に確保する
MISRA C++との関係
MISRA C++ 2008では動的メモリ確保(Rule 18-4-1)に制限があります。プロジェクトのMISRA準拠レベルに応じて、上記の代替手法の採用を検討してください。
ステートパターンと状態遷移表の併用
組込み開発では状態遷移表による網羅性の確認が重要です。ステートパターンを導入した場合でも、設計ドキュメントとしての状態遷移表は引き続き作成することをお勧めします。各ConcreteStateクラスでオーバーライドしている関数を一覧にすれば、状態遷移表との対応関係を確認できます。
応用例
ステートパターンは状態管理以外にも応用できます。
- ユーザー権限(ロール): Admin / Editor / Viewer で操作可能な機能を切り替える
- サブスクリプションプラン: Free / Standard / Premium で利用可能な機能を制御する
- 出力フォーマット: CSV / JSON / XML で出力処理を切り替える(※一般的にはストラテジーパターンと呼ばれます)
まとめ
- ステートパターンは、状態ごとの振る舞いを独立したクラスに分離するデザインパターン
- 状態追加時の変更箇所を局所化でき、開放閉鎖原則(OCP)に沿った設計になる
- 組込み環境ではヒープ確保の可否に応じて実装方法を選択する
- 状態が少なくシンプルな場合は、switch文の方が適切な場合もある
参考文献
- エリック・ガンマ他『オブジェクト指向における再利用のためのデザインパターン(改訂版)』ソフトバンククリエイティブ、1999年
- マーチン・ファウラー『リファクタリング(第2版)既存のコードを安全に改善する』オーム社、2019年
- Refactoring.Guru - State Pattern