SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

状態遷移図/表、すなわち設計をコードでテストする

状態遷移のモデル検証

  • X ポスト
  • このエントリーをはてなブックマークに追加

 昔々に書いたコード(とその覚え書き)が役に立つことは少なくないもので、3年以上前のアーティクル:『状態遷移表からStateパターンを自動生成する』で紹介した、T4-templateを用いて状態遷移表からC#/C++のひな型コードを吐くってネタが使える案件が降ってきました。とはいえ今回、吐かせたいひな型はCとのこと……まぁ、キモは同じだからなんとかなるっしょ。新たな試みとして、生成されたひな型を使って状態遷移図/表すなわち"設計"をテストします。

  • X ポスト
  • このエントリーをはてなブックマークに追加

状態遷移表を作成する

 サンプルとして

「ソースコード(テキストファイル)から 
ブロックコメント:/*から*/まで と 
ラインコメント://から改行まで を抽出し出力する」

を与えられた要件/要求とします。

 この要求を状態遷移で実現するにあたり思いつく状態としては、"コメントの外","ブロックコメントの中","ラインコメントの中"……ほかにも'/'の直後はコメントの中か外か定まっていないので"/の直後"なんて状態も必要になりそうです。

 そしてイベントはファイルから'/','*',改行,その他 の各文字を読み込んだ、の4種でいいかな。

 プリンタ用紙に鉛筆で丸と矢印を描いては消してできあがった状態遷移図がコレ。

 状態遷移図を基に、遷移に伴うアクションを加えて転記した状態遷移表がコレです。

状態遷移表からひな型コードを生成する

 この状態遷移表からコードを起こすわけですが、状態遷移の実装については『StateパターンでCSVを読む』を書きました。デザイン・パターンの一つ:Stateによる実装です。今回の実装はC、継承も仮想関数も使えないという利き腕を封じられた条件なので戦術を大きく変えにゃならんです。

 状態遷移の実装は要するに「(1)現状態 と (2)受理したイベント の組」に対応する「(3)アクション と (4)遷移先(新たな状態)」を引き当てることに他なりません。ならば上記(1)~(4)の並びをレコードとし、そのレコード列(=状態遷移表)から「(1)現状態 と (2)受理したイベント の組」に一致するレコードを探し出して「(3)アクション を実行して (4)新たな状態 に遷移」すればいい。

 状態遷移表からひな型コードの生成には使い慣れた「T4-template」を用います。テンプレートに記述するのは enum S { 状態... } と enum E { イベント... } そして状態遷移表: transition(状態,イベント,遷移先...) を並べたものです。アクションは状態とイベントから適当な名前を与えた関数(当然中身からっぽ)を吐かせます。遷移先は複数個(4つまで)書けるようにしました。というのも、実際の設計ではアクションの結果次第で遷移先が異なるケースがままあるのです。例えば「初期状態で開始イベントがきたらチャネルをオープンして受信待ちに遷移する」場合、チャネルのオープンに失敗したら初期状態に戻りたい、こんなケースに対応すべく、アクションが返した値nに応じて遷移先そのnを指定できるように。

 コメント抽出(ExtractComment)を行う状態遷移表テンプレート:state_transition.ttはこんなやつです。

list01 state_transition.tt
<#@ output encoding="shift_jis" #>
<#@ template debug="false" hostspecific="true" language="C#" #>
<#
   string prefix = this.Host.ResolveParameterValue("","","prefix"); 
   int slen = 0; foreach( string s in Enum.GetNames(typeof(S)) ) if ( slen < s.Length ) slen = s.Length;
   int elen = 0; foreach( string e in Enum.GetNames(typeof(E)) ) if ( elen < e.Length ) elen = e.Length;
#>
<#@ include file="state_transition_.tt" #>
<#+

    static Tuple<S,E,S[]> transition(S s, E e, params S[] n) { return Tuple.Create(s,e,n); }

    // 状態を列挙
    enum S { 
      OUT_COMMENT,
      SECOND_LEAD,
      IN_BLOCKCOMMENT,
      IN_LINECOMMENT,
      SECOND_TRAIL,
    };

    // イベントを列挙
    enum E { 
      STAR,
      SLASH,
      LF,
      OTHER
    };

    // 以下に書かれた状態遷移表に基づいてコードを生成する
    Tuple<S,E,S[]>[] _transition_ = {
      //         状態,              イベント,次の状態...
      transition(S.OUT_COMMENT,     E.STAR,  S.OUT_COMMENT),
      transition(S.OUT_COMMENT,     E.SLASH, S.SECOND_LEAD),
      transition(S.OUT_COMMENT,     E.LF,    S.OUT_COMMENT),
      transition(S.OUT_COMMENT,     E.OTHER, S.OUT_COMMENT),

      transition(S.SECOND_LEAD,     E.STAR,  S.IN_BLOCKCOMMENT),
      transition(S.SECOND_LEAD,     E.SLASH, S.IN_BLOCKCOMMENT),
      transition(S.SECOND_LEAD,     E.LF,    S.OUT_COMMENT),
      transition(S.SECOND_LEAD,     E.OTHER, S.OUT_COMMENT),

      transition(S.IN_BLOCKCOMMENT, E.STAR,  S.SECOND_TRAIL),
      transition(S.IN_BLOCKCOMMENT, E.SLASH, S.IN_BLOCKCOMMENT),
      transition(S.IN_BLOCKCOMMENT, E.LF,    S.IN_BLOCKCOMMENT),
      transition(S.IN_BLOCKCOMMENT, E.OTHER, S.IN_BLOCKCOMMENT),

      transition(S.IN_LINECOMMENT,  E.STAR,  S.IN_LINECOMMENT),
      transition(S.IN_LINECOMMENT,  E.SLASH, S.IN_LINECOMMENT),
      transition(S.IN_LINECOMMENT,  E.LF,    S.OUT_COMMENT),
      transition(S.IN_LINECOMMENT,  E.OTHER, S.IN_LINECOMMENT),

      transition(S.SECOND_TRAIL,    E.STAR,  S.SECOND_TRAIL),
      transition(S.SECOND_TRAIL,    E.SLASH, S.OUT_COMMENT),
      transition(S.SECOND_TRAIL,    E.LF,    S.IN_BLOCKCOMMENT),
      transition(S.SECOND_TRAIL,    E.OTHER, S.IN_BLOCKCOMMENT),
    };

#>

 サンプルファイルのディレクトリ:state_transitonに上記テンプレートと生成バッチ:state_transiton.batを用意しました。コマンドラインから state_transition ec すると、ひな型コード:ec_state_transition.h/.c が生成されます。Cは名前空間を持たないのでシンボルの衝突回避のためプレフィクス(ここではec)を付けられるようにしました。

list02 ec_state_transition.h
#ifndef EC_STATE_TRANSITION_H_
#define EC_STATE_TRANSITION_H_

/* -----------------------------------------------------
 * 状態遷移 ec 
 * -----------------------------------------------------
 */

#include <stdbool.h> /* from C99 : defines bool, true, false */

#include "statemachine_defs.h"

/*
 * 状態を列挙
 */
enum ec_state_code_t {
  EC_S_OUT_COMMENT,
  EC_S_SECOND_LEAD,
  EC_S_IN_BLOCKCOMMENT,
  EC_S_IN_LINECOMMENT,
  EC_S_SECOND_TRAIL,
};
typedef enum ec_state_code_t ec_state_code;
 
/*
 * イベントを列挙
 */
enum ec_event_code_t {
  EC_E_STAR,
  EC_E_SLASH,
  EC_E_LF,
  EC_E_OTHER,
};
typedef enum ec_event_code_t ec_event_code;

/*
 * イベントおよび付随するデータ
 */
struct ec_event_t {
  ec_event_code event_code_;
  /* your ftuff here */
};
typedef struct ec_event_t ec_event;

/*
 * アクションの実行に必要なデータ
 */
struct ec_context_t {
  int dummy; /* you may remove this */
  /* your stuff here */
};
typedef struct ec_context_t ec_context;

/* ---------------- */

#ifdef __cplusplus
extern "C" {
#endif

event_code  ec_event_get_event_code(const event* ev);
void        ec_event_set_event_code(event* ev, event_code evc);
void        ec_event_make(ec_event* ev, void* arg, ec_state_code st);

bool        ec_context_setup(ec_context* ctx, void* arg);
void        ec_context_teardown(ec_context* ctx);

transition* ec_state_transition_table(void);

const char* ec_state_code_to_c_str(state_code st);
const char* ec_event_code_to_c_str(event_code ev);

#ifdef __cplusplus
}
#endif

#endif
list03 ec_state_transition.c
#include <assert.h>
#include <stddef.h>

#include "ec_state_transition.h"

/* -----------------------
 * state 
 * -----------------------
 */

// stcに対応する文字列を返す
const char* ec_state_code_to_c_str(state_code stc) {
  const char* result;
  switch ( stc ) {
  case EC_S_OUT_COMMENT : 
    result = "EC_S_OUT_COMMENT"; break;
  case EC_S_SECOND_LEAD : 
    result = "EC_S_SECOND_LEAD"; break;
  case EC_S_IN_BLOCKCOMMENT : 
    result = "EC_S_IN_BLOCKCOMMENT"; break;
  case EC_S_IN_LINECOMMENT : 
    result = "EC_S_IN_LINECOMMENT"; break;
  case EC_S_SECOND_TRAIL : 
    result = "EC_S_SECOND_TRAIL"; break;
  default                : 
    result = "ec_S_BAD_STATE";       break;
  }
  return result;
}

/* -----------------------
 * event 
 * -----------------------
 */

// ev内のevent_codeを返す
event_code ec_event_get_event_code(const event* ev) {
  return ((const ec_event*)ev)->event_code_;
}

// ev内にevent_codeを設定する
void       ec_event_set_event_code(event* ev, event_code evc) {
  ((ec_event*)ev)->event_code_ = evc;
}

// event_codeに対応した文字列を返す
const char* ec_event_code_to_c_str(event_code ev) {
  const char* result;
  switch ( ev ) {
  case EC_E_STAR : 
    result = "EC_E_STAR"; break;
  case EC_E_SLASH : 
    result = "EC_E_SLASH"; break;
  case EC_E_LF : 
    result = "EC_E_LF"; break;
  case EC_E_OTHER : 
    result = "EC_E_OTHER"; break;
  default      : 
    result = "EC_E_BAD_EVENT"; break;
  }
  return result;
}

// argおよびstを基に、ev内にevent_codeおよび付帯情報を設定する
void ec_event_make(ec_event* ev, void* arg, ec_state_code st) {
  /* your stuff here */
}

/* -----------------------
 * context
 * -----------------------
 */

// contextを初期化する。成功/失敗時にtrue/falseを返す。
bool ec_context_setup(ec_context* ctx, void* arg) {
  /* your stuff here */
  return true;
}

// contextを解体する。
void ec_context_teardown(ec_context* ctx) {
  /* do nothing */
}

/* -----------------------
 * action
 * -----------------------
 */

/* ec action for state:OUT_COMMENT event:STAR */
static int ec_action_S0_E0(const event* ev, context* ctx) {
  int result = 0;
#ifndef STATEMACHINE_TEST
  /* your stuff here */
#endif  
  return result;
}

... 中略 ...

/* -----------------------
 * state_transition_table
 * -----------------------
 */
transition* ec_state_transition_table(void) {
  static transition table[] = {
    { EC_S_OUT_COMMENT    , EC_E_STAR , &ec_action_S0_E0, { EC_S_OUT_COMMENT,  }},
    { EC_S_OUT_COMMENT    , EC_E_SLASH, &ec_action_S0_E1, { EC_S_SECOND_LEAD,  }},
    { EC_S_OUT_COMMENT    , EC_E_LF   , &ec_action_S0_E2, { EC_S_OUT_COMMENT,  }},
    { EC_S_OUT_COMMENT    , EC_E_OTHER, &ec_action_S0_E3, { EC_S_OUT_COMMENT,  }},
    { EC_S_SECOND_LEAD    , EC_E_STAR , &ec_action_S1_E0, { EC_S_IN_BLOCKCOMMENT,  }},
    { EC_S_SECOND_LEAD    , EC_E_SLASH, &ec_action_S1_E1, { EC_S_IN_BLOCKCOMMENT,  }},
    { EC_S_SECOND_LEAD    , EC_E_LF   , &ec_action_S1_E2, { EC_S_OUT_COMMENT,  }},
    { EC_S_SECOND_LEAD    , EC_E_OTHER, &ec_action_S1_E3, { EC_S_OUT_COMMENT,  }},
    { EC_S_IN_BLOCKCOMMENT, EC_E_STAR , &ec_action_S2_E0, { EC_S_SECOND_TRAIL,  }},
    { EC_S_IN_BLOCKCOMMENT, EC_E_SLASH, &ec_action_S2_E1, { EC_S_IN_BLOCKCOMMENT,  }},
    { EC_S_IN_BLOCKCOMMENT, EC_E_LF   , &ec_action_S2_E2, { EC_S_IN_BLOCKCOMMENT,  }},
    { EC_S_IN_BLOCKCOMMENT, EC_E_OTHER, &ec_action_S2_E3, { EC_S_IN_BLOCKCOMMENT,  }},
    { EC_S_IN_LINECOMMENT , EC_E_STAR , &ec_action_S3_E0, { EC_S_IN_LINECOMMENT,  }},
    { EC_S_IN_LINECOMMENT , EC_E_SLASH, &ec_action_S3_E1, { EC_S_IN_LINECOMMENT,  }},
    { EC_S_IN_LINECOMMENT , EC_E_LF   , &ec_action_S3_E2, { EC_S_OUT_COMMENT,  }},
    { EC_S_IN_LINECOMMENT , EC_E_OTHER, &ec_action_S3_E3, { EC_S_IN_LINECOMMENT,  }},
    { EC_S_SECOND_TRAIL   , EC_E_STAR , &ec_action_S4_E0, { EC_S_SECOND_TRAIL,  }},
    { EC_S_SECOND_TRAIL   , EC_E_SLASH, &ec_action_S4_E1, { EC_S_OUT_COMMENT,  }},
    { EC_S_SECOND_TRAIL   , EC_E_LF   , &ec_action_S4_E2, { EC_S_IN_BLOCKCOMMENT,  }},
    { EC_S_SECOND_TRAIL   , EC_E_OTHER, &ec_action_S4_E3, { EC_S_IN_BLOCKCOMMENT,  }},
    { -1, -1, NULL, { -1 }} /* DO NOT FORGET THIS TERMINATOR! */
  };
  return table;
}

次のページ
設計をコードでテストする

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/8359 2015/01/14 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング