進化するアーキテクチャーと新方式の設計: DSL の使用

ドメイン特化言語を使用して、イディオムのような領域特有のパターンを抽出する

シリーズ「進化するアーキテクチャーと新方式の設計」では、これまで主に技術的なパターンを対象とした新方式の設計に焦点を当ててきました。今回の記事では、ドメイン特化言語 (DSL) の技術を使用して、特定の領域でイディオムのようなパターンを抽出する方法を取り上げます。このシリーズの著者、Neal Ford が、DSL を使用した手法を具体的な例で説明し、この抽象化方式を使ってイディオムのようなパターンを抽出するメリットを明らかにします。

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.

Photo of Neal FordNeal Ford は世界的な IT コンサルティング企業である ThoughtWorks のソフトウェア・アーキテクトであり、Meme Wrangler でもあります。また彼は、アプリケーション、教育資料、雑誌記事、コースウェア、ビデオや DVD によるプレゼンテーションなどの設計と開発も行っています。さまざまな技術に関する本の著者、編集者でもあり、最新の著書は『プロダクティブ・プログラマ ― プログラマのための生産性向上術』です。彼は大規模なエンタープライズ・アプリケーションの設計や構築を専門にしています。また彼は世界各地で開催される開発者会議での講演者としても国際的に有名です。彼の Web サイトをご覧ください。



2010年 6月 08日

イディオムのようなパターンは、技術的なパターンであることも、ある領域に特有のパターンであることもあります。技術的なパターンが表すのは、アプリケーションの中で (あるいはアプリケーション・スイートのなかで) 検証、セキュリティー、トラザクション・データをどのように扱うかといった、よくある技術的なソフトウェア問題に対するソリューションです。これまでの記事では、イディオムのような技術的パターンをメタプログラミングなどの手法で抽出する方法に焦点を当ててきました。一方、領域特有のパターンは、よくあるビジネス問題をどのように抽象化するかということに関わってきます。技術的なパターンは、ほとんどあらゆる類のソフトウェアに現れてきますが、それに対し、領域特有のパターンは個々のビジネスが異なるのと同じように、領域ごとに異なります。けれども、領域特有のパターンを抽出する手法はさまざまに揃っています。このシリーズではこれらの手法について、今回から数回の記事にわたって説明します。

このシリーズについて

このシリーズの目的は、ソフトウェアのアーキテクチャーと設計という、繰り返し議論されていながら捉えどころのない概念を新しい視点で捉えなおすことです。Neal Ford が示す具体的な例をとおして、進化するアーキテクチャーと新方式の設計におけるアジャイル・プラクティスの確固たる基礎を学びます。アーキテクチャーと設計に関する重要な決定事項を最終的に必要な瞬間まで遅らせることで、アーキテクチャーと設計が必要以上に複雑にならないようにし、ソフトウェア・プロジェクトが強固なものでなくなる事態を避けることができます。

この記事では、特定の領域におけるパターンを抽出するための抽象化方式として、DSL の技術を使用する動機を提供します。DSLには、それぞれに独自パターンの用語体系をはじめ、豊富なオプションが揃っています。Martin Fowler は最新の著書で、さまざまな DSL 技術について深く掘り下げて調査しています (「参考文献」を参照)。次回の記事からは、彼が使用している多くのパターン名、そして彼の例と私の例を織り交ぜながら、具体的な手法を取り上げていきます。

DSL を使用する動機

イディオムのようなパターンを見つけるためだけに、わざわざ DSL を作成する理由は何でしょうか。「Leveraging reusable code, Part 2」で指摘したように、イディオムのようなパターンをコードの残りの部分と差別化するのに最適な方法の 1 つは、その見掛けを変えることです。この視覚的な違いが、通常の API を見ているのではないという直接的なヒントとなります。これと同じように、DSL を使用することによる目標の 1 つは、作成するコードをソース・コードというよりは、解決しようとしている問題のように見せることです。この目標を達成することができれば (あるいは、その目標に近づくだけでも)、大抵のソフトウェア・プロジェクトにおける重要なギャップを埋めることになります。それはつまり、開発者とビジネス利害関係者とのコミュニケーション・ギャップです。ユーザーがコードを読めるようにすれば、コードをユーザーが理解できる言葉に翻訳するという、エラーの元となりがちな作業が必要なくなるため、多大なメリットがもたらされます。ソフトウェアが果たすべき役割はわかっていても、技術的知識を持たない人々に理解できるようなコードにすることで、開発者とビジネス利害関係者との間で、より踏み込んだ会話ができるようになります。

この手法を使用する動機付けの手段として、DSL を主題にした Fowler の著書 (「参考文献」を参照) から 1 つの例を引用します。例えば、私はソフトウェアで制御された隠し部屋 (ジェームズ・ボンドを思い浮かべてください) を作る会社に勤めているとします。会社の顧客の 1 人、H 夫人が寝室に隠し部屋を設置して欲しいと依頼してきました。けれども私の会社では、ドットコムがもてはやされていた頃の名残である Java™ を使ったトースターでソフトウェアを実行しています。このトースターは低コストではあるものの、このトースター上でソフトウェアを実行するとなるとコストがかかります。そこで私に課せられたのが、基本的な隠し部屋のコードを作成し、そのコードをトースターに恒久的に組み込んだ上で、個々の顧客が必要とする隠し部屋を構成する方法を考え出すという任務です。お気付きの通り、これは最近のソフトウェアの世界ではよくある問題で、頻繁に変わることのない通例の振る舞いに、個々の状況に合わせて変更可能な構成を組み合わせるというものです。

H 夫人が要望している隠し部屋は、最初に寝室のドアを閉め、次にドレッサーの 2 番目の引き出しを開け、最後にベッドの脇にあるライトを点けると、隠し部屋のドアが開くという仕掛けです。この 3 つの動作は順序通りに行われなければなりません。順序が狂った場合には、最初から始める必要があります。この H 夫人の隠し部屋を制御するソフトウェアは、図 1 に示すステート・マシンとして想像できるはずです。

図 1. ステート・マシンとしての H 夫人の隠し部屋
ステート・マシンの図

ステート・マシンの基本となる API は単純なものです。私は、ステート・マシン内のイベントとコマンドの両方を処理する抽象イベント・クラスを作成しました (リスト 1 を参照)。

リスト 1. ステート・マシンの抽象イベント
public class AbstractEvent {
  private String name, code;

  public AbstractEvent(String name, String code) {
    this.name = name;
    this.code = code;
  }
  public String getCode() { return code;}
  public String getName() { return name;}

ステート・マシン内のステートは、States という名前の別の単純なクラスを使ってモデル化できます (リスト 2 を参照)。

リスト 2. ステート・マシン・クラスの開始部分
public class States {
  private State content;
  private List<TransitionBuilder> transitions = new ArrayList<TransitionBuilder>();
  private List<Commands> commands = new ArrayList<Commands>();

  public States(String name, StateMachineBuilder builder) {
    super(name, builder);
    content = new State(name);
  }

  State getState() {
    return content;
  }

  public States actions(Commands... identifiers) {
    builder.definingState(this);
    commands.addAll(Arrays.asList(identifiers));
    return this;
  }


  public TransitionBuilder transition(Events identifier) {
    builder.definingState(this);
    return new TransitionBuilder(this, identifier);
  }

  void addTransition(TransitionBuilder arg) {
    transitions.add(arg);
  }


  void produce() {
    for (Commands c : commands)
      content.addAction(c.getCommand());
    for (TransitionBuilder t : transitions)
      t.produce();
  }
}

リスト 1リスト 2 は、参考のために記載しているだけです。ここで解決すべき興味深い問題は、ステート・マシンの構成をどのように表現するかです。この表現は、隠し部屋を設置するというビジネスを対象としたイディオムのようなパターンとなります。リスト 3 に、Java をベースとした場合のステート・マシンの構成を記載します。

リスト 3. 構成方法の 1 つ: Java コード
Event doorClosed = new Event("doorClosed", "D1CL");
Event drawerOpened = new Event("drawerOpened", "D2OP");
Event lightOn = new Event("lightOn", "L1ON");
Event doorOpened = new Event("doorOpened", "D1OP");
Event panelClosed = new Event("panelClosed", "PNCL");

Command unlockPanelCmd = new Command("unlockPanel", "PNUL");
Command lockPanelCmd = new Command("lockPanel", "PNLK");
Command lockDoorCmd = new Command("lockDoor", "D1LK");
Command unlockDoorCmd = new Command("unlockDoor", "D1UL");

State idle = new State("idle");
State activeState = new State("active");
State waitingForLightState = new State("waitingForLight");
State waitingForDrawerState = new State("waitingForDrawer");
State unlockedPanelState = new State("unlockedPanel");

StateMachine machine = new StateMachine(idle);

idle.addTransition(doorClosed, activeState);
idle.addAction(unlockDoorCmd);
idle.addAction(lockPanelCmd);

activeState.addTransition(drawerOpened, waitingForLightState);
activeState.addTransition(lightOn, waitingForDrawerState);

waitingForLightState.addTransition(lightOn, unlockedPanelState);

waitingForDrawerState.addTransition(drawerOpened, unlockedPanelState);

unlockedPanelState.addAction(unlockPanelCmd);
unlockedPanelState.addAction(lockDoorCmd);
unlockedPanelState.addTransition(panelClosed, idle);

machine.addResetEvents(doorOpened);

リスト 3 は、ステート・マシンの構成に Java コードを使用した場合の問題をいくつか浮き彫りにしています。まず、このコードを読んでも、これがステート・マシンのコードであるとはすぐにわかりません。ほとんどの Java API のように、同じようなコードが続いているだけです。第 2 に、このコードは冗長で、何度も同じことが繰り返されています。例えば、ステート・マシンの各構成部分のステートとステート間の遷移を設定するごとに、同じ変数名を何度も繰り返し使用しています。このような重複は、コードをますます読みにくくするだけです。第 3 の問題として、このコードは、コードを再コンパイルすることなく隠し部屋を構成できるようにするという当初の目標を達成していません。

実際のところ、Java の世界でこのようなコードを目にすることは今ではめったにありません。現在、構成コードには XML が好んで使われるようになっています。XML で構成を作成するのは簡単です (リスト 4 を参照)。

リスト 4. XML でのステート・マシンの構成
<stateMachine start = "idle">
    <event name="doorClosed" code="D1CL"/>
    <event name="drawerOpened" code="D2OP"/>
    <event name="lightOn" code="L1ON"/>
    <event name="doorOpened" code="D1OP"/>
    <event name="panelClosed" code="PNCL"/>

    <command name="unlockPanel" code="PNUL"/>
    <command name="lockPanel" code="PNLK"/>
    <command name="lockDoor" code="D1LK"/>
    <command name="unlockDoor" code="D1UL"/>

  <state name="idle">
    <transition event="doorClosed" target="active"/>
    <action command="unlockDoor"/>
    <action command="lockPanel"/>
  </state>

  <state name="active">
    <transition event="drawerOpened" target="waitingForLight"/>
    <transition event="lightOn" target="waitingForDrawer"/>
  </state>

  <state name="waitingForLight">
    <transition event="lightOn" target="unlockedPanel"/>
  </state>

  <state name="waitingForDrawer">
    <transition event="drawerOpened" target="unlockedPanel"/>
  </state>

  <state name="unlockedPanel">
    <action command="unlockPanel"/>
    <action command="lockDoor"/>    
    <transition event="panelClosed" target="idle"/>
   </state>

  <resetEvent name = "doorOpened"/>
</stateMachine>

リスト 4 のコードには、Java で作成したコードに勝る利点がいくつかあります。まず 1 つは、遅延バインディングを使えることです。つまり、構成コードを変更してトースターにドロップすれば、XML パーサーで新しい構成を読み取ることができます。第 2 に、この特定の問題の場合、XML でのコードのほうが遥かに的確に表現できるようになります。それは、XML はコンテナーの概念を導入するためです。ステートが、それぞれの構成を子要素として組み込めば、Java バージョンに見られたあの煩わしい冗長性がなくなります。第 3 に、このコードは本質的に宣言型です。作成しているのが文だけで、ifwhile などの構文が必要ないのであれば、多くの場合、宣言型のコードのほうが大幅に読みやすくなります。

ここで一歩下がって、このコードに含まれる意味を理解してください。構成を外部化するというパターンは、最近の Java の世界ではあまりにも一般的なパターンとなっています。そのため、私たちは外部化された構成を個別のエンティティーとして考えることさえしなくなっていますが、これは実質的に、あらゆる Java フレームワークの特徴です。構成がイディオムのようなパターンであれば、周りのフレームワークの一般的な振る舞いとは切り離し、差別化してパターンを抽出する方法が必要となります。このように構成に XML を使用するという方法で、外部 DSL にコードを作成すれば (構文は XML で、文法はこの XML 文書に関連付けられたスキーマで定義)、フレームワークのコードを再コンパイルすることなく、コードに変更を加えられます。

XML にこだわらなくても、XML がもたらす利点を得ることはできます。例えば、リスト 5 に記載する構成コードを見てください。

リスト 5. カスタム文法でのステート・マシンの構成
events
  doorClosed  D1CL
  drawerOpened  D2OP
  lightOn     L1ON
  doorOpened  D1OP
  panelClosed PNCL
end

resetEvents
  doorOpened
end

commands
  unlockPanel PNUL
  lockPanel   PNLK
  lockDoor    D1LK
  unlockDoor  D1UL
end

state idle
  actions {unlockDoor lockPanel}
  doorClosed => active
end

state active
  drawerOpened => waitingForLight
  lightOn    => waitingForDrawer
end

state waitingForLight
  lightOn => unlockedPanel
end

state waitingForDrawer
  drawerOpened => unlockedPanel
end

state unlockedPanel
  actions {unlockPanel lockDoor}
  panelClosed => idle
end

このコードにも XML バージョンのコードと共通の利点が数多くあり、宣言型で、コンテナーを使用できて簡潔なコードになっています。さらに、XML バージョンと Java バージョンの両方に勝る利点もあります。それは、技術の実装には必要ではあるものの、読みやすさの妨げとなるノイズ文字 (<> など) が少なくなることです。

このバージョンの構成コードは、ANTLR を使用して作成されたカスタム外部 DSL です。ANTLR はオープンソースのツールで、このツールを使用することによって、独自のカスタム言語を簡単に作成できるようになります (「参考文献」を参照)。大学でのコンパイラー (昔ながらの Lex や YACC といったツールを含め) の講義を夢で見て、今でもうなされている人々にとっては嬉しいことに、これらのツールは遥かに改良されています。上記の例は、Fowler の著書から引用したものです。彼は、XML バージョンを作成する時間とカスタム言語バージョンを作成する時間は、ほとんど同じだったと言っています。

リスト 6 に、別の手段として今度は Ruby で作成したコードを記載します。

リスト 6. JRuby でのステート・マシンの構成
event :doorClosed, "D1CL"
event :drawerOpened, "D2OP"
event :lightOn, "L1ON"
event :doorOpened, "D1OP"
event :panelClosed, "PNCL"

command :unlockPanel, "PNUL"
command :lockPanel, "PNLK"
command :lockDoor, "D1LK"
command :unlockDoor, "D1UL"

resetEvents :doorOpened

state :idle do
  actions :unlockDoor, :lockPanel
  transitions :doorClosed => :active
end

state :active do
  transitions :drawerOpened => :waitingForLight,
              :lightOn => :waitingForDrawer
end

state :waitingForLight do
  transitions :lightOn => :unlockedPanel
end

state :waitingForDrawer do
  transitions :drawerOpened => :unlockedPanel
end

state :unlockedPanel do
  actions :unlockPanel, :lockDoor
  transitions :panelClosed => :idle
end

これは内部 DSL の好例です。内部 DSL はベースとなる言語の構文を使用することから、構文的には正規の Ruby コードとなります (このコードは Ruby で作成されているため、JRuby を使って実行することができます。つまり、トースターに必要なのは JRuby の JAR ファイルだけです)。

リスト 6 には、カスタム言語と同じ利点が多数あります。コンテナーの役割を果たす Ruby ブロックを多用していることに注目してください。こうすることによって、XML バージョンとカスタム言語バージョンと同じようなコンテナーの動作がもたらされます。また、このバージョンで使用しているノイズ文字は、カスタム言語よりもやや多いだけです。例えば Ruby の「:」接頭辞は、あるシンボルを示します。この例の場合、シンボルは基本的に ID として使用される不変のストリングです。

この類の DSL を Ruby で実装するのは至って簡単です (リスト 7 を参照)。

リスト 7. JRuby DSL のクラス定義 (一部抜粋)
class StateMachineBuilder
  attr_reader :machine, :events, :states, :commands

  def initialize
    @events = {}
    @states = {}
    @state_blocks = {}
    @commands = {}
  end

  def event name, code
    @events[name] = Event.new(name.to_s, code)
  end

  def state name, &block
    @states[name] = State.new(name.to_s)
    @state_blocks[name] = block
    @start_state ||= @states[name]
  end

  def command name, code
    @commands[name] = Command.new(name.to_s, code)
  end

Ruby の構文に関する規則は柔軟であることから、このタイプの DSL には Ruby が適しています。例えばイベントを宣言するときに、メソッド呼び出しの一部として括弧を含めるという規則は強制されません。このバージョンでは、独自の言語を作成する必要も、不等号括弧に煩わされることもありません。このことから、DSL 手法は Ruby で非常によく使われている理由がわかるはずです。


DSL の特徴

DSL は、イディオムのようなパターンを抽出するための優れた代替構文となります。Martin Fowler が定義しているように、DSL には 5 つの重要な特徴があります。

コンピューター・プログラミング言語であること

DSL としての言語は、コンピューター・プログラミング言語でなければなりません。このように限定されていなければ、どの言語も DSL と見なしてしまうという危険な道に進んでしまうことでしょう。DSL という用語をあまりにも広義な意味で定義すると、コンテキストに基づくすべての対話が DSL ということになってしまいます。例えば、私の同僚のなかに、クリケットの熱狂的ファンが数人います。彼らがクリケットについて話しているときに傍を通りかかると、彼らが英語で話しているにも関わらず、私には話の内容がさっぱり理解できません。それは私には、彼らが使っているクリケットに関する言葉を理解する上で必要な背景知識がないからです。したがって、クリケットや他のスポーツには、それぞれの用語における DSL があると主張する人がいるかもしれません。しかし、DSL の定義をこのように幅広くしてしまうと、DSL を有用な制約に絞り込むのが困難になります。そのため Fowler は、DSL をコンピューター・プログラミング言語に限るべきだと主張しているわけです。

言語的性質があること

Fowler が定義している 2 つ目の DSL の基準は、「言語的性質」があることです。これは DSL が、プログラマーでない人でも (漠然とでも) 理解できるようでなければならないことを意味します。DSL が持つ言語的性質は、さまざまな形で表されます。その多くについては、今後の記事で引き続きイディオムのようなパターンを抽出する手段としての DSL の使用方法を検討するなかで説明します。

領域に特化していること

正式な DSL としての言語は、特定の問題領域に焦点を絞り込んでいなければなりません。DSL を作成する際に伴う危険の 1 つは、DSL の範囲を広くすることです。DSL は抽象化メカニズムです。抽象化の範囲を広げすぎると、抽象化のメリットが損なわれてしまいます。

表現力が限られていること

表現力が限られていることも、DSL の特徴です。例えば、ループや決定などの制御構造体が含まれる DSL は非常に稀にしかありません。DSL の焦点は、それが記述しようとしている特定の領域だけに絞る必要があります。このことから、かなり多くの DSL は命令型ではなく宣言型となります。

チューリング完全でないこと

この特徴は、前の 2 つの基準が示唆していますが、ここで正式に説明しておきます。DSL は、チューリング完全であってはなりません (「参考文献」を参照)。実際、DSL が偶発的にチューリング完全になってしまうことは、DSL のアンチパターンとみなされています。これに該当する例が、昔ながらの UNIX®sendmail 構成ファイルが偶発的にチューリング完全になってしまう場合です。もし興味があって、時間を持て余しているようであれば、sendmail 構成ファイルにオペレーティング・システムを組み込んでみるのもいいかもしれません。

偶発的にチューリング完全になるのは、驚くほど容易なことです。お馴染みのインフラストラクチャー・ツールのなかには、このチューリング完全への遷移を偶発的に引き起こしているものがあります。それは例えば、XSLT です。言語が DSL であるかないは、その言語が使用されているコンテキストによって左右される場合があります。XSLT を使ってあるバージョンのテキストを別のバージョンのテキストに変換する場合には、XSLT を DSL として使用していることになります。一方、「ハノイの塔」問題を解くために XSLT を使っているとしたら、それは、XSLT をチューリング完全な言語として使用しているということです (そしてその場合には、新しい趣味を探してみるべきだと思います)。


まとめ

この記事では、イディオムのようなパターンを抽出するためのメカニズムとして DSL を使用する基礎を固めました。イディオムのようなパターンを抽出するには、DSL は極めて有効に機能します。DSL は通常の API とは区別しやすいこと、本質的に宣言型になる傾向があること、そしてプロジェクトに関する開発者と非開発者とのやりとりのフィードバック・ループを改善することが、その理由です。このシリーズでは今後、DSL を作成するための複数の手法を検討していきます。まずは次回から数回にわたり、コードの中で特定のパターンを検出したり、コードを設計したりする際に利用できる、DSL のいくつかの技術を具体的に説明します。

参考文献

学ぶために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=498925
ArticleTitle=進化するアーキテクチャーと新方式の設計: DSL の使用
publish-date=06082010