@IT」にはない内容をエンジニアライフで技術紹介するコラム。広く議論する場になることを目指します。2017/05/29今回はVisitorからトークンリストの生成機能(役割)を分離します。TokenManager以前に述べたSample1~4の実装例は字句解析が本来生成するトークン列をVisitorが直に持っています。これでは実際のXMLを読み込むことはできません。トークンリストを生成する機能をVisitorに含めると、Visitorが持つ「巡回する際の処理」という意味合いを大きく外れてしまいます。そこでVisitorの代わりにトークンリストを管理するクラス「TokenManager」を検討します。位置付けTokenManagerクラスは必要があればトークンを生成し、トークンリストに追加します。よってソフトウェア構成において字句解析器に属します。構文解析器が字句解析器に対して要求することがトークンの取得であることから、TokenManagerは構文解析器がアクセスする唯一の字句解析クラス(窓口)としておくことでクラス構成を簡潔にできます。機能要件TokenManagerに必要な機能は以下の5個です。(後述)3.Visitorからの指示を起点に取得したtokenを消費する機能この機能を実行するメソッドをconsumeTokenと命名します。全体のクラス設計に関して簡単に説明した後に上記5つの機能を順に検討します。0.クラス設計に関する機能TokenManagerは、字句解析処理中に唯一つあれば十分です。よってシングルトンパターンで設計します。}}1.無条件字句解析器が取得したDocumentTokenを保持する機能TokenManager#setDocumentTokenメソッドを用意します。Lexer(無条件字句解析器)から呼び出して対象となる文字列バッファをフィールド変数に保持します。}}2.次のtokenを適切な粒度で取得する機能TokenManager#nextTokenメソッドを用意します。このメソッドは、Sample1~4のnextToken()と同じタイミングで呼び出します。TokenManager#nextTokenは以下の機能が必要です。1.トークンの消費指示がない限り同じトークンを取り出す2.トークンの消費指示があると次のトークンを取り出す尚、トークンの消費指示とは後述する3番の機能:consumeTokenのことです。上記の機能を実現するためには2つの方法があります。生成したトークンはトークンリストに追加する生成したトークンを呼び出し元に返す2.nextToken()呼び出し時にトークンリストが空でない場合トークンリストから1つのトークンを取り出す(トークン自体は消えない)3.consumeToken()呼び出し時にトークンリストの先頭を削除する1-1.メリット何度もnextToken()を呼び出す場合に生成コストが少なくて済みます。試行規則(|,*,?のように一致しなくてもエラーにならない規則)が多くても解析時間への影響が少なくて済みます。1-2.デメリット条件付字句解析は取りうる全ての解析ができなくてはいけません。('&#x')を取る可能性4.CDSectの先頭のSYMBOL('<![CDATA[')を取る可能性5.PIの先頭のSYMBOL('<?')を取る可能性6.COMMENTトークンを取る可能性7.contentが空でETagの先頭のSYMBOL('</')を取る可能性があります。上記の検討はCHARDATA_WORDを単に取り出すトークナイザ(トークン生成器)よりずっと複雑な実装になります。■2.トークンバッファを直接操作する方法1.nextToken()呼び出し時にトークンバッファからトークンを生成します。※ただし、consumeToken()が呼ばれるまでトークンバッファを変更しません。2.consumeToken()を呼び出さずに再度nextToken()を呼び出した場合は同じトークンを生成します。3.consumeToken()呼び出し時にトークンバッファから消費したトークン分のバッファを削除します。2-1.メリットトークンの生成を何度も行うため、条件付字句解析は取りうる全ての解析ができる必要がありません。構文によっては間違ったトークンを生成してしまっても良いです。2-2.デメリットnextToken()を呼び出す毎に生成コストが必要なため、consumeToken()を呼び出さない場合(試行規則が多い場合)生成コストが多くなります。構文の適用順序によっては、間違ったトークンを生成したまま処理が終了する場合があります。このとき、エラー文言に表示するトークンはユーザの認識とずれてしまうことがあり得ます。■3.案1と案2の検討XMLの条件付字句解析器においてContentspecの区間やAttValueの区間のためのトークンを取得する部分は複雑になります。そこで、字句解析器の実装が難しくなることを避けるために基本構造は案1を選択します。トークン生成コストを最小限にするため案2とのハイブリット案3(以下に説明)について考えて見ます。■4.FIFOを使いつつ、トークンバッファからの削除処理はconsumeToken()時にする方法1.nextToken()呼び出し時にトークンバッファからトークンを生成してFIFOに追加します。※ただし、consumeToken()が呼ばれるまでトークンバッファを変更しません。2.consumeToken()を呼び出す前で、かつ、FIFOが空でない場合、次のnextToken()を呼び出した時は単にFIFO内の同じトークンを返します。3.consumeToken()呼び出し時にFIFOの先頭トークンを消費します。同時に、先頭トークンと同じ文字数分、トークンバッファからも削除します。4.条件付字句解析の条件が変更になった場合、nextToken()の取り出し内容が変わるかもしれないのでFIFOの中身を空にします。4-1.メリット条件付字句解析の条件が変わらない限り、何度もnextToken()を呼び出す場合に生成コストが少なくて済みます。条件付字句解析は取りうる全ての解析ができなくても良く、構文によっては間違ったトークンを生成してしまっても良くなります。4-2.デメリット構文の適用順序によっては、間違ったトークンを生成したまま処理が終了する場合があります。このとき、エラー文言に表示するトークンはユーザの認識とずれてしまうことがあり得ます。実装は次のようにします。tokenize()部分は次回説明する字句解析器を使ってトークンを取得するため今回は実装しません。}}3.Visitorからの指示を起点に取得したtokenを消費する機能consumToken()メソッドを追加します。内容については、先ほどの案3に沿って実装します。RepetitionやAlternativeでNG判定をするためにトークンの消費数をカウントする機能が必要になります。}}さて少々長くなりましたので、残りの機能は次回コラムとします。通信系と自動車関連のシステム開発の下請けを経て、現在は家業を継いでIT系の仕事から離れています。本コラムは趣味で研究している内容がメインです。