ホーム < ゲームつくろー! < デザインパターン習得編
Interpreter
〜式をクラスのつながりで表して答えを出す
@ Interpreterパターンとは何だろう?
クラスを組み合わせるオブジェクト指向プログラムは、いわば「動作の型」です。「こう命令すると、こういう答えが返ってくるはず」という一連の動作を定義しています。これらの動作はメンバ関数が役割を担いますね。しかし、クラスにはもう1つの要素「メンバ変数」が存在します。これはあるオブジェクトの具体的な「実体」を表す値で、オブジェクトの振る舞いに影響を与えます。
オブジェクトの実体である値、すなわち「データ」は誰が与えるのでしょうか?実は、これはクラスの組み合わせとは異なるプログラムのもう1つの局面であります。膨大なデータをまさかプログラムやクラス内に直接書き込むわけにはいきません。普通、データはファイルにまとめられます。そのファイルにあるデータを読み出してオブジェクトに渡す。この一連の流れには必ず読み出すための「文法」が関わります。ファイル上に体系的な文法で書かれた小さなプログラム。それが「スクリプト」に当たります。
スクリプトは単純な物から恐ろしく複雑な物まで多種多様存在しています。プログラマが勝手に決めてしまう物ですから、それこそプログラマの数だけスクリプト文法が存在するかもしれません。それ自体はたいした問題ではないと思います。しかし、問題はそれを読み出す仕組みの方です。多種多様な文法に対応する読み出し部分は、どうしてもそれ専用になります。その専用部分をいかに柔軟にするか?その1つの答えが「Interpreterパターン」です。
非常に多くのスクリプトにはある共通項があります。まず、スクリプトには「文字・数字」があります。文字や数字が集まると、それは「字句」となります。英単語のようなものですね。そして、字句の並びが「文法」となります。コンピュータは曖昧を嫌いますから、スクリプトで用いる文法は英語などの文法と違い、厳格で例外はありません。
厳格な文法ということは、ある字句が来た時に「次に何が来るかがわかる」。ここがInterpreterパターンのキーです。Interpreterパターンは、字句を「オブジェクト」と考え、そのオブジェクトが次の字句に相当するオブジェクトを作ります。作られたオブジェクトは、次に来た字句に見合うオブジェクトをまた作っていく。これを数珠繋ぎにしていくと、字句の繋がりがそっくりオブジェクトのつながりに置き換わってしまいます。
Interpreterパターンをざっと見た時、「字句をオブジェクトに置き換えて、どうなのよ?」と私などは思ってしまいまいた。字句に無くてオブジェクトにあるもの。それは「振る舞い」です。ある字句に対応して振る舞えるということは、字句が立派にミニプログラムとして働き、何かの答えを返してくるということです。
やけに文章が長くなってしまいました。まとめると、ある文法に従っているスクリプトを解析して、振る舞いを持つオブジェクトに字句を置き換えて何かの結果を得る。それがInterpreterパターンです。
A 簡単なスクリプトでInterpreterパターンの実験
このパターン、はっきりいって実践した方が覚えます。そこで、激烈に簡単なスクリプトで練習してみましょう。
練習スクリプト 0 go 100
1 go 50
これは、一番左に行番号、次に命令「go」、次に移動距離というくくりで1つの動作を定義しているスクリプトです。2行ありますから、2回命令を出して150だけ進むことになります。ここで、「0」「g」「o」などが文字、「0」「go」「100」などが字句(token: トークン)です。我々が字句と判断するのは、そこに区切りがあるからでして、上のスクリプトは「スペース」を区切り文字としています。
先に、字句をオブジェクトにすると言いました。字句は様々ですが「私はだあれ?」という事は必ずしますから、字句を表す抽象クラスとして「CNode」というクラスを設け、構文解析をするParse関数を設けます。Parseとは正に「構文解析する」という意味で、関数の引数には解析したい文字列を渡します。
1行目に注目します。最初は「行番号」を表しますから「CLineNode」などとクラス名を付けることにします。これはCNodeクラスの派生クラスです。次は「go」という具体的な動作を表す大切な部分ですから「CGoNode」とします。その次は移動距離ですが、これは具体的なデータですからCGoNodeオブジェクトに格納することにします。
Parse関数に渡す文字列とは具体的にはスクリプトの文字列そのものです。上の場合、スタートはCLineNodeオブジェクトから始まります。CLineNode::Parse関数内は次のようになるでしょう。
CLineNode::Parse関数 void CLineNode::Parse(Context &str)
{
// 引数の文字の先頭字句が数値かをチェック
if(str.IsTokenNum() == TRUE){
// 次のトークンをチェック
if(str.CheckNextToken("go") == TRUE){
CGoNode node;
node.Parse(str.MoveNext()); // 次のトークンに移った文字列を引数にする
}
}
}
関数の中で最初に先頭の行番号をチェックし、正しければ次の字句である「go」をチェックします。それも正しければ、CGoNode型のオブジェクトを生成して、そのParse関数に「go」を先頭とする文字列を渡します。Contextクラスは字句分割を担当するクラスですが、ここでは詳しく説明しません。字句をばらばらに出来るんだなとくらいに思って頂ければ十分です。
CGoNode::Perse関数は次のようになります。
CGoNode::Parse関数 void CGoNode::Parse(Context &str)
{
// 次のトークンをチェック
if(str.IsNextTokenNum() == TRUE){
// 移動距離を格納
m_Dist = str.GetNextToken(); // 数値変換は出来る物とします
str.SkipToken(); // 字句を1つスキップさせる
}
}
渡された文字列の先頭は「go」のはずですから、その次の字句には距離を表す「数字」が来ているはずです。よって、それをm_Distに格納して、字句を1つ飛ばします。これにより、先頭は「改行」になります(次の「1」では無いことに注意)。
さて、この段階で、CGoNodeオブジェクトに距離100が格納されることがわかります。後は、このオブジェクトを外に出すなりリストに格納するなりすれば「振る舞い」として利用できそうです。
このようにInterpreterパターンは、必要なオブジェクトを動的にどんどん作り出していきます。go以外にbackとかleftとか、色々な行動が定義でいると思いますが、その場合にもそれらに相当するクラスを新しく作っていってCLineNode::Parse関数に追加していけば、複雑なスクリプトも扱えます。この拡張性と柔軟性がInterpreterパターンの魅力ですね。さらに拡張していけば、ループや条件分岐も可能になります。
B オブジェクトに振舞ってもらうのがなかなか難しい
Interpreterパターンの本来の目的は、Nodeクラス群が一まとまりで柔軟に振舞うことで「ある1つの答えを出す」ということです。「オブジェクト指向における再利用のためのデザインパターン改訂版」のC++での実装では、(true and x) or (y and (not x))のような評価式に対してxやyに値を入れたときの答えを解析してBOOL型の結果を返すInterpreterパターンを例にしています。沢山のオブジェクトを使いますが答えは1つなのです。
一方スクリプトのように評価する式が行ごとに異なり、返す答えが複数で様々な型の場合、実装はかなり面倒なのです。例えば、先ほどのスクリプトに「loadbmp bmp/character01.bmp」というスクリプトを追加し、このトークンに対応するクラスにとしてCLoadBMPNodeクラスを作ったとします。ファイル名を格納することはデータなのでできます。でも、このCLoadBMPNodeオブジェクトを外に出して振舞わせるのが難しいのです。なぜなら、先ほどのCGoNodeクラスも外に出したいのですが、それらに共通するのはCNode親クラスだけだからです。リストや配列を渡して外に持ち出したとしても、CNodeクラスのポインタを格納するのがやっとでしょう。ポインタを通してでは、異なる振る舞いをさせることが出来ません。
1つの解決策は「ダウンキャスト」です。リストのポインタをダウンキャストして特定の子オブジェクトへのポインタに変換できれば、任意の振る舞いはし放題です。しかし、どうしてもswitch〜caseが入ってくるので、即時性(リアルタイムで実行を振り分けていく事)は多少厳しくなります(正にインタプリタ言語ですね)。別の切り口は「Interpreterパターンではデータを整理してもらうだけに限定する」という使い方です。ただ、これはあまりにも寂しいかもしれません。Visitorパターンを使うことも考えられますが、Visitorオブジェクトに難しい仕事をさせると使う方が大混乱しますので、やはりちょっと難しい気もします。
結局のところ、Interpreterパターンは、複雑すぎる振る舞いを期待するスクリプトにはあまり向いていないかもしれません。非常に複雑なスクリプトの場合、スクリプトアナライザーのような本格的な解析プログラムを作った方が良いかもしれません。このパターン、取り扱い注意のようですね。