目次


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

JRuby で DSL を作成する

Java コード・ベースの JRuby を使用するという手段によって Ruby の表現力を利用する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 進化するアーキテクチャーと新方式の設計

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:進化するアーキテクチャーと新方式の設計

このシリーズの続きに乞うご期待。

数回前の記事から、ドメイン特化言語 (DSL) を使用してイディオムのようなドメイン・パターン (新しく持ちあがってきたビジネス問題に対するソリューション) を抽出するという話題を取り上げてきました。DSL は簡潔で (ほとんどのノイズが構文から取り除かれます)、開発者でなくても読んで理解できることから、イディオムのようなドメイン・パターンを抽出するには申し分なく役立つと同時に、API 中心のコードからは際立つ存在になっています。前回の記事では Groovy の機能を利用して DSL を作成する方法を紹介しました。今回はこの DSL を使用してイディオムのようなパターンを抽出する話題の締めくくりとして、JRuby を活用することによって、Ruby で一段と洗練された DSL を作成する方法を説明します。

Ruby は現在、内部 DSL を作成する際に最もよく使われている言語です。Ruby で開発する際のインフラストラクチャーとして皆さんが思い付くほとんどは、DSL をベースとしています (Ruby on Rails、RSpec、Cucumber、Rake、その他多数 (「参考文献」を参照))。それは、Ruby は内部 DSL をホストするのに適しているからです。また、最近流行の手法となっているビヘイビア駆動開発 (BDD) でも、その人気を得るためには強力な DSL の基盤が欠かせませでした。DSL の熱烈な支持者たちの間でなぜ Ruby がこれほどの人気を集めているのかは、この記事を読めば納得していただけると思います。

Ruby のオープン・クラス

オープン・クラスを使用して組み込みクラスに新しいメソッドを追加するという手法は、DSL の表現力を強化するために一般的に使用されています。前回の記事では、Groovy がサポートする 2 種類のオープン・クラスの構文を紹介しました。Ruby にも同じメカニズムがありますが、使用する構文は 1 つだけです。例として、料理のレシピ用 DSL を作成するとします。この場合、DSL を作成するには数量を取り込む手段が必要です。そこで、DSL の一部を示したリスト 1 について考えてみましょう。

リスト 1. Ruby ベースのレシピ用 DSL のターゲット構文
recipe = Recipe.new "Spicy bread"
recipe.add 200.grams.of "flour"
recipe.add 1.lb.of "nutmeg"

上記のコードを実行可能なコードにするためには、gram メソッドと lb メソッドを数値に追加しなければなりません。それには、リスト 2 のように Numeric クラスをオープンします。

リスト 2. Ruby でのオープン・クラスの定義
class Numeric
  def gram
    self
  end
  alias_method :grams, :gram

  def pound
    self * 453.59237
  end
  alias_method :pounds, :pound
  alias_method :lb, :pound
  alias_method :lbs, :pound

Ruby では、クラス名は大文字で始めなければなりません。これは Ruby の定数にも当てはまるルールです。つまり、すべてのクラス名は定数でもあることを意味します。Ruby はクラス定義を見つけると、そのクラスがすでにクラス・パスにロードされているかどうかを調べます。クラス名は定数であることから、ある特定の名前のクラスは 1 つしかありません。そこで、クラスがすでにロードされている場合には、クラス定義でそのクラスを再オープンして、変更を加えられるようにするというわけです。リスト 2 では、Numeric クラス (固定値と浮動小数点数の両方を処理するクラス) を再オープンして、gram および pound メソッドを追加しています。Groovy とは異なり、Ruby には、引数を受け付けないメソッドについては空の括弧を使って呼び出さなければならないというルールはありません。つまり、Ruby ではプロパティーとメソッドを区別する必要はないということです。Ruby には、もう 1 つの重宝な DSL メカニズムがあります。それは、alias_method クラス・メソッドです。皆さんは、DSL をできるだけ流ちょうなものにするために、単数形を複数形に変えるなどといった場合にも対処する必要があると考えるかもしれません (単数形を複数形に変えるための手の込んだコードを調べたいのであれば、Ruby on Rails でモデル・クラス名を複数形にする場合のコードを見てください)。追加しているグラム数が 1 グラムを超えていることは明らかなのに、DSL で recipe.add 2.gram.of("flour") などといった文法的に体裁の悪い文を作成するようなことを、私はしたくありません。Ruby の alias_method メカニズムを使えば、簡単に代替名を作成して、メソッドを読み易くすることができます。その理由から、リスト 2 では gram に対して複数形のメソッドを追加し、pound に対しても省略形、複数形のバージョン両方を使えるように追加しています。

流れるようなインターフェースの作成

DSL を使用してイディオムのようなパターンを抽出する目的の 1 つは、プログラミング言語に置き換えた抽象概念から、ノイズとなる構文を排除することです。そこでリスト 3 に記載する、ノイズの多いレシピ用 DSL のコード・スニペットについて考えてみましょう。

リスト 3. ノイズの多い、レシピの定義
recipe = Recipe.new "Spicy bread"
recipe.add 200.grams.of "flour"
recipe.add 1.lb.of "nutmeg"
recipe.directions << "mix ingredients"
recipe.directions << "cook for 30 minutes at 250 degrees"

レシピの材料を加えることと調理の指示を示すリスト 3 の構文は、かなり簡潔なものの、ここではホスト変数の名前 (recipe) が何度も繰り返されています。リスト 4 に、この構文を簡潔にしたバージョンを記載します。

リスト 4. コンテキストを適用した、レシピの定義
alternate_recipe = Recipe.new("Milky Gravy")
alternate_recipe.consists_of {
  add 1.lb.of "flour"
  add 200.grams.of "milk"
  add 1.gram.of "nutmeg"
  
  steps(
    "mix ingredients",
    "cook for some amount of time"
  )
}

上記の構文では、consists_of メソッドを流れるようなインターフェースに追加して、コンテナーを使用できるようにしています (波括弧 ({}) で区切られたクロージャー・ブロックを使用して Ruby にコンテナーを組み込むことにより、ノイズとなるホスト・オブジェクトの繰り返しをなくしています)。このメソッドを実装するのは、Ruby ではわけのないことです (リスト 5 を参照)。

リスト 5. consists_of メソッドを組み込んだ Recipe クラスの定義
class Recipe
  attr_reader :ingredients
  attr_accessor :name
  attr_accessor :directions

  def initialize(name="")
    @ingredients = []
    @directions = []
    @name = name
  end

  def add ingredient
    @ingredients << ingredient
    return self
  end
  
  def steps *direction_list
    @directions = direction_list.collect
  end
  
def consists_of &block
    instance_eval &block
  end
end

consists_of メソッドは、コード・ブロックを引数に取ります (これに該当するのは、パラメーター名の前にアンパサンドが付いた構文です。アンパサンドによって、このパラメーターはコード・ブロックのホルダーとして識別されます)。このメソッドは、instance_eval メソッドを使用してコード・ブロックを実行します。Ruby の組み込みメソッドの 1 つである instance_eval メソッドは、渡されたコードを実行するために、ホスト・オブジェクトの定義を変更します。別の言葉で説明すると、instance_eval を使ってコードを実行するということは、self (Ruby では、これが Java 言語の this に相当します) が、instance_eval を呼び出した変数になるということです。したがって、add メソッドと steps メソッドを recipe.instance_eval を使って呼び出す場合、recipe ホスト・オブジェクトを使わずに、この 2 つのメソッドを呼び出すことができるということです。そしてこれが、consists_of メソッドが実行している内容です。

このシリーズを毎回読んでくださっている読者は、この概念から、「Leveraging reusable code, Part 2」の記事で取り上げた Java 構文 (リスト 6 を参照) を思い出すことでしょう。

リスト 6. インスタンス・イニシャライザーを使用した、Java コード内の流れるようなコード・ブロック
MarketingDescription desc = new MarketingDescriptionImpl() {{
    setType("Box");
    setSubType("Insulated");
    setAttribute("length", "50.5");
    setAttribute("ladder", "yes");
    setAttribute("lining type", "cork");
}};

一見すると、上記の構文は Ruby バージョンと同じように見えますが、Java バージョンにはいくつかの重大な制約があります。第 1 に、これが Java 言語では特異な構文であることです (ほとんどの開発者は、日常のコーディング作業でインスタンス・イニシャライザーを目にすることはありません)。第 2 に、この構文は匿名内部クラス (Java で唯一の、コード・ブロックのようなメカニズム) を使用することから、外部スコープからの変数を final として宣言する必要があることです。これは、コード・ブロック内部で実行できる処理に重大な制限を課すことになります。Ruby では、instance_eval メソッドは標準の (特異ではない) 言語機能です。つまり、一般的に使用されています。

ポリッシング

多くの DSL (特に、開発者でない人を対象とした DSL) で共通して使われている手法の 1 つに、話し言葉を使用するという手法があります。ベースにしているコンピューター言語に十分な柔軟性があれば、話し言葉を対象にしたコンピューターの構文を作成することは可能です。これまでの作業で作成したレシピ DSL を例に取ると、単純なデータ構造 (材料のリストと調理手順のリスト) を保持するためだけに完全な DSL を作成するのでは、多少やり過ぎの感があります。それならば、単純にこの情報を標準データ構造に持たせるというのはどうでしょう。DSL に処理をエンコードすることによって、データ構造にデータを取り込むだけでなく、さらなるアクション (有益な副次効果をもたらすようなアクション) も可能になります。例えば、DSL に定義する材料ごとに栄養成分の情報を取得すれば、完成した料理の栄養価を提供することができます。そのための NutritionProfile クラスは、単純なデータ・ホルダーです (リスト 7 を参照)。

リスト 7. レシピの栄養成分レコード
class NutritionProfile
  attr_accessor :name, :protein, :lipid, :sugars, :calcium, :sodium

  def initialize(name, protein=0, lipid=0, sugars=0, calcium=0, sodium=0)
    @name = name
    @protein, @lipid, @sugars =  protein, lipid, sugars
    @calcium, @sodium = calcium, sodium
  end
  
  def self.create_from_hash(name, h)
    new(name, h['protein'], h['lipid'], h['sugars'], h['calcium'], h['sodium'])
  end

  def to_s()
    "\tProtein: " +   @protein.to_s       +
    "\n\tLipid: " +   @lipid.to_s         +
    "\n\tSugars: " +  @sugars.to_s        +
    "\n\tCalcium: " + @calcium.to_s       +
    "\n\tSodium: " +  @sodium.to_s
  end

end

上記のような栄養成分レコードをデータベースに取り込むため、各行に 1 つのレコードを記載するテキスト・ファイルを作成します。

ingredient "flour" has protein=11.5, lipid=1.45, sugars=1.12, calcium=20, and sodium=0

ご想像のとおり、この定義ファイルの各行は Ruby ベースの DSL です。上記の構文を単なるテキストの行と考えるのではなく、コンピューター言語の観点からはどのように「見えるか」を考えてください (図 1 を参照)。

メソッド呼び出しとしての材料のテキスト定義
メソッド呼び出しとしての材料のテキスト定義
メソッド呼び出しとしての材料のテキスト定義

各行は、メソッド名である ingredient で始まります。メソッドの最初の引数は材料の名前です。has という語はバブル・ワードと呼ばれます。つまり、DSL をより読み易くする一方、最終的な定義には関係しない語のことです。行の残りの部分は、カンマで区切られた名前と値のペアで構成されます。このテキストは正規の Ruby 構文ではありませんが、どのようにして Ruby に変換すればよいのでしょうか。Ruby に変換するための作業はポリッシングと呼ばれます。ポリッシングでは、ほとんど正規の構文に近いコードを取り、それを実際の構文に仕上げます。この DSL の場合、ポリッシング作業は NutritionProfileDefinition クラスによって行われます (リスト 8 を参照)。

リスト 8. NutritionProfileDefinition クラス
class NutritionProfileDefinition
  
  def polish_text(definition_line)
    polished_text = definition_line.clone
    polished_text.gsub!(/=/, '=>')
    polished_text.sub!(/and /, '')
    polished_text.sub!(/has /, ',')
    polished_text
  end

  def process_definition(definition)
    instance_eval polish_text(definition)
  end

  def ingredient(name, ingredients)
    NutritionProfile.create_from_hash name, ingredients
  end    
   
end

このクラスのエントリー・ポイントは、process_definition メソッドです (リスト 9 を参照)。

リスト 9. process_definition メソッド
def process_definition(definition)
  instance_eval polish_text(definition)
end

上記のメソッドは instance_eval を使って polish_text を呼び出し、polish_text の実行コンテキストを NutritionProfileDefinition インスタンスに切り替えます。polish_text メソッド (リスト 10 を参照) は、必要な置換と変換を行って、正規の構文に近いコードを正規の構文のコードに変換します。

リスト 10. polish_text メソッド
def polish_text(definition_line)
  polished_text = definition_line.clone
  polished_text.gsub!(/=/, '=>')
  polished_text.sub!(/and /, '')
  polished_text.sub!(/has /, ',')
  polished_text
end

polish_text メソッドは、定義構文を Ruby 構文に変換する単純なストリング置換からなります。これらのストリング置換では、等号をハッシュ ID (=>) に変換し、余計な and のインスタンスを排除し、has をカンマに変換します。このようにポリッシングされたコード行が instance_eval に渡されて、NutritionProfileDefinition クラスの ingredient メソッドを介して実行されます。

このコードは Java 言語でも作成することができますが、Java の構文上の制約事項によってノイズが追加されることになり、流れるようなインターフェースのメリットは損なわれてしまいます。したがって、作業する価値があるかどうかは疑問です。Ruby では十分なシンタックス・シュガーを提供して、抽象化を DSL としてキャストすることを可能に (そして価値のあることに) しています。

メソッド・ミッシング

前のセクションで説明した例とは異なり、次に説明する例は、複雑な構文を使ったとしても Java コードでは実現することはできません。一般に DSL をサポートする言語には、メソッド・ミッシングという便利なメカニズムがあります。このメカニズムは、Ruby に存在しないメソッドを呼び出しても、即時に例外が発生することがないように、存在しないメソッドの呼び出しを処理する method_missing メソッドをクラスに追加するというものです。このメカニズムは、内部データ構造を組み立てる DSL で多用されています。ここで、Ruby の XMLBuilder (「参考文献」を参照) から抜粋した例について考えてみましょう。

リスト 11. Ruby の XMLBuilder を使用する
xml = Builder::XmlMarkup.new(:indent => 2)
xml.person {
  xml.name("Neo")
  xml.catch_phrase("Whoa")
}
puts xml.target!

このコードは、DSL に示された構造で XML 文書を出力します。Builder の魔法を可能にしているのは、method_missing です。xml 変数のメソッドを呼び出すと、そのメソッドがまだ存在していない場合は method_missing に分類され、それによって対応する XML が作成されます。そのため、Builder ライブラリーのコードはかなり小さくなっています。つまりこの仕組みの大部分で、ベースとなっている Ruby 言語の機能を利用しているのです。ただし、この手法には 1 つの問題があります。その問題は、リスト 12 のコードに説明されています。

リスト 12. メソッド・ミッシングと組み込みメソッドの衝突
xml = Builder::XmlMarkup.new(:indent => 2)
xml.person {
  xml.name("Neo")
  xml.catch_phrase("Whoa")
  xml.class("pod-born")
}
puts xml.target!

method_missing だけに依存する場合、リスト 12 のコードは機能しません。class メソッドはすでに、(Java 言語の場合と同じく) すべてのクラスの基底クラスである Object の一部として Ruby で定義されていて、method_missing は当然、既存のメソッドでは機能しないためです。このことは、この手法の失敗を決定付けるかのように思えますが、Jim Weirich (Builder の作成者) が考え出した素晴らしいソリューションがあります。それは、彼が作成した BlankSlate です。BlankSlateObject を継承するクラスですが、通常 Object にあるメソッドのすべてをプログラムによって削除します。このようにして既存のメソッドが削除されれば、副次作用に悩まされることなく method_missing インフラストラクチャーを使用できるようになります。

この強力で有用な BlankSlate メカニズムは、次の Ruby のメジャー・バージョンに組み込まれることになっています。Ruby 1.9では、SimpleObject がオブジェクト階層の最上位となり、Object がその直下に置かれます。SimpleObject があれば BlankSlate は必要なくなるため、ビルダー DSL を遥かに簡単に作成できるようになります。

Builder のような DSL を作成できると、なぜプログラミング言語では表現力と処理能力がそれほど重要なのかが明らかになります。Ruby の Builder のコードの量は、他の言語の同様のライブラリーに比べて遥かに少なくなっています。それは、他の言語よりも柔軟な設計媒体となる Ruby を利用して、この Builder が作成されているためです。

まとめ

このシリーズの当初から、ソフトウェア・システムの設計はそのソース・コードのすべてを包含すると主張してきました。この主張の言外に含まれる意味は、使用する言語の表現力が豊かであればあるほど、広範な設計基盤を使えるということです。このことは、皆さんが汎用言語 (Java、Ruby、Groovy、Clojure) を選択する場合に当てはまるだけでなく、ある言語をベースに DSL を使用して作成できる言語にも当てはまります。ビジネスの概念を正確に表現する言語を作成すると、組織にとってかけがえのない資産になります。そのビジネスの目的に極めてよく適合する言語で実際の問題を解決するという、重要な手段を獲得することになるからです。

組織が大々的に Ruby または Groovy などの言語での開発に乗り換えないとしても、Ruby で実装された RSpec や Groovy で実装された easyb などのツール (「参考文献」を参照) を使用するという方法で、これらの言語を忍び込ませることはできます。Ruby または Groovy をこっそり採り入れることによって、新しい言語の導入に対して必要以上に慎重になっている人々でも、これらの言語がもたらす顕著なメリットを理解できるようになるはずです。


ダウンロード可能なリソース


関連トピック

  • プロダクティブ・プログラマ – プログラマのための生産性向上術』(Neal Ford 著、オライリー・ジャパン、2008年): Neal Ford の最新の本で、このシリーズで取り上げるいくつもの話題を詳細に解説しています。
  • JRuby: JRuby は、あらゆるプラットフォームでの Ruby 実装のなかで、最も優れた実装の 1 つとして数えられます。
  • Ruby on Rails: Rails はよく使われている Web 開発プラットフォームで、多数の DSL を使用します。
  • RSpec: Ruby で作成されたテスト・フレームワーク、RSpec では DSL 手法を使用しています。
  • Rake: Rake は Ruby プラットフォーム用のビルド・ツールです。
  • Cucumber: Cucumber は、Ruby の多くの優れた DSL 手法を実証する強力な BDD テスト・フレームワークです。
  • easyb: easyb は、Groovy ベースの Java プラットフォーム対応 BDD フレームワークです。
  • Builder: Builder は、プログラムによる XML 文書の生成を簡易化する Ruby ライブラリーです。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=556050
ArticleTitle=進化するアーキテクチャーと新方式の設計: JRuby で DSL を作成する
publish-date=09282010