この記事はJohan den Haan氏のブログ記事「DSL development: 7 recommendations for Domain Specific Language design based on Domain-Driven Design」を氏の許可を得て翻訳したものです。(原文公開日:2009年5月6日)
ドメイン固有言語(DSL)という用語は今日多く聞かれる。DSLとは与えられたドメインの要求に対処するために開発される言語である。ドメインは問題領域(例えば、保険、健康管理、運送)である場合もあれば、システム的な側面(例えば、データ、プレゼンテーション、ビジネスロジック、ワークフロー)である場合もある。これは制限された概念を用いて言語を作るという考え方であり、これらの概念は特定のドメインに焦点を絞ったものである。この考え方により、開発者の生産性とドメインエキスパートとのコミュニケーションを改善するような、より高次の言語が導き出される。多くの場合においては、ドメインエキスパートにDSLを用いてアプリケーションを開発してもらうことすら可能になるのだ。
この記事が提示する問いとは、どうやってドメイン固有言語を開発するのか?である。
まず最初にDSLのライフサイクルについて解説しよう。これは次のフェーズから成り立っている。すなわち、決定("decision")、分析("analysis")、設計("design")、実装("implementation")、配備("deployment")、保守("maintenance")である。その後で、これまである程度大掛かりなDSLを開発してきた経験に基づき、DSLを開発するための7つの提言を行う。
DSLのライフサイクル
Mernikらによれば[1] DSLのライフサイクルは5つの開発フェーズからなる。それが、決定、分析、設計、実装、配備である。Eelco Visser氏[2]が保守をDSLの6つ目のフェーズとして付け加えている。なお実際にはDSL開発は、シーケンシャルなプロセスではなく、これらのフェーズはイテレーティブに適用される。
それぞれのフェーズについてより詳細に見ていこう。
1. 決定("decision")
DSLの開発は、DSLを開発するのか、既存のものをつかうのか、汎用言語(GPL)を使うのかを決定するところから始まる。ドメインがきわめて新しいもので、それに関する知識がほとんど得られない場合、DSLの実装を始めることにあまり意味はない。対象領域における基本的な概念を見極めるため、最初は標準的なソフトウェア開発プロセスを適用し、ライブラリによってサポートされるコードベースを開発すべきである。
換言すれば、特定のドメインに関するアプリケーションを手書きで開発したことがなく、現行のコードベースが存在しないならば、DSLとそれに関連するコードジェネレータないし実行エンジンを実装することから始めるのは好ましくないということだ。
非実行性のDSLに関してはもちろん話が変わる。しかし、実行性のDSLのために現行コードに関する経験が必要なのと同様に、非実行性のDSLのためにはモデリングしているドメインについての深い理解が必要になる。
2. 分析("analysis")
分析フェーズにおいては、問題となっているドメインが識別され、それに関する知識が集められる。正式なドメイン分析のアウトプットは、以下のものからなるドメインモデルである。
- ドメインのスコープを定義するドメイン定義("a domain definition")
- ドメイン用語集 ("domain terminology")(ボキャブラリー, オントロジー),
- ドメイン概念の記述("descriptions of domain concepts")、それに
- ドメイン概念と相互依存性の共通性("commonality")と可変性("variability")を記述したフィーチャーモデル。
このフェーズにおいて集められた情報は実際のDSL開発に使用される。可変性が示しているのは、どの要素がDSLにおいて規定されるべきかであり、共通性は実行エンジンやドメインフレームワークを定義するのに用いられる。
例えば、もしあるドメインにおいて現行のコードベースを2、3分析するとすると、そのコードに含まれるある要素はコードベースごとに異なる場所と共通の場所の2つに分けることができる。スタティックな場所(共通性)は、実装手法に応じて、DSLを解釈する実行エンジンの一部になるか、自動生成されたコードによって利用されるドメインフレームワークの一部となる。異なる場所(可変性)はDSLによって規定されるべきである。つまりこれらはDSLの利用者が「設定」する必要がある場所なのだ。
Eelco Visser[2]は帰納的な手法を提案している。これは実装前に完全なDSLを設計するのではなく、徐々に抽象を導入し、それによって特定のドメインのためのソフトウェア開発における共通のプログラミングパターンを把握できるようにするというものだ。同様にVisser氏はイテレーションの中でDSLを開発することで、失敗するリスクを軽減できると論じている。最終的に機能的なDSLを作り出すような巨大プロジェクトとは異なり、イテレーティブなプロセスは早い段階でサブドメインのための便利なDSLを作り出すのだ。
この記事の後半で、私は自らの経験に基づきDSLの分析および設計フェーズのための提言を7つ付け加える。
3. 設計("design")
DSLを設計するためのアプローチは2つの直交する次元によって特徴づけられる。すなわち、DSLと既存の言語との関係と、設計を記述することが持つ形式的な性質である[1]。DSLはスクラッチから設計することもできるが、既存の言語を元にした方が簡単である。
Mernikら[1]は、既存の言語に基づいた3つの異なる設計パターンを識別している。
- 抱き合わせ("piggyback"): 既存の言語が一部に利用されている。
- 特殊化("specialization"):既存の言語が制限されている。
- 拡張("extension"):既存の言語が拡張されている。
既存の言語との関係に加え、形式的な性質にも幅がある。
どの手法を取るのかを決定するのは重要だが、次の教訓を心に留めておくことがより重要だろう[3]:
教訓その2: あなたはこれまでにプログラミング言語を設計したことがない
ほとんどのDSL設計者は言語設計のバックグラウンドを持っている。そこでの直交性や形式の経済学についての原則には敬意を評すべきだが、それらが必ずしもDSLの設計にうまく当てはまる訳ではない。既に存在するジャーゴンやドメインの記法に合わせる際には、言語を飾り立てたり過度に作り込んだりしないように注意しなければならない。
教訓その2の帰結:必要なものだけを設計せよ。過剰な設計("over-design")をしがちな自分の傾向を認識することを学ぶべきだ。
4. 実装("implementation")
実行性のDSLにおいては、最も適切な実装手法が選択されるべきだ。Mernikら[1]は7つの異なる実装パターンを識別しており、それぞれが異なる性質を持っている。
- インタープリタ:DSLの構造は通常のフェッチ-デコード-実行サイクルを用いて認識され、解釈される。このパターンを用いると、変換は一切行われない。モデルが直接実行可能なのだ。
- コンパイラ/アプリケーションジェネレータ:DSLの構造は基礎となる言語の構造とライブラリ呼び出しに変換される。この実装パターンを指す時には、ほとんどコードジェネレーションが話題になる。
- プリプロセッサ:DSLの構造は既存の言語(基礎となる言語)の構造へと変換される。スタティックな分析は基礎となる言語のプロセッサによって実行されるものに限られる。
- 組み込み:DSLの構造は、新しい抽象データ型と操作を定義することにより、既存の汎用言語(ホスト言語)の中に組み込まれる。これについての初歩的な例はアプリケーションのライブラリである。このタイプのDSLは主に内部DSLと呼ばれる。
- 拡張可能なコンパイラ/インタープリタ:汎用言語のコンパイラ/インタープリタがドメイン固有の最適化ルールやドメイン固有のコードジェネレーションを用いて拡張される。通常、インタープリタの拡張は比較的簡単なのが普通であるのに対して、コンパイラの拡張はそれを想定して設計されていない限り困難である。
- 商用既製品:既存のツールや記法は特定のドメインに適用される。DSLやエディタ、DSLの実装手法を自分で定義する必要がなく、ただモデル駆動ソフトウェアファクトリーを利用すればよい。例えば、サービス指向ビジネスアプリケーションのドメインを対象とした、Mendix モデル駆動エンタープライズアプリケーションプラットフォームを利用することができる。
- ハイブリッド:上記のアプローチの組み合わせ。
手法が異なればDSL開発に費やされる全体的な労力が大きく変わってくるので、特定の手法を選択することが非常に重要である。
5. 配備("deployment")
配備フェーズにおいては、DSLとそれを用いて構築されたアプリケーションが利用される。開発者やドメインエキスパートはモデルの仕様を定めるのにDSLを使う。これらのモデルは前節で紹介した実装パターンの1つを用いて実装される(例えば、モデルがエンジンによって解釈される)。このような実装により、エンドユーザによって利用される実際に機能するソフトウェアができあがる。
6. 保守("maintenance")
DSLで表現されたモデルに順応することで、ドメインエキスパート自身がソフトウェアを理解し、妥当性を評価し、修正することができるが、修正する方が作るより容易であるし、そのインパクトも理解しやすい。しかし、ソフトウェアにおけるより本質的な変更により、DSLの実装が変わることもあるかもしれない。ソフトウェアにおける他の要素と同じく、DSLも時間を経て進化するのだ。したがって、DSL移行戦略を持つことが極めて重要である。
移行戦略に加え、DSLを保守する際のリスクを軽減するための提言が2つある。
- 優れたDSLツールを使う。少なくとも言語定義からエディタやジェネレータもしくはインタプリタを生成できるもの。
- 異なるDSLにおいて定義されたモデルを、異なる疎結合のエンジンによって実装する。このやり方によって、特定のDSLやそのコンパイラの保守はシステム全体に影響を与えることなく、エンジンは異なる技術を活用できる。ビジネスアプリケーションの場合、これに必要とされるのが、サービス指向ビジネスアプリケーションを推進するアーキテクチャすなわち、複数の疎結合なサービスから構成されるアプリケーションである。
ドメイン駆動設計に基づく、DSL開発のための7つの提言
今やDSLのライフサイクルが明確になったので、分析と設計フェーズにおける経験のいくつかを共有したい。その他のフェーズについては別の記事で書くことにする。
DSL設計の詳細に踏み込む前に、これらの経験のコンテキストについて理解を試みよう。まず、これらは複合的に接続されたDSL("multiple connected DSLs")を生成することに注力している。つまり相互に参照する別々のDSLによって表現されたモデルを生成できるのだ。例えば、フォームモデルにおいては、データモデルに由来するエレメントに参照できる。より限定すれば、ここで語っているのはサービス指向ビジネスアプリケーションが持つ全てのシステム的な側面をカバーする一連のDSLなのだ。
ここで語られているDSLにおけるもう一つの重要なポイントは、これらがどれもプログラマではないドメインエキスパートを対象にしたものであるということだ。ほとんどのケースにおいて、これが意味しているのはドメインエキスパートがこういったDSLで表現されたモデルを作れるということであり、少なくとも読むことはできるということである。もちろんこのためには、常に柔軟性と複雑性の間でバランスを取る必要がある。
Domain-Driven Design[4]の概念に影響を受けた私の経験に基づき、DSL開発の為に以下の7つの提言を行う。
1. ドメインの知識をメタモデルにおいてとらえよ
DSLのためのモデルについて語ろうとすれば、メタモデルという用語に行き着くだろう。多くの人にとって、この単語は読むのを止めるのに十分なほど恐ろしいものだ。しかし、これは単に言語の抽象的構造のモデルにすぎない。換言すれば、メタモデルのモデルは言語の概念とその関係なのだ。注文入力ポータルのようなソフトウェアを構築する際に「注文」「製品」「顧客」といった概念をモデリングするのと同様である。
メタモデルはDSLを構築する上で本質的なものだ。これはDSLが対象とするドメインの知識をとらえるのである。このモデルが反映しているのは、DSLを開発しているチームがどのようにドメインの知識を構成しているか、最も重要な要素が何であると考えているかである。モデルと実装との結びつきにより、初期バージョンのDSLによって得られた経験はモデリングプロセスにおいてフィードバックとして利用される。
2. ユビキタス言語を用いてコミュニケーションせよ
メタモデルはコミュニケーションという目的のためにも重要である。DSLを設計する際には、言語のユーザ(ドメインエキスパート)と開発者との間に多くのコミュニケーションが必要になる。メタモデルはチームメンバ全員によって用いられる言語の根本となる。モデルが実装と結びつけられるので、開発者はDSLについて語る時にこの言語を用いることができる。つまり、ドメインエキスパートと翻訳することなくコミュニケーションすることができるのだ。
DSLについて語る際には、モデルと戯れなければならない。シナリオについてモデルの観点で語ることができなければ、それができるようになるまでモデルを適合させなければならない。ドメインエキスパートがモデルを理解できなければ、そのモデルには何か問題があるのだ。ドメインエキスパートはドメインに関する理解を伝える上で、ぎこちなかったり不十分であったりするような用語や構造に対して抵抗しなければならない。開発者はあいまいさや不整合に注意しなければならない。こういったものは設計をつまづかせることになる。
3. メタモデルに実装を駆動させよ
言語定義が単なるメタモデル(抽象シンタックス)に留まらないことを忘れないように。言語定義は具象シンタックスとセマンティクスも含んでいるのだ。DSLを設計、実装する際、具象シンタックスはソリューションワークベンチにおいてとらえられる。これはモデルを記述する際にテキストや図による具象シンタックスによってDSLを用いることができるような環境である。言語のセマンティクスは変換ルールやモデルインタプリタにおいてとらえられる(これは用いられている実装パターンによる。上記参照)。
重要なのは、ソリューションワークベンチの実装やインタプリタをメタモデルが駆動させることである。つまり、メタモデルがDSLの実装を駆動させるべきなのだ。実装がメタモデルにマッピングされなければ、そのメタモデルにはほとんど価値がない。同時にメタモデルと実装の間にある複雑なマッピングは理解が難しく、実践的に見ても設計の変更に合わせた保守が困難である。メタモデルと実装が著しく乖離していたら、それぞれの活動において得られる洞察が互いにフィードバックを与えることがない。
したがって、メタモデルを設計する時には、実装をまさに文字通りの仕方で反映するようなやり方にするべきだ。しかし同時に、ある特定のメタモデルはユビキタス言語をサポートするという目的に従わなければならない。実装はメタモデルの表現にならなければならず、コードに対する変更はメタモデルに対する変更であり、逆もまたしかりである。DSLの実装とメタモデルとをこのようなやり方で結びつけるためには、通常、メタモデルからのDSL実装のうち大部分を生成してくれるDSLツールが必要となる。図1は、ソリューションワークベンチとインタプリタの一部がメタモデルから生成されるシナリオを示している。
4. ドメインを分離せよ
前述した通り、DSLは時間をかけて進化する。モデルと実装を結びつけることが重要であることは既に見た通りで、モデルが実装を駆動させるべきなのだ。しかしそのためにはドメインを分離する必要がある。メタモデルを表象するドメインのコードが、コード全体の中に散らばっていたら、変更を加えるのが非常に難しくなる。モデリング環境におけるGUIやインタプリタの基盤における変更によって、実はドメインコードが変更されることになり得るのだ。
原則として、「普通の」ソフトウェアについて言えることは、DSL実装においても言える。コードを複数のレイヤに分割し、ドメインモデルに関連するすべてのコードを1つのレイヤに集約せよ。そしてそのレイヤはGUIや基盤のコードから分離されていなければならない。ドメインオブジェクトは、それ自身を表示ないし保存したり、アプリケーションのタスクを管理したりといった責務から解放されていなければならない。ドメインオブジェクトはドメインモデルを表現することに集中するべきなのだ。
モデルが実装を駆動するべきだということは既に述べた。そしてこれは可能な限り文字通りの意味で行うことを意図している。このことはドメインを分離してはじめて可能になるのだ!ジェネレーションギャップパターンを用いることで、その他のコードから分離しつつ全てのドメインコードを生成することができる。
したがって、ドメインモデルを分離し、それによってモデルがドメインを表現すると同時にそのドメインの変更を追跡できるくらい、豊かに進化できるようにしよう。
5. 継続的にリファクタリングせよ
同様に、リファクタリングも常に行うべきだ。知識の咀嚼("knowledge crunching")の間も、メタモデルを使ったコミュニケーションをしている時も、DSLの実装にかかり切りになっている時もメタモデルからコードを生成している時も、リファクタリングをするべきだ。Eric Evans[4]によれば、特にリファクタリングするべきなのは以下の場合である。
- 設計がドメインに対してチームが持つ現在の理解を表現していない。
- 重要な概念が設計の中に隠れている(そしてそれを明確にするやり方が分かっている)。
- 設計の中のある重要な部分をより柔軟にするチャンスがある。
このような手法を採る場合に、ドメインエキスパートを含むチームメンバ全員が深く関わらなければならないということは、言わずもがなである。
6. メタモデルの整合性を保て
複雑なドメインをドメイン固有モデルを用いて抽象化するためには、1つのDSLでは足りない。複雑なプロジェクトにおいては、複合的DSLが別々のコンテキストを扱うために通常必要となる。換言すれば、異なるDSLによって定義される複合的ドメイン固有言語(DMS)が、複雑なシステムを正確に抽象化するには必要なのだ。
巨大なドメインのためのメタモデルを統一すること(メタモデルが設計しているDSLのコンセプトを記述していることを忘れないように)は実現も難しく、コストに見合うものでもない。最大の原因は、単一のメタモデル(したがって単一の言語)によって皆を満足させるよう試みることは、言語の使用を難しくしてしまうような複雑な選択肢へとつながっていくということだ。そしてこれこそが、DSLを設計する理由ではないか。別々のドメインエキスパートはその人の視点から見るシステムを定義するために独自のドメイン固有言語を必要とする。
したがって、複合的なドメイン固有言語が求められるのであり、その結果、複合的なメタモデルも必要になる。しかし、異なるメタモデル間の境界と関係は意識的に可視化する必要がある。複数のDSLを開発する上でいくつかの提言をする。
- 各メタモデルのコンテキストを明確に定義すること。つまりDSLが設計されるドメイン(例えばシステム的側面)を定義すること。
- メタモデルの実装を継続的に結合("integrate")し、他のメタモデルのためのインタフェースを自動化されたテストの一部にすること。
- メタモデル間の連結ポイントをモデリングし、そのモデルをユビキタス言語において使用すること。これらの連結ポイントは、異なるDSLにおいて表現されたモデルが、どのように相互参照できるかを定義している。例えば、GUIの要素はデータモデルの要素を参照できる。
- 参照解決戦略について考えること。インタープリタ/エンジンを用いてDSLによって表現されるモデルを実行しているならば、遅延ビルドを利用することができる(つまり、ソフト参照を用いて、実行時に解決する)。この戦略のメリットは、柔軟性と適応力だ。コード生成と一緒に利用される手法は事前バインディング("early-binding")であり、参照は生成されたコードにおいて明示的に反映される。この戦略に従う理由は性能であると思われる。
7. 人間指向の手法を用いよ
DSL実装プロセスを実行すること、特にこれまでに示してきたやり方で行うことは簡単ではない。これには力のある開発チームとドメインエキスパートが必要になる。最後の、そして最も重要な提言は、DSL開発においては人間を第一に考える手法を用いるべきということだ。DSL開発は高度にクリエイティブで専門的な仕事だ。開発者は技術的な決定をしなければならず、技術的な仕事をどう行うかを決定するのに最適な人間でもある。ドメインエキスパートはドメインに住んでいるので、言語の概念が適用できるのかを決定するのに最適だ。
私はこれまで述べてきた6つのポイントを反映した形で仕事をすることを強く勧めるが、チームはプロセスにおいて意思決定をしなければならない。プロセスを受け入れることにはコミットメントが必要であるが、それ自体チーム全員が積極的に参加する必要があるのだ。
DSL開発のためのキーポイント
- ドメインの知識をメタモデルにおいてとらえよ
- ユビキタス言語を用いてコミュニケーションせよ
- メタモデルに実装を駆動させよ
- ドメインを分離せよ
- 継続的にリファクタリングせよ
- メタモデルの整合性を保て
- 人間指向の手法を用いよ
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
[1] Marjan Mernik, Jan Heering, and Anthony M. Sloane. When and how to develop domain-specific languages. ACM Comput. Surv., 37(4):316-344, 2005.
[2] Eelco Visser. WebDSL: A case study in domain-specic language engineering. In R. Lammel, J. Saraiva, and J. Visser, editors, Generative and Transformational Techniques in Software Engineering (GTTSE 2007), Lecture Notes in Computer Science. Springer, 2008.
[3] Wile, D. S. 2004. Lessons learned from real DSL experiments. Sci. Comput. Program. 51, 265-290.
[4] Eric Evans, Domain Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2004.
Photos by Gail S and Hélio Costa