「西洋スパゲッティが大好き、スパゲッティコードが嫌い」
「スパゲッティコード」は、認知的観点と美的観点の両方から蒸し暑いカオスであるソフトウェアを記述するための理想的な表現です。 この記事では、スパゲッティコードを破壊するための3点計画について説明します。
- スパゲッティコードがそれほど美味しくない理由について説明します。
- コードが実際に行うことの新しい外観を紹介します。
- 開発者がペーストのボールを解くのに役立つFrame Machine Notation(FMN)について議論しています。
私たちは皆、誰かのコードを読むのがどれほど難しいか知っています。 これは、タスク自体が複雑であるか、コードの構造が「創造的」すぎるためです。 多くの場合、これら2つの問題は密接に関連しています。
課題は複雑なタスクであり、通常、革新的な発見以外はそれらを単純化できません。 ただし、ソフトウェア構造自体が不必要な複雑さを追加することがあり、この問題は解決する価値があります。
スパゲッティコードのさは、複雑な条件付きロジックにあります。 そして、多くのトリッキーなif-then-elseコンストラクトがないと人生を想像するのは難しいかもしれませんが、この記事ではより良い解決策を示します。
スパゲッティコードを使用して状況を説明するには、まずこれを有効にする必要があります。
クリスピーパスタ
これで:
アルデンテ!
料理を始めましょう。
暗黙の状態
パスタを作るには、必ずお湯が必要です。 ただし、スパゲッティコードを含むこのような一見単純な要素でさえ、非常に混乱する可能性があります。
以下に簡単な例を示します。
(temp < 32)
このチェックは本当に何をしますか? 明らかに、数値の行を2つの部分に分割していますが、これらの部分はどういう意味ですか? あなたは論理的な仮定を立てることができると思いますが、問題はコードが実際にこれを明示的に報告しないことです。
彼女が水が固まっているかどうかを彼女が確認していることを本当に確認したら[おおよそ。 レーン:華氏スケールによると、水は+32度で凍結します] 、論理的にはfalseの戻り値を意味しますか?
if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) }
チェックでは数値を2つのグループに分けましたが、実際には3つの論理状態があります-固体、液体、気体(固体、液体、ガス)です!
つまり、次の数値行:
次のように条件チェックで区切られています。
if (temp < 32) {
} else {
}
スパゲッティコードの性質を理解するために非常に重要であるため、何が起こったかに注目してください。 ブールチェックは、数値空間を2つの部分に分割しましたが、システムを(SOLID、LIQUID、GAS)からの実際の論理構造として分類しませんでした。 代わりに、チェックはスペースを(SOLID、その他すべて)に分割しました。
同様のチェックを次に示します。
if (temp > 212) { // GAS water } else { // not GAS water. is (SOLID | LIQUID) }
視覚的には、次のようになります。
if (temp > 212) {
} else {
}
以下に注意してください。
- 可能な状態の完全なセットはどこにも宣言されていません
- 条件構造のどこにも、検証可能な論理状態または宣言された状態のグループがない
- 一部の状態は、条件付きロジックと分岐の構造によって間接的にグループ化されます
このようなコードは脆弱ですが、非常に一般的であり、サポートに問題を引き起こすほど大きくはありません。 状況を悪化させましょう。
とにかくあなたのコードが好きではなかった
上記のコードは、物質の3つの状態-固体、液体、ガスの存在を意味します。 しかし、科学データによると、実際にはプラズマ(PLASMA)が含まれる4つの観測可能な状態があります(実際、他にも多くありますが、これで十分です)。 誰もプラズマからペーストを準備していませんが、このコードがGithubで公開され、高エネルギー物理学を研究している大学院生によって分岐された場合、この状態も維持する必要があります。
ただし、プラズマを追加する場合、上記のコードは単純に次のことを行います。
if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) + (PLASMA?) // how did PLASMA get in here?? } if (temp > 212) { // GAS water + (PLASMA) // again with the PLASMA!! } else { // not GAS water. is (SOLID | LIQUID) }
古いコードは、多くのプラズマ状態に追加されると、elseブランチで破損する可能性があります。 残念ながら、コード構造には、新しい状態の存在を報告したり、変更に影響を与えたりするものは何もありません。 さらに、バグは目立たない可能性が高いです。つまり、バグを見つけることが最も困難です。 スパゲッティの中の虫にはノーとだけ言ってください。
要するに、問題はこれです。ブールチェックは状態を間接的に決定するために使用されます。 多くの場合、論理状態は宣言されておらず、コードに表示されません。 上で見たように、システムが新しい論理状態を追加すると、既存のコードが壊れる場合があります。 これを回避するには、 開発者は個々の条件チェックとブランチを再検査して、コードパスがすべての論理状態に対して有効であることを確認する必要があります。 これは、大きなコードフラグメントがより複雑になるにつれて劣化する主な理由です。
条件付きデータチェックを完全に削除する方法はありませんが、それらを最小化する手法はコードの複雑さを軽減します。
ここで、水量の非常に単純なモデルを作成するクラスの典型的なオブジェクト指向の実装を見てみましょう。 クラスは水の状態の変化を管理します。 この問題の古典的な解決策の問題を研究した後、 Frameと呼ばれる新しい表記法について議論し、発見した困難にどのように対処できるかを示します。
まず水を沸騰させて......
科学は、温度が変化したときに物質ができるすべての可能な遷移に名前を付けました。
このクラスは非常に単純です(特に有用ではありません)。 状態間の遷移の課題に答え、目的のターゲット状態に適するまで温度を変更します。
(注:私はこの擬似コードを書きました。あなた自身の危険とリスクでのみあなたの仕事でそれを使用してください。)
class WaterSample { temp:int Water(temp:int) { this.temp = temp } // gas -> solid func depose() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do depose while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // gas -> liquid func condense() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do condense while (temp > WATER_GAS_TEMP) decreaseTemp(1) } // liquid -> gas func vaporize() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do vaporize while (temp < WATER_GAS_TEMP) increaseTemp(1) } // liquid -> solid func freeze() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do freeze while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // solid -> liquid func melt() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do melt while (temp < WATER_SOLID_TEMP) increaseTemp(1) } // solid -> gas func sublimate() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do sublimate while (temp < WATER_GAS_TEMP) increaseTemp(1) } func getState():string { if (temp < WATER_SOLID_TEMP) return "SOLID" if (temp > WATER_GAS_TEMP) return "GAS" return "LIQUID" } }
最初の例と比較して、このコードには特定の改善点があります。 まず、ハードコードされた「マジック」番号(32、212)は、状態温度境界の定数(WATER_SOLID_TEMP、WATER_GAS_TEMP)に置き換えられます。 この変更により、間接的ではありますが、状態がより明確になり始めます。
また、このコードは、操作に不適切な状態にあるメソッドの呼び出しを制限する「防御的プログラミング」チェックも導入しています。 たとえば、水が液体でない場合、水は凍結できません。これは(自然の)法に違反します。 ただし、ウォッチドッグ条件を追加すると、コードの目的の理解が複雑になります。 例:
// liquid -> solid if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError()
この条件付きチェックは次のことを行います。
-
temp
GAS限界温度より低いかどうかを確認します -
temp
が固体限界温度を超えているかどうかを確認します - これらのチェックのいずれかが真でない場合、エラーを返します
このロジックは紛らわしいです。 第一に、液体状態にあるかどうかは、物質が固体または気体ではないものによって決定されます。
(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid?
次に、コードは水が液体かどうかを確認して、エラーを返す必要があるかどうかを調べます。
!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously?
この状態の二重否定を初めて理解するのは簡単ではありません。 式の複雑さをわずかに減らす単純化を次に示します。
bool isLiquidWater = (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) if (!isLiquidWater) throw new IllegalStateError()
isLiquidWater状態は明示的であるため、このコードは理解しやすいです。
現在、問題を解決する最善の方法として、 明示的な状態を修正する手法を検討しています。 このアプローチでは、システムの論理状態がソフトウェアの物理構造になり、コードが改善されて理解が容易になります。
フレームマシン表記
フレームマシン表記法(FMN)は、さまざまなタイプのマシンを定義および実装するためのカテゴリカル、方法論、およびシンプルなアプローチを定義するドメイン固有言語(DSL)です。 簡単にするために、フレームオートマトンを単に「マシン」と呼びます。この表記法は、さまざまなタイプ(ステートマシン、ストアオートマトン、およびオートマトンの最上の進化—チューリングマシン)の理論的基準を定義できるためです。 さまざまなタイプのマシンとそのアプリケーションについて知るには、 Wikipediaのページを調べることをお勧めします。
オートマトン理論は興味深いかもしれませんが(非常に疑わしい声明)、この記事では、システムの構築とコードの記述のためのこれらの強力な概念の実用化に焦点を当てます。
この問題を解決するために、Frameは3つの統合レベルで機能する標準化された表記法を導入します。
- エレガントで簡潔な構文でフレームコントローラーを定義するためのテキストDSL
- Frameが「コントローラー」と呼ぶマシンの形でオブジェクト指向クラスを実装するための一連の参照コーディングパターン
- FMNを使用してグラフィカルに表現するのが難しい複雑な操作を表現する視覚表記 - フレーム視覚表記(FVN)
この記事では、最初の2つのポイント、FMNと参照パターンを検討し、FVNの議論は今後の記事のために残します。
フレームは、いくつかの重要な側面を持つ表記法です。
- FMNには、オートマトンの概念に関連する第1レベルのオブジェクトがありますが、オブジェクト指向言語では使用できません。
- FMN仕様では、FMN表記の実装方法を示す標準コードを擬似コードで定義しています。
- FMNはまもなくオブジェクト指向言語でコンパイル(作業中)できるようになります
注:リファレンス実装は、FMN表記の絶対等価性と、オブジェクト指向言語で実装するための簡単な方法を示すために使用されます。 任意の方法を選択できます。
ここで、Frameの2つの最も重要な第1レベルのオブジェクト、 Frame EventsとFrame Controllersを紹介します。
フレームイベント
FrameEventsは、FMN表記の単純さの不可欠な部分です。 FrameEventは、少なくとも次のメンバー変数を持つ構造体またはクラスとして実装されます。
- メッセージID
- 辞書またはパラメーターリスト
- オブジェクトを返す
FrameEventクラスの擬似コードは次のとおりです。
class FrameEvent { var _msg:String var _params:Object var _return:Object FrameEvent(msg:String, params:Object = null) { _msg = msg _params = params } }
フレーム表記では、FrameEventオブジェクトを識別する@記号を使用します。 必要な各FrameEvent属性には、それにアクセスするための特別なトークンがあります。
@|message| : - _msg @[param1] : [] @^ : _return
多くの場合、FrameEventの動作を指定する必要はありません。 ほとんどのコンテキストは一度に1つのFrameEventのみで機能するため、属性セレクターのみを使用するように表記を確実に簡素化できます。 したがって、アクセスを簡素化できます。
|buttonClick| // Select for a "buttonClick" event _msg [firstName] = "Mark" // Set firstName _params property to "Mark" ^ = "YES" // Set the _return object to "YES"
このような表記は、最初は奇妙に思えるかもしれませんが、イベントのこのような単純な構文がFMNコードの理解を大幅に簡素化する方法がすぐにわかります。
フレームコントローラー
フレームコントローラは、オブジェクト指向クラスであり、フレームマシンを実装するために明確に定義された方法で順序付けられています。 コントローラタイプは、 #プレフィックスで識別されます。
#MyController
これは、次のオブジェクト指向の擬似コードと同等です。
class MyController {}
明らかに、このクラスは特に有用ではありません。 彼が何かできるように、コントローラーはイベントに応答するために少なくとも1つの状態を必要とします。
コントローラは、さまざまなタイプのブロックを含むように構造化されています。ブロックは、ブロックタイプの名前を囲むダッシュで識別されます。
#MyController<br> -block 1- -block 2- -block 3-
コントローラーには、各ブロックのインスタンスを1つだけ含めることができ、ブロックタイプには特定のタイプのサブコンポーネントのみを含めることができます。 この記事では、状態のみを含むことができる-machine-ブロックのみを調べます。 状態は、 $プレフィックストークンによって識別されます。
ここでは、1つの状態のみを持つマシンを含むコントローラーのFMNが表示されます。
#MyController // controller declaration -machine- // machine block $S1 // state declaration
上記のFMNコードの実装は次のとおりです。
class MyController { // -machine- var _state(e:FrameEvent) = S1 // initialize state variable // to $S1 func S1(e:FrameEvent) { // state $S1 does nothing } }
マシンブロックの実装は、次の要素で構成されます。
- _state変数。現在の状態の関数を参照します。 コントローラの最初の状態関数で初期化されます。
- 1つ以上の状態メソッド
フレーム状態メソッドは、次のシグネチャを持つ関数として定義されます。
func MyState(e:FrameEvent);
マシンブロックの実装のこれらの基本を定義した後、FrameEventオブジェクトがマシンとどの程度相互作用するかを確認できます。
インターフェースユニット
マシンの動作を制御するFrameEventsの相互作用は、フレーム表記法のシンプルさとパワーの本質です。 ただし、FrameEventがどこから来たのかという質問にはまだ答えていません。どのようにしてコントローラーにアクセスして制御するのでしょうか。 1つのオプション:外部クライアント自体がFrameEventsを作成および初期化し、_stateメンバー変数が指すメソッドを直接呼び出すことができます。
myController._state(new FrameEvent("buttonClick"))
より良い代替方法は、_stateメンバー変数への直接呼び出しをラップする共通インターフェースを作成することです。
myController.sendEvent(new FrameEvent("buttonClick"))
ただし、オブジェクト指向ソフトウェアを作成する通常の方法に対応する最も手間のかからない方法は、クライアントに代わってイベントを内部マシンに送信する一般的な方法を作成することです。
class MyController { func buttonClick() { FrameEvent e = new FrameEvent("buttonClick") _state(e) return e._return } }
Frameは、呼び出しをFrameEventsの共通インターフェイスに変換するメソッドを含むインターフェイスブロックの構文を定義します。
#MyController -interface- buttonClick ...
interface
ブロックには他にも多くの機能がありますが、この例では、この仕組みの概要を説明します。 シリーズの以下の記事でさらに説明します。
それでは、フレームオートマトンの操作を引き続き学習しましょう。
イベントハンドラー
車を定義する方法を示しましたが、まだ何かを行うための表記法がありません 。 イベントを処理するには、1)処理する必要のあるイベントを選択できること、2)実行中の動作にそれを添付することが必要です。
イベントを処理するためのインフラストラクチャを提供するシンプルなフレームコントローラーを次に示します。
#MyController // controller declaration -machine- // machine block $S1 // state declaration |e1| ^ // e1 event handler and return
上記のように、
_msg
イベントの
_msg
属性にアクセスする
_msg
に、FMN表記は垂直線の角かっこを使用します。
|messageName|
FMNは、returnステートメントを表す指数トークンも使用します。 上記のコントローラーは次のように実装されます。
class MyController { // #MyController // -machine- var _state(e:FrameEvent) = S1 func S1(e:FrameEvent) { // $S1 if (e._msg == "e1") { // |e1| return // ^ } } }
ここでは、FMN表記が理解しやすく、コーディングしやすい実装パターンにどれほど明確に対応しているかを確認します。
イベント、コントローラー、マシン、ステート、イベントハンドラーのこれらの基本的な側面を設定したら、それらの助けを借りて実際の問題の解決に進むことができます。
シングルフォーカスマシン
上記では、かなり役に立たないステートレスコントローラーを見ました。
#MyController
有用性のフードチェーンの1つ上の段階は、単一の状態を持つクラスです。これは、役に立たないものの、単に退屈です。 しかし、少なくとも彼は少なくとも何かをしている 。
最初に、(暗黙の)状態が1つだけのクラスがどのように実装されるかを見てみましょう。
class Mono { String status() { return "OFF" } }
ここでは状態は宣言されておらず、暗示されていませんが、コードが何かを実行した場合、システムは「稼働中」状態にあると仮定しましょう。
また、重要なアイデアを紹介します。インターフェイス呼び出しは、オブジェクトにイベントを送信することに似ていると見なされます。 したがって、上記のコードは、イベントを送信する方法と見なすことができます| status | 常に$ Working状態のMonoクラス。
この状況は、イベントバインディングテーブルを使用して視覚化できます。
次に、同じ機能を示し、同じバインディングテーブルに一致するFMNを見てみましょう。
#Mono -machine- $Working |status| ^("OFF")
実装は次のようになります。
class Mono { // #Mono // -machine- var _state(e:FrameEvent) = Working // initialize start state func Working(e:FrameEvent) { // $Working if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } }
returnステートメントに新しい表記法も導入されていることがわかります。これは、式を評価し、結果をインターフェイスに返すことを意味します。
^(return_expr)
この演算子は同等です
@^ = return_expr
または単に
^ = return_expr
これらの演算子はすべて機能的に同等であり、どれでも使用できますが、
^(return_expr)
は最も表現力があります。
ストーブの電源を入れます
これまでに、状態が0のコントローラーと状態が1のコントローラーを見てきました。 それらはまだあまり有用ではありませんが、私たちはすでに興味深い何かの危機にonしています。
パスタを調理するには、まずストーブの電源を入れる必要があります。 以下は、単一のブール変数を持つ単純なSwitchクラスです。
class Switch { boolean _isOn; func status() { if (_isOn) { return "ON"; } else { return "OFF"; } } }
これは一見して明らかではありませんが、上記のコードは次のイベントバインディングのテーブルを実装しています。
比較のために、同じ動作のFMNを次に示します。
#Switch1 -machine- $Off |status| ^("OFF") $On |status| ^("ON")
これで、Frame表記がコードの目的にどのように一致するかがわかります。つまり、コントローラーの位置に基づいて、イベント(メソッド呼び出し)を動作にアタッチします。 さらに、実装構造はバインディングテーブルにも対応しています。
class Switch1 { // #Switch1 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { // $Off if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { // $On if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } }
この表により、さまざまな状態のコントローラーの目的をすばやく理解できます。 フレーム表記構造と実装パターンの両方に同様の利点があります。
ただし、スイッチには顕著な機能上の問題があります。 状態$ Offで初期化されますが、状態$ Onに切り替えることはできません! これを行うには、 状態変更演算子を入力する必要があります。
状態を変更
状態変更ステートメントは次のとおりです。
->> $NewState
これで、この演算子を使用して$ Offと$ Onを切り替えることができます。
#Switch2 -machine- $Off |toggle| ->> $On ^ |status| ^("OFF") $On |toggle| ->> $Off ^ |status| ^("ON")
対応するイベントバインディングテーブルは次のとおりです。
新しいイベント|トグル| 2つの状態を単純に繰り返す変更がトリガーされます。 状態変更操作はどのように実装できますか?
どこも簡単です。 Switch2の実装は次のとおりです。
class Switch2 { // #Switch2 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^("OFF") } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } }
Switch2で最後の改善を行うことで、状態を切り替えることができるだけでなく、状態を明示的に設定することもできます。
#Switch3 -machine- $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON")
| toggle |イベントとは異なり、if | turnOn | Switch3がすでにオンになっているとき、または| offOff |既にオフになっているときに送信され、メッセージは無視され、何も起こりません。 このわずかな改善により、クライアントはスイッチの状態を明示的に示すことができます。
class Switch3 { // #Switch3 // -machine- var _state(e:FrameEvent) = Off /********************************** $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") ***********************************/ func Off(e:FrameEvent) { if (e._msg == "turnOn") { // |turnOn| _state = On // ->> $On return // ^ } if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } /********************************** $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") ***********************************/ func On(e:FrameEvent) { if (e._msg == "turnOff") { // |turnOff| _state = Off // ->> $Off return // ^ } if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^ } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } }
スイッチの進化の最後のステップは、FMNコントローラーの目的を理解することがいかに簡単かを示しています。関連するコードは、フレームメカニズムを使用して実装することがいかに簡単かを示しています。
Switchマシンを作成したら、火をつけて調理を開始できます!
鳴り具合
オートマトンの重要な側面は、微妙ではありますが、マシンの現在の状態は、状況(たとえば、電源を入れる)またはデータまたは環境の何らかの分析の結果であるということです。マシンが目的の状態に切り替わると、それが暗示されます。車の知識がなくても状況は変わらないということです。
ただし、この仮定は常に正しいとは限りません。状況によっては、現在の論理状態を判断するためにデータの検証(または「センシング」)が必要です。
- 初期復元状態 -マシンが一定の状態から復元されたとき
- 外部状態 -マシンの作成、復元、または操作時に環境に存在する「実際の状況」を定義します
- 揮発性内部状態 -実行中のマシンによって管理される内部データの一部がマシンの制御外で変更できる場合
これらのすべての場合、状況を判断し、それに応じてマシンの状態を設定するには、データ、環境、またはその両方を「プローブ」する必要があります。理想的には、このブール論理は、正しい論理状態を定義する単一の関数で実装できます。このパターンをサポートするために、フレーム表記には、ユニバースをプローブして現在の状況を判断する特別なタイプの機能があります。そのような関数は、状態へのリンクを返すメソッドの名前の前の$プレフィックスで示されます:
$probeForState()
私たちの状況では、このメソッドは次のように実装できます。
func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas }
ご覧のとおり、このメソッドは正しい論理状態に対応する状態関数への参照を返すだけです。この検知機能を使用して、正しい状態に入ることができます。
->> $probeForState()
実装メカニズムは次のようになります。
_state = probeForState()
状態検知方法は、所定の方法で状態を管理するためのフレーム表記の例です。次に、FrameEventsを管理するための重要な表記法についても学習します。
行動の継承とディスパッチャー
動作の継承とディスパッチャーは強力なプログラミングパラダイムであり、この記事のフレーム表記に関する最後のトピックです。
Frameは、データまたは他の属性の継承ではなく、動作の継承を使用します。この状態では、初期状態がイベントを処理しない場合(または、次の記事で説明するように、単にイベントを渡したい場合)、FrameEventsは他の状態に送信されます。この一連のイベントは、任意の深さに到達できます。
このために、マシンはメソッドチェーンと呼ばれる手法を使用して実装できます。ある状態から別の状態にイベントを送信するためのFMN表記は、ディスパッチャ =>です。
$S1 => $S2
このFMNステートメントは、次のように実装できます。
func S1(e:FrameEvent) { S2(e) // $S1 => $S2 }
状態メソッドを連鎖させるのがどれほど簡単かがわかりました。この手法をかなり難しい状況に適用してみましょう。
#Movement -machine- $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) $AtAttention => $Motionless |isStanding| ^(true) $LyingDown => $Motionless |isStanding| ^(false) $Moving |isMoving| ^(true) $Motionless |getSpeed| ^(0) |isMoving| ^(false)
上記のコードでは、2つの基本的な状態-$ Movingと$ Motionless-があり、他の5つの状態はそれらから重要な機能を継承しています。イベントバインディングは、バインディングの一般的な外観を明確に示しています。
私たちが学んだテクニックのおかげで、実装は非常に簡単になります。
class Movement { // #Movement // -machine- /********************************** $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) ***********************************/ func Walking(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 3 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Walking => $Moving } /********************************** $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) ***********************************/ func Running(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 6 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Running => $Moving } /********************************** $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) ***********************************/ func Crawling(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = .5 return } if (e._msg == "isStanding") { e._return = false return } Moving(e) // $Crawling => $Moving } /********************************** $AtAttention => $Motionless |isStanding| ^(true) ***********************************/ func AtAttention(e:FrameEvent) { if (e._msg == "isStanding") { e._return = true return } Motionless(e) // $AtAttention => $Motionless } /********************************** $LyingDown => $Motionless |isStanding| ^(false) ***********************************/ func LyingDown(e:FrameEvent) { if (e._msg == "isStanding") { e._return = false return } Motionless(e) // $AtAttention => $Motionless } /********************************** $Moving |isMoving| ^(true) ***********************************/ func Moving(e:FrameEvent) { if (e._msg == "isMoving") { e._return = true return } } /********************************** $Motionless |getSpeed| ^(0) |isMoving| ^(false) ***********************************/ func Motionless(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 0 return } if (e._msg == "isMoving") { e._return = false return } } }
給水機
これで、FMNに関する基本的な知識が得られ、WaterSampleクラスを状態を使用して、よりインテリジェントな方法で再実装する方法を理解できるようになりました。また、大学院生の物理学者にとって有用なものにし、新しい$プラズマ状態を追加します。
FMNの完全な実装は次のようになります。
#WaterSample -machine- $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |getState| ^("SOLID") $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError()
ご覧のとおり、初期状態は$ Beginで、メッセージに応答します| create |そして価値を保持します
temp
。検知機能は最初に初期値
temp
をチェックして論理状態を決定し、次にマシンのこの状態への移行を実行します。
すべての物理的状態($固体、$液体、$ガス、$プラズマ)は、$デフォルト状態から保護動作を継承します。現在の状態に対して無効なすべてのイベントは、$ Default状態に渡され、InvalidStateErrorエラーがスローされます。これは、振る舞いの継承を使用して、単純な防御プログラミングを実装する方法を示しています。
そして今、実装:
class WaterSample { // -machine- var _state(e:FrameEvent) = Begin /********************************** $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ ***********************************/ func Begin(e:FrameEvent) { if (e._msg == "create") { setTemp(e["temp"]) _state = probeForState() return } } /********************************** $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |sublimate| ^("SOLID") ***********************************/ func Solid(e:FrameEvent) { if (e._msg == "melt") { doMelt() _state = Liquid return } if (e._msg == "sublimate") { doSublimate() _state = Gas return } if (e._msg == "getState") { e._return = "SOLID" return } Default(e) } /********************************** $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") ***********************************/ func Liquid(e:FrameEvent) { if (e._msg == "freeze") { doFreeze() _state = Solid return } if (e._msg == "vaporize") { doVaporize() _state = Gas return } if (e._msg == "getState") { e._return = "LIQUID" return } Default(e) } /********************************** $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") ***********************************/ func Gas(e:FrameEvent) { if (e._msg == "condense") { doCondense() _state = Liquid return } if (e._msg == "depose") { doDepose() _state = Solid return } if (e._msg == "ionize") { doIonize() _state = Plasma return } if (e._msg == "getState") { e._return = "GAS" return } Default(e) } /********************************** $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") ***********************************/ func Plasma(e:FrameEvent) { if (e._msg == "recombine") { doRecombine() _state = Gas return } if (e._msg == "getState") { e._return = "PLASMA" return } Default(e) } /********************************** $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() ***********************************/ func Default(e:FrameEvent) { if (e._msg == "melt") { throw new InvalidStateError() } if (e._msg == "sublimate") { throw new InvalidStateError() } if (e._msg == "freeze") { throw new InvalidStateError() } if (e._msg == "vaporize") { throw new InvalidStateError() } if (e._msg == "condense") { throw new InvalidStateError() } if (e._msg == "depose") { throw new InvalidStateError() } if (e._msg == "ionize") { throw new InvalidStateError() } if (e._msg == "recombine") { throw new InvalidStateError() } if (e._msg == "getState") { throw new InvalidStateError() } } }
おわりに
オートマトンは、ソフトウェアとハードウェアの開発の専門分野でのみ長い間使用されてきたコンピューターサイエンスの基本概念です。 Frameの主な目的は、オートマトンを記述するための表記法を作成し、コードまたは「メカニズム」を実装するための簡単なパターンを設定することです。 Frame表記がプログラマーのマシンの見方を変え、日常のプログラミングタスクでそれらを実践する簡単な方法を提供し、そしてもちろん、コードのスパゲッティからそれらを保存することを願っています。
ターミネーターがパスタを食べる(鈴木さん撮影)
今後の記事では、私たちが学んだ概念に基づいて、FMN表記法のさらに大きな力と表現力を作成します。時間が経つにつれて、FMNを含み、ソフトウェアモデリングへの最新のアプローチにおける不確実な動作の問題を解決するビジュアルモデリングの研究に議論を拡大します。