抽象モデルと意味モデル

Martin FowlerのDSL Bookにおける意味モデル("Semantic Model")について、サンプルを示しつつ解説する。

意味モデルとは

意味モデル("Semantic Model")について、Fowlerは次のように解説しています。

A Semantic Model is realy just a Domain Model that is populated by the DSL. As with any Domain Model it contains the heart of the behavior for the domain. The the Semantic Model of a DSL is usually a subset of the overal Domain Model for an application.[sic] From the Domain Model's point of view, the DSL is just a fancy alternative way of creating its objects and hooking them together. From the DSL's point of view the Semantic Model is the output of the overall parsing operation.


意味モデル("Semantic Model")とは、実は、DSLによって値を設定されたドメインモデルに過ぎません。他のドメインモデルと同様に、意味モデルもドメインのための中核的なふるまいを含んでいます。DSLのセマンティックモデルは通常、アプリケーションのために作られたドメインモデル全体のサブセットです。ドメインモデルの観点からすれば、DSLはオブジェクトを生成してそれぞれを結びつけるために別に用意された洒落た方法にすぎませんし、DSLの観点からすれば、意味モデルは全パース処理の結果出力されるものとなります。
http://martinfowler.com/dslwip/SemanticModel.html

ドメインモデルとは別に「意味モデル」の存在を明確に意識しなければならない理由について、サンプルを見ながら考察していきます。

サンプル:グラント嬢のセキュリティ

今回使用するサンプルは、DSL本の冒頭で紹介される「グラント嬢のコントローラ」です。これは一言で言えば、からくり屋敷の制御システムで、「ドアを閉めた後、ライトをつけて引出しを開けると、パネルのロックが外れる」という仕組みになっています(ライトと引出しの順序はどちらでも構いません)。これを実装するために用いられるフレームワークはシンプルなステートマシンです(クラス図は原文を参照して下さい)。なお、実装に使用している言語はGroovyです。

抽象的なモデル

ステートマシンを実装するために、私はテストコードで以下のような状態遷移を設定しました(ソースコードこちら)。

State locked = new State(name:"LOCKED")
State unlocked = new State(name:"UNLOCKED")

Command lockGateCmd = new Command(name:"lockGate", code:"LG")
Command unlockGateCmd = new Command(name:"unlockGate", code:"UG")

Event coinInserted = new Event(name:"coinInserted", code:"CI")
Event passengerPassed = new Event(name:"passengerPassed", code:"PP")

locked.addTransition(coinInserted, unlocked)
locked.addAction(lockGateCmd)

unlocked.addTransition(passengerPassed, locked)
unlocked.addAction(unlockGateCmd)
        
StateMachine stateMachine = new StateMachine(defaultState:locked)

StateMachineController gate = 
    new StateMachineController(stateMachine:stateMachine, 
        currentState:locked)

MissGrantsControllerTest.gate = gate

ここで設定しているのは、「コインを入れるとロックが外れる」というゲート制御機能で、グラント嬢のからくり屋敷とは全く関係がありません。つまり、ドメインモデルの構造自体は抽象的なものにすぎず、そこに「意味」は込められていないのです。実際のアプリケーションのふるまいを制御するのは特定の意味(LOCKやUNLOCKなど)にしたがって組み立てられ、メモリ上に存在しているオブジェクトモデルです。Fowlerが「意味モデル」と呼んでいるのは、このようなオブジェクトモデルのことです。

意味モデルの構築

ドメインモデルが完成した所で、要件を満たす意味モデルを生成するためのDSLを構築します。今回は原文を参考にしつつ、からくり屋敷の状態遷移を以下のようなDSLによって記述することにしました。

void defineStateMachine() {

    def doorClosed  = event("doorClosed",  "D1CL")
    def drawOpened  = event("drawOpened",  "D2OP")
    def lightOn     = event("lightOn",     "L1ON")
    def doorOpened  = event("doorOpened",  "D1OP")
    def panelClosed = event("panelClosed", "PNCL")

    state("idle")
        .actions(
            command("unlockDoor", "D1UL"), 
            command("lockPanel", "PNLK"))
        .when(doorClosed).then(state("active")) 

    state("active")
        .when(drawOpened).then(state("waitingForLight"))
        .when(lightOn).then(state("waitingForDraw"))

    state("waitingForLight")
        .when(lightOn).then(state("unlockedPanel"))

    state("waitingForDraw")
        .when(drawOpened).then(state("unlockedPanel"))

    state("unlockedPanel")
        .actions(
            command("unlockPanel","PNUL"), 
            command("lockDoor", "D1LK"))
        .when(panelClosed).then(state("idle"))

    defaultState "idle"
    startState   "idle"
    }

このうち、stateやcommandといったメソッドは前回のエントリと同じくObject Scopingパターンを用いて、基底クラスに実装しています。

Event event(String name, String code) {
    new Event(name:name, code:code)
}

Command command(String name, String code ) {
    new Command(name:name, code:code)
}

StateBuilder state(String name) {
    if(!states.containsKey(name)) { 
        states[name] = new StateBuilder(name) 
    }
    states[name]
}

一方、when(イベント).then(状態)の部分は、Method Chainingパターンを使って実装しています。

public class StateBuilder implements IAbstractEventBuilder, 
    IStateBuilder {

    State state

    Event trigger
    
    public StateBuilder(String name) {
        state = new State(name:name)
    }

    IAbstractEventBuilder actions(Command... actions) {
        actions.each { state.addAction(it) }
        this
    }

    IStateBuilder when(Event trigger) {
        this.trigger = trigger
        this
    }

    IAbstractEventBuilder then(StateBuilder target) {
        state.addTransition(trigger, target.state)
        this
    }
}

ポイントとしては以下の3点です。

  • DSLメソッドの呼び出しの際に、内部ではStateのコマンドクエリAPIを操作している。
  • メソッドが連鎖するように、各イベントが戻り値としてthisを返している。
  • when...then...の順序を守るため、それぞれのメソッドを定義したインタフェースを準備している。
流れるようなインタフェース

Fluent Interfaceについて、若干補足しておきます。今回紹介したMethod Chainingは「流れるようなインタフェース」の代表格ですが、DSL本の中でFowlerが「Fluent Interface」ないし「Fluent API」という表現を用いる場合に指しているのは、このようなthisを返すsetterだけではありません。これについては、Fowler自身もこう明記しています。

For many people the central pattern for a fluent interface is that of Method Chaining. ... But Method Chaining isn't the only way we can use combine functions to create more fluent fragments of program code.


多くの人にとって、fluent interfaceの中心的なパターンはMethod Chainingのそれでしょう。(中略)しかし、Method Chainingだけが、関数を組み合わせてよりfluentなプログラムコードの断片を作る方法だという訳ではありません。
Fluent and command-query APIs

"fluent"という単語に込められているのが、液体が流れるようにメソッドがつながっていくイメージだけではなく、言語的な流暢さであるということは重要です。

まとめ

グラント嬢のからくり屋敷をサンプルに、意味モデルについて見てきました。サンプルで使われているMethod Chainingの実装は結果だけ見るとやや難しく見えるかもしれませんが、実際の実装は以下の順序で行っており、それほど難解なものではありません。

  1. まず作成したいDSLdefineStateMachineメソッド)を最初に記述する。
  2. DSLメソッドがStateBuilderを返す状態でStateBuilderを実装する。
  3. DSLの記述ルールに従って、IXxxBuilderインタフェースを切り出す。

DSLは意味モデルを読みやすく構築するためのものにすぎず、重要なのはあくまでもドメインモデルであるということは強調しておく価値があります。特にこういった抽象モデルの構築にはオブジェクト指向に関する深い理解が必要だと言えるでしょう。


今回のサンプルコードは、こちらで公開しています。