Martin FowlerのDSL Bookで紹介されている内部DSLの基本的なアーキテクチャについて、サンプルコードと合わせて考察する。
導入
内部DSL("Internal DSL")とは、ホスト言語を使って構築されるDSLを指します。外部DSL("External DSL")がXMLなどを用いてホスト言語とは関係なく構築できるのと対称的に、内部DSLではホスト言語の文法的制約の中で自然言語的なAPIが構築されます。具体例としては、Mockitoのコードを以下に挙げます(サイトに紹介されているサンプルを一部改変)。
LinkedList mockedList = mock(LinkedList.class); when(mockedList.get(0)).thenReturn("first"); when(mockedList.get(1)).thenThrow(new RuntimeException()); // "first"が出力される System.out.println(mockedList.get(0)); // 例外がスローされる System.out.println(mockedList.get(1));
ここで重要なのはwhen(<条件>).thenReturn(<戻り値>)のステートメントで、英語としてそのまま読むことができることがポイントです。なお、このようなAPIはメソッドが流れるように連鎖することから「流れるようなインタフェース("fluent interface")」とも呼ばれています。こうした内部DSLの実装における基本的なアーキテクチャを理解することがこの記事の目的です。
モデルレイヤと言語レイヤ
内部DSLのアーキテクチャにおいて最も重要なのは、ドメインモデル自体とそれを操作する言語を明確に区別することであると言えます。これについて、Fowlerはこう書いています。
I said earlier on that a DSL is a thin veneer over a model. This phrase should remind us that whenever you think about the benefits (or disadvantages) of a DSL it's important to separate the benefits provided by the model from the benefits of the DSL. I find it's a common mistake that people confuse the two.
前述した通り、DSLはモデル上に被せられた薄いベニヤ板です。このフレーズが常に思い起こさせてくれるのは、DSLのメリット(あるいはデメリット)について考える時は常に、モデルによって提供されるメリットとDSLによって提供されるメリットを区別することが重要だということです。どうやらこの2つを混同するという間違いはよくあるようです。
http://martinfowler.com/dslwip/UsingDsls.html#WhyUseADsl
これはモデルはモデル自体で完全に機能するものでなければならず、DSLは単にそのモデルを操作するAPIを自然言語的にラップする存在に過ぎないということを意味しています。この2つのレイヤが具体的な実装にどう反映されているかについて、サンプルを見ていきたいと思います。
サンプル:Security Codes
ここで紹介するサンプルは、上記サイト内に紹介されているC#のサンプルコードをGroovyを使って書き直したものです(オリジナルはこちら)。要件はあるセキュリティゾーンに対する従業員の入室可否ルールを実装するというものですが、実際のルールを見て頂く方が早いでしょう。
public class MyZone extends ZoneBuilder { void doBuild() { allow( gradeAtLeast(Grade.SENIOR_PROGRAMMER), department("MF")) refuse(department("Finance")) refuse(department("Audit")) allow( gradeAtLeast(Grade.DIRECTOR)) } }
上記のルールを日本語で表現すると以下のようになります。
- MF部のシニアプログラマ以上ならば入室可能。
- Finance部の従業員は立ち入り禁止
- Audit部の従業員は立ち入り禁止
- ディレクターはどこの部所であっても入室可能
モデルレイヤ
モデル層は大きく以下の3つの概念から構成されています。
- Zone:セキュリティゾーン
- AdmissionRule:許可/禁止を示すルール
- RuleElement:役職、部所といった具体的なルールを表す要素
ある従業員の入室可否の問い合わせは、ZoneクラスのwillAdmitメソッドを呼び出すことで行います。
myZone.willAdmit( new Employee( grade:Grade.SENIOR_PROGRAMMER, department:"K9"))
実際のルールの設定は、Zoneクラスに対してAdmissionRuleおよびRuleElementの実装クラスを設定することで行います。
myZone.addRule(new AllowRule( new AndExpr( new MinimumGradeExpr(Grade.SENIOR_PROGRAMMER), new DepartmentExpr("K9"))))
ここでAllowRuleが許可を表すAdmissionRuleの具象クラス、MinimumGradeExprとDepartmentExprがそれぞれ役職と部所を示すルール要素で、AndExprは複数のルール要素をアンド条件で結合させる特殊なルール要素です。ここで示したルールの設定方法は自然言語からはかけ離れたものではありますが、モデルとしては完全に機能することができます。
言語レイヤ
このサンプルで言語レイヤを提供しているのがZoneBuilderクラスです。特定の部所についてルールを作成したい場合には、ZoneBuilderクラスを継承し、doBuildメソッド内にDSLでルールを記述するという方法になっています。ZoneBuilderクラスの実装を示します。
public abstract class ZoneBuilder { (一部省略) void allow(RuleElement... rules) { zone.addRule(new AllowRule(new AndExpr(rules))) } void refuse(RuleElement rule) { zone.addRule(new RefusalRule(new AndExpr(rule))) } RuleElement gradeAtLeast(Grade grade) { new MinimumGradeExpr(grade) } RuleElement department(department) { new DepartmentExpr(department) } }
allow, refuseといったメソッドが、モデルのAPIに対する操作をラップしているのが分かると思います。
ルールを記述するクラスは常にZoneBuilderクラスを継承する必要があります。これは一見不自由な制約に見えますが、特定のZoneに対するルールの設定は基本的に1回限りで良く、その後はZoneに対する操作が中心になるため、このような場合であれば問題ないと言えるでしょう。
まとめ
「内部DSLを作る」と考えると、どうしても言語レイヤに視線が向いてしまいがちです。しかし、重要なのはあくまで適切なモデルを構築することであり、DSLはモデルを操作するレイヤにすぎません。サンプルのモデルに関しては、コンポジット構造によって実現されているRuleElementの具象クラス群(DDDの表現に従うと、「Specification」パターン)、特にAndExprクラスが一つのポイントになっていると言えるのではないでしょうか。
※今回ご紹介したGroovy版のソースコードは全てこちらで公開しています(EclipseはVersion: 3.4.2)。改善点なども含めて、フィードバックを頂ければ幸いです。(なお、Groovy版ではオリジナルにある期間指定機能は省略しています。)