オブジェクト指向プログラミングで便利なステートパターン。
手続き型であるC言語でも同じように状態をモノとして表現できれば、複雑な状態の処理もコードを分けて書くことができ、保守性が上がります。
そこで今回は、C言語でのステートパターン実装方法を紹介します。
基本どのマイコンでも使えるように実装していますが、今回は参考としてArduinoのプロジェクトファイルでご紹介します。
シリアル通信の都合からファイルの拡張子がC++になっていますが中身はC言語で記述しています。
ちなみに今回はArduino Mega2560で動作確認しました。
ソフト構成
今回のソフト構成です。
メイン関数はmain.inoにあります。
今回のプログラムはメイン関数から現在(架空)の時間をセットすると、時間に合わせて状態(昼・夜)が変わったり、状態が変わることで処理(挨拶)が変わったりするプログラムになっています。
context.h
context部分の実装説明に入ります。
context.hでは、S_CONTEXTの構造体を定義しています。
特に重要なのは、構造体STATEの不完全型宣言が重要になります。この宣言があることでS_CONTETのSTATEのポインター型が宣言できます。
#ifndef CONTEXT_H
#define CONTEXT_H
struct STATE;
typedef struct{
struct STATE* state;
void (*ChangeState)(struct STATE*);
}S_CONTEXT;
#endif
state.h
続いて、stateの実装説明です。
まず初めに、先ほど作成したcontext.hをインクルードします。
context.hをインクルードする目的は、メイン関数からS_CONTEXTのポインター型を引数でもらうことが目的です。こうすることで、状態の実装部分で違う状態への変更が可能になります。
state.hでは、構造体のSTATEを宣言します。先ほど、context.hで定義した不完全型宣言の中身です。
ここでは、すべての状態で処理を行う関数分だけの関数ポインタを宣言します。
中身の実装は、各状態の実装部分で行います。
#ifndef STATE_H
#define STATE_H
#include "context.h"
typedef struct STATE{
void (*Greeting)(void);
void (*SetTime)(S_CONTEXT* context, unsigned int time);
}STATE;
#endif
day
今回は状態として、昼の状態・夜の状態を参考として実装しています。
まず昼のdayからご紹介します。
day.h
day.hの中身です。
C言語ではシングルトンの実装が出来ないので、day.hをインクルードすることで、dayのSTATE型の構造体をstaticで宣言した変数を返す関数を使えるようにします。
#ifndef DAY_H
#define DAY_H
#include "state.h"
STATE* GetDayInstans(void);
#endif
day.cpp
day.cppの中身です。
ここでは、staticで宣言したSTATE型のdayの関数ポインタをGreetingDayと、SetTimeDayで初期化しています。
GetDayInstansでは、staticで宣言したSTATE型のday変数を返します。
#include <./Arduino.h>
#include "night.h"
// プロトタイプ宣言
void GreetingDay();
void SetTimeDay(S_CONTEXT* context, unsigned int time);
// 現在の時間
unsigned int dayHour;
// インスタンス
static STATE day = {
GreetingDay,
SetTimeDay
};
// インスタンスを返す
STATE* GetDayInstans(void) {
return &day;
}
// 挨拶 (状態の処理)
void GreetingDay() {
Serial.print("こんにちは。今日はいい天気ですね!今は");
Serial.print(dayHour);
Serial.print("時です。\n");
}
// 時間をセットする
void SetTimeDay(S_CONTEXT* context, unsigned int time) {
dayHour = time;
//if (5 < dayHour && dayHour < 17) {
if (5 > dayHour || dayHour > 17) {
// 状態を夜に変更
context->ChangeState(GetNightInstans());
}
}
night
今回もう一つの状態であるnight部分の実装になります。
dayとほぼ同じので細かい説明は省略させていただきます。
night.h
#ifndef NIGHT_H
#define NIGHT_H
#include "state.h"
STATE* GetNightInstans (void);
#endif
night.cpp
#include <./Arduino.h>
#include "day.h"
// プロトタイプ宣言
void GreetingNight();
void SetTimeNight(S_CONTEXT* context, unsigned int time);
// 現在の時間
unsigned int nightHour;
// インスタンス
static STATE night = {
GreetingNight,
SetTimeNight
};
// インスタンスを返す
STATE* GetNightInstans(void) {
return &night;
}
// 挨拶 (状態の処理)
void GreetingNight() {
Serial.print("こんにちは。今日はいい天気ですね!今は");
Serial.print(nightHour);
Serial.print("時です。\n");
}
// 時間をセットする
void SetTimeNight(S_CONTEXT* context, unsigned int time) {
nightHour = time;
if (5 < nightHour && nightHour < 17) {
// 状態を昼に変更
context->ChangeState(GetDayInstans());
}
}
メイン (main.ino)
メインでは、S_CONTEXT型のcontextを生成します。
context.stateには、最初の状態であるdayのインスタンスを格納しています。
そして、context.ChangeStateの関数ポインタは、メインで実装しているChangeState関数を格納しています。
後は、疑似的に生成した時間を状態にセットするだけで、自動的に状態が変更され、それに応じた挨拶が行われるようになります。
#include"context.h"
#include"day.h"
// プロトタイプ宣言
void ChangeState(STATE* state);
// コンテキスト
static S_CONTEXT context;
// 時間
static unsigned int hour = 0;
void setup() {
// 初期化
Serial.begin(9600);
memset(&context, 0x00, sizeof(context));
context.state = GetDayInstans();
context.ChangeState = ChangeState;
hour = 0;
Serial.print("start\n");
}
void loop() {
// モードの処理
context.state->SetTime(&context, hour);
// 挨拶をする (状態の処理)
context.state->Greeting();
// 時間を更新する
hour++;
if (hour > 24) {
hour = 0;
}
delay(1000);
}
// 状態変更
void ChangeState(STATE* state){
Serial.print("状態が変更された\n");
context.state = state;
// モードが変わったら初期化する
context.state->SetTime(&context, hour);
}
まとめ
C言語でのステートパターン実装方法についてご紹介しました。
複雑な状態遷移の実装は、ステートパターンを使って分けて記述することで、保守性・可読性がグンと上がります。
是非お試しください。