Adult-Oriented Punk!

memo/20130224

last mod.2020/05/08 


自分だったら、Stateパターンをどう活用するか。


結論から書きます。

Stateパターンが活きるのは、少なくとも、

  • 目的を実現する機構を、状態遷移機械もしくは有限オートマトンとして設計した
  • メッセージ数がとても多い
  • 状態によって、受信者にとって関心のあるメッセージがガラッと変わる
  • 状態によらず、送信者はメッセージを出しつづけたい
の全てを満たす場合、だと考えます。


GoF本のStateパターンの図

たくさんのイベントメッセージは、たくさんのメソッドの羅列で実現

状態基底クラス
 △
 |__具体状態クラス1
 |  ...
 |__具体状態クラスM

// 無視メソッド
int 状態基底クラス::メッセージ1処理メソッド(メッセージ1の具体パラメータ並び v1, v2)
int 状態基底クラス::メッセージ2処理メソッド(メッセージ2の具体パラメータ並び v1, v2, v3)
int 状態基底クラス::メッセージN処理メソッド(メッセージNの具体パラメータ並び v1)

// 具体状態クラス1 が メッセージ1 に関心があるとして 
int 具体状態クラス1::メッセージ1処理メソッド(メッセージ1の具体パラメータ並び v1, v2)

// 具体状態クラス2 が メッセージ1 と メッセージ2 に関心があるとして 
int 具体状態クラス2::メッセージ1処理メソッド(メッセージ1の具体パラメータ並び v1, v2)
int 具体状態クラス2::メッセージ2処理メソッド(メッセージ1の具体パラメータ並び v1, v2, v3)

「メッセージの振り分け」は、言語自体が持っている多態機能にマッピングします。


拡張Stateパターン

私が考える、現場で使えるStateパターン、つまり「拡張Stateパターン」はこうです。

  • メッセージを、union構造体や継承関係を持つクラス群で実装する
    • ので、「メッセージのうちどれか1つ」を受信できることを、union構造体もしくは基底クラス1つのパラメータ形式で表現しきれる

拡張Stateパターンの図

イベントメッセージ基底クラス
 △
 |__具体メッセージクラス1
 |__具体メッセージクラス2
 |  ...
 |__具体メッセージクラスN

状態基底クラス
 △
 |__具体状態クラス1
 |  ...
 |__具体状態クラスM

// 無視メソッド
int 状態基底クラス::メッセージ処理メソッド(イベントメッセージ基底クラス msg)

// 具体状態クラス1 が メッセージ1 に関心があるとして
int 具体状態クラス1::メッセージ処理メソッド(具体メッセージクラス1 msg)

// 具体状態クラス2 が メッセージ1 と メッセージ2 に関心があるとして 
int 具体状態クラス2::メッセージ処理メソッド(具体メッセージクラス1 msg)
int 具体状態クラス2::メッセージ処理メソッド(具体メッセージクラス2 msg)


実際の現場では何がうれしいのか

メッセージ数がN、状態数Mで、状態遷移機械は N x M の大きな一覧表で描けますが、実際の仕事で状態遷移機械を使うときは、NもMも大きな値になり、一覧表は米粒サイズの文字でA3用紙に数枚に渡って印刷するような状況に、よくなります。

そして、ある状態がほしいメッセージは、ごくわずかだ、つまり棲み分けがかなりできている、という状況になることが、よくあります。

このような状況では、「無視する」メソッドを基底クラスに書き、各状態を、別々のソースファイルで書くことで、改修時に「見なくてよいソースを早い段階で枝刈りでき、見なくてすむ」という、うれしい状況になります。





そもそも、なんでStateパターンを再考しようとしたかというと...

リンク備考
状態管理用の変数をインスタンスに持たせるな

この文書を見て、非常に違和感を持ったのです。

リンク先、著者さんの文書は...間違った事は言っていないと...思いたいです。
他のエントリを見ても、よく分かっている人が、極力わかりやすくしようと書かれていると感じます。

けれども、「bestでない例題によって、意図がわかりにくい説明になっている」ように思いました。

私自身、動物クラスを継承する犬や猫がワンニャーと鳴く、のような、「犬猫問題」を持ち出してしまった恥ずかしい過去があります。しかし、小規模プログラムでオブジェクト指向のメリットを説明するのは、本当に難しいのです。

ソフトウェア工学が問題にするのは大規模ソフトウェアを完納すること。中小規模はどうやろうと管理できる。ジャンボジェットをどう設計して完納しようか、という話においては、昆虫の飛行理論は、規模が違いすぎて、専門家にはほぼ別の話なのです。

揚げ足とりであることを承知で苦言を。
  • classがならんでるところをswitch文で書いたとしても、ソースリストの視覚的複雑さ(可読性)は同じですよね。つまり、「if文の条件の並べ方を変えると見やすくなる」という例文になってしまっている。
  • むしろnew処理がある分、switch文で書いた場合より処理が重い。

あとStateとは関係ないですが:
当初から決まっている要件をStateパターンで実装する話ではなく、次々に襲う仕様変更要求に対応する話になっていますが、このような論説は、十分な説得力を得ないことが多いです。

今回、Stateパターンとは何か、ということを深く考えました。

この文脈(だれかがGoF本のパターンを、ネット上のBlog等で物知りさんの立場で説明しているのを読んで、違和感を感じる)において、大事なことが3つあると整理しました。

  • (1)「状態遷移機械」(などGoF本が取り上げるパターンの基礎となる概念)は、クラスの概念や、GoF本より遥か昔から存在しているものである
  • (2) GoF本は「継承機能って、ウィンドウマネージャのプログラミング位にしか使えないよね」と言われていた時代背景において、「古くからある(ウィンドウマネージャ以外の)様々なソフトウェア概念を、クラスと継承関係を用いるとこのように実装できる」という例をたくさん例示した点で優れていたものであって、取り上げた例を実際にクラスと継承関係を用いて実装することが最適だと、比較検討するような事は、一言も言っていない
  • (3) 本当の意味での「ソフトウェアの実装パターン集」としては、POSA本やPOSA2本のほうが、遥かに優れている

ということです。

難しい言い方でごめんなさい。要するに基本的な私の考えは、

GoF本は、OOP言語の継承機能の活用例としてはイケてるけど、ソフトウェアパターンとしてはそんなにイケてない

です。


で。

GoF本が示したStateパターンとは何か。私は、以下のものだと考えています。

  • ソフトウェアを状態遷移機械として(=状態遷移モデルで)設計した場合には、必ず「ディスパッチャ」が必要となる。
  • オブジェクト指向言語は多態機能を実現する機構として"仮想関数テーブル"を持ちます
  • 状態遷移機械のディスパッチャをどう実装するかという「機能-実装 マッピング」選択において、それをオブジェクト指向言語の多態機能(仮想関数テーブル)に割り当てた 場合のことを、GoF本ではStateパターンと呼ぶ

と。

それ以上でもそれ以下でもない。つまり、
  • 状態遷移機械をクラス構造として実装することで、状態遷移機械自体が洗練されるものではないし、
  • 何らかのソフトウェア要件を状態遷移機械として実装するか、しないかの選択に、何らかの示唆を与えるものではない
と考えます。

何よりも、「より適した実例を」出したかったのですが、自分自身が、お金を得る実際の仕事でStateパターンを活用して腹の底から成功した、といえる実体験をしていないので、

これが具体例だ、という例を、出せませんでした。ごめんなさい。

ただし、某、特殊船舶の仕事で、あるものを発射する、というプログラムの実装でStateパターン風の実装を行った経験はあります。

それは
  • 要件として
    • 「インターロックが解除されないと発射できない」もので、
    • 「インターロック解除の条件はいろいろあって」
    • 「インターロック解除状態以外では"発射"メッセージは必ず無視する、ということをソフトウェアソースコード上でわかりやすく実装してほしい」
があって
  • 対応として
    • インターロック状態とインターロック解除状態を別の状態として実装し、
    • ソフトウェアのソースファイルを根本的に分離する
としたものでした。

しかし、このプログラムでは
  • ディスパッチャ機構は、クラスメソッド内で独自に実装していた
  • そのため、ディスパッチャを「仮想関数テーブルにはマッピングしなかった」
のです。
つまり、真の意味でGoF本のようなStateパターンにはしなかったのです。

ということで、なんとも歯切れの悪い結論ですが、私の考えを整理した結果は先頭に書いたとおりです。

(再掲)
Stateパターンが活きるのは、少なくとも、
  • 目的を実現する機構を、状態遷移機械もしくは有限オートマトンとして設計した
  • メッセージ数がとても多い
  • 状態によって、受信者にとって関心のあるメッセージがガラッと変わる
  • 状態によらず、送信者はメッセージを出しつづけたい
の全てを満たす場合、だと考えます。


実際のコーディングでは、状態が遷移するときに、「状態クラス間で内部データを引き継ぎたい」事態が、よく発生します。

卑しい例を出すと、商社において取引先A社の担当はS君で固定だったのが、内部状態の変更でT君に変更になる、という状況で、S君からT君にA社固有のノウハウを引き継ぎしたいのです。

「商社」は状態遷移マシン、「取引先A社」はメッセージの送信者、「S君」「T君」はある状態を実装するクラス、に対応します。

状態変更時、このような引継ぎ問題をどう解決するかは、GoF本のStateパターンでは何も触れていません。

追記: つまり何が言いたいかと言うと、こうです。
GoF本は、ステートパターンなどと命名して大層な事を言っている様に見えますが、実際には、継承というプログラミング言語機能を活用する中級テクニックを解説した物に過ぎません。
クラスを用いて状態を実装する事で、ソフトウェアの可読性や可管理性がどう向上するかを語っていない上に、状態遷移モデルを活用する上で度々問題となる話題に触れていません。
つまりは、真の意味で「状態遷移モデルを用いるソフトウェアの設計理論の発展に寄与した」とは言えないのです。

だから、有能なソフトウェアエンジニアの皆さん。GoF本をありがたがる事なんか辞めて、本質的なソフトウェア設計理論を高める研究をしましょう。





* 日々のメモ