出力ファイルを用意し、変更したい部分を置き換えて出力ファイルを生成する。
基本的な考えは、変化させたい部分を呼び出し(call-outs)によって置き換え、出力ファイルを書きだす。
Templated Generationは非常に古く身近なテクニックで、Web開発にもよく利用されている。Cのprintf関数やテキストマクロのプリプロセッサもTemplated Generationの一つ。Templated GenerationとTransformer Generationは混同しやすい。
Templated Generationには3つの主要コンポーネントがある。出力ファイルのソーステキストを表すテンプレート、テンプレートを生成する際のデータ構造を表すコンテキスト、テンプレートとコンテキストから出力を生成するツールであるテンプレートエンジン。
最も一般的な形式のテンプレートプロセッサは、JSPやASPのように、呼び出しによって置き換えられる任意のホストコード表現を可能にする。呼び出し内では、可能なかぎりEmbedment Helperを使ってシンプルな関数呼び出しのみに制限すること。
カオスを防ぐため、多くのテンプレートプロセッサは任意のホストコードを許可しておらず、テンプレート言語を提供している。
基本はmapデータ構造をコンテキストとして扱うが、繰り返し(loop)や条件(if)、サブルーチンが必要になっていくケースも多い。
Templated Generationの最大の強みは、テンプレートファイルを見ることができ、生成された出力がどのようになるかが理解しやすいこと。
Templated Generationを使う最初の指標は、生成ファイル内に静的コンテンツが沢山あるかどうか。2つ目は動的コンテンツの複雑度で、複雑度を増していくとTransformer Generationを検討すべき。
例はModel Ignorant Generationで取りあげたコードを生成することにする。
#define EVENT_doorClosed "D1CL" #define EVENT_drawOpened "D2OP" #define EVENT_lightOn "L1ON" #define EVENT_doorOpened "D1OP" #define EVENT_panelClosed "PNCL" #define STATE_idle 1 #define STATE_active 0 #define STATE_waitingForDraw 3 #define STATE_unlockedPanel 2 #define STATE_waitingForLight 4 #define COMMAND_lockDoor "D1LK" #define COMMAND_lockPanel "PNLK" #define COMMAND_unlockPanel "PNUL" #define COMMAND_unlockDoor "D1UL" static int current_state_id = -99; void init_controller() { current_state_id = STATE_idle; } void hard_reset() { init_controller(); } void handle_event_while_idle (char *code) { if (0 == strcmp(code, EVENT_doorClosed)) { current_state_id = STATE_active; } if (0 == strcmp(code, EVENT_doorOpened)) { current_state_id = STATE_idle; send_command(COMMAND_unlockDoor); send_command(COMMAND_lockPanel); } } void handle_event_while_active (char *code) { if (0 == strcmp(code, EVENT_lightOn)) { current_state_id = STATE_waitingForDraw; } if (0 == strcmp(code, EVENT_drawOpened)) { current_state_id = STATE_waitingForLight; } if (0 == strcmp(code, EVENT_doorOpened)) { current_state_id = STATE_idle; send_command(COMMAND_unlockDoor); send_command(COMMAND_lockPanel); } } void handle_event_while_waitingForDraw (char *code) { if (0 == strcmp(code, EVENT_drawOpened)) { current_state_id = STATE_unlockedPanel; send_command(COMMAND_unlockPanel); send_command(COMMAND_lockDoor); } if (0 == strcmp(code, EVENT_doorOpened)) { current_state_id = STATE_idle; send_command(COMMAND_unlockDoor); send_command(COMMAND_lockPanel); } } void handle_event_while_unlockedPanel (char *code) { if (0 == strcmp(code, EVENT_panelClosed)) { current_state_id = STATE_idle; send_command(COMMAND_unlockDoor); send_command(COMMAND_lockPanel); } if (0 == strcmp(code, EVENT_doorOpened)) { current_state_id = STATE_idle; send_command(COMMAND_unlockDoor); send_command(COMMAND_lockPanel); } } void handle_event_while_waitingForLight (char *code) { if (0 == strcmp(code, EVENT_lightOn)) { current_state_id = STATE_unlockedPanel; send_command(COMMAND_unlockPanel); send_command(COMMAND_lockDoor); } if (0 == strcmp(code, EVENT_doorOpened)) { current_state_id = STATE_idle; send_command(COMMAND_unlockDoor); send_command(COMMAND_lockPanel); } } void handle_event(char *code) { switch(current_state_id) { case STATE_idle: { handle_event_while_idle (code); return; } case STATE_active: { handle_event_while_active (code); return; } case STATE_waitingForDraw: { handle_event_while_waitingForDraw (code); return; } case STATE_unlockedPanel: { handle_event_while_unlockedPanel (code); return; } case STATE_waitingForLight: { handle_event_while_waitingForLight (code); return; } default: { printf("reached a bad spot"); exit(2); } } }
テンプレートエンジンはVelocityを利用する。イベント定義から見ていく。テンプレートのコードは以下のとおり。
#foreach ($e in $helper.events) #define $helper.eventEnum($e) "$e.code" #end
velocityのテンプレートコマンドとCプリプロセッサで「#」がカブったがvelocity側にて#defineを無視する。#foreachは繰り返し命令。Embedment Helperを利用し、SwitchHelper?のインスタンスをvelocityコンテキストに置く。
class SwitchHelper... private StateMachine machine; public SwitchHelper(StateMachine machine) { this.machine = machine; } public Collection<Event> getEvents() { return machine.getEvents(); }
コードの中で参照する定数を作成するのはもう少し作業が必要。
class SwitchHelper... public String eventEnum(Event e) { return String.format("EVENT_%s", e.getName()); }
イベントコードそのものを使うこともできるが、読みやすさの観点から定数を生成しました。次は、状態を生成する部分。
#foreach ($s in $helper.states) #define $helper.stateEnum($s) $helper.stateId($s) #end
class SwitchHelper... public Collection<State> getStates() { return machine.getStates(); } public String stateEnum(State s) { return String.format("STATE_%s", s.getName()); } public int stateId(State s) { List<State> orderedStates = new ArrayList<State>(getStates()); Collections.sort(orderedStates); return orderedStates.indexOf(s); }
条件文の生成。
void handle_event(char *code) { switch(current_state_id) { #foreach ($s in $helper.states) case $helper.stateEnum($s): { handle_event_while_$s.name (code); return; } #end default: { printf("reached a bad spot"); exit(2); } } }
内側の条件の生成。
#foreach ($s in $helper.states) void handle_event_while_$s.name (char *code) { #foreach ($t in $helper.getTransitions($s)) if (0 == strcmp(code, $helper.eventEnum($t.trigger))) { current_state_id = $helper.stateEnum($t.target); #foreach($c in $t.target.commands) send_command($helper.commandEnum($c)); #end } #end } #end
各状態の遷移を取得する。Semantic Modelで定義された遷移とその他のイベント遷移の両方が必要。
class SwitchHelper... public Collection<Transition> getTransitions(State s) { Collection<Transition> result = new ArrayList<Transition>(); result.addAll(s.getTransitions()); result.addAll(getResetTransitions(s)); return result; } private Collection<Transition> getResetTransitions(State s) { Collection<Transition> result = new ArrayList<Transition>(); for (Event e : machine.getResetEvents()) { if (!s.hasTransition(e.getCode())) result.add(new Transition(s, e, machine.getStart())); } return result; }