今そこにある“DSL”
第3回 流暢なインターフェース
最終回となる今回は,文章を書くようにプログラムを記述する流暢なインターフェースについて解説します。
レビュー:内部DSLと外部DSL
内部DSLを採用したDSLは,とても親しみやすいと言えます。なぜなら,ホスト言語のパワーと今まで使っていたツールをそのまま使うことができるということと,内部DSLは,文章のように書かれていることが多いので,処理の結果を容易に想像することができるからです。しかし良いことばかりではなく,良く設計されていない内部DSLは,処理の結果を想像することが難しくなる,という問題点もあります。
外部DSLは,その構文やルールを新たに学ばなければなりません。そして,決して読みやすいとは言えません。Eclipseプラグインのようなツールを提供しなければならなくケースが非常に多くあります。しかしその反面,外部DSLの開発者が自由にDSLのフォーマットを決めることができます。
内部DSLを学習するにあたり耳にする言葉は,“Fluent Interface(流暢なインターフェイス)”です。この概念は,アーキテクト Martin Fowler氏と,『Domain-Driven Design』(※1)の著書で知られるEric Evans氏の二人で考えられました。
次は,“Fluent Interface”を見ていきます。
- ※1)
- 『Domain-Driven Design』(ISBN-10: 0321125215)
Fluent Interface - 流暢なインターフェース
一般的なプログラムは,そこに書かれているメソッドそれぞれに意味があります。それに対して,Fluent Interfaceは,そのメソッドには意味がなく,記述されたプログラムが意味を持ちます。なぜなら,Fluent Interfaceは,文章のように読めることを第一に設計されているからです。
例を挙げて説明します。コーヒーショップでコーヒーの注文を受ける簡単なアプリケーションを考えます。
一般的なプログラムでは,メソッドに意味があるので,注文伝票に対して注文を3件追加し,その後にコーヒーを作っているのがわかります。それに対して,Fluent Interfaceで記述されたプログラムのメソッドには意味がなく,プログラム全体が1つの文章になっているのがわかります。
Fluent Interfaceは,一見するとメソッド・チェインのように見えます。Fluent Interfaceとメソッド・チェインの大きな違いは,「文章を書くように,流れるようにプログラムを書く事が可能か」という点に尽きます。メソッド・チェインは,次に来るメソッドを考えながらプログラムを書かねばなりません。
リスト1一般的なプログラム(Ruby)
order = Order.new
order.add(Coffee.new(:LATTE, :TALL, {milk=>:SOY})
order.add(Coffee.new(:CAPPUCCUINO, :SHORT, {milk=>:NON_FAT})
order.add(Coffee.new(:AMERICANO, :GRANDE)
order.prepare
リスト2Fluent Interface(Ruby)
order = Order.new
.with(:LATTE, :size=>:TALL, :milk=>:SOY)
.with(:CAPPUCCUINO, :size=>:SHORT, :milk=>:NON_FAT)
.with(:AMERICANO, :size=>:GRANDE)
.prepare
Fluent Interfaceは,Rubyなどの動的型付け言語以外でも実現することが可能です。リスト3は,Seasar S2JDBCを利用したJavaのプログラムの例です。非常に可読性が高く,処理の内容とその結果を直ぐに理解することができるはずです。
リスト3Searsar S2JDBCの例(Java)
List<Contact> results = jdbcManager.from(Contact.class)
.join("prefecture")
.where("id in (? , ?)", 1, 2)
.orderBy("last_name")
.getResultList();
しかし,Fluent Interfaceは,良いことばかりではありません。1つ目の問題点は,一般的なプログラムとはそのプログラムの性質が異なるので,非常に違和感のある実装をしなければなりません。たとえば,一般的なプログラムでは,値を追加するメソッド,値を設定するメソッドのリターン値は,void型です。しかし,Fluent Interfaceは,文を継続するために何らかの値を返す必要があります。こうした一般的な慣習を無視する必要が出てくるのです。
2つ目は,Fluent Interfaceの構成要素の1つとなっているメソッド単体だけを見ても何をするメソッドなのかわからないことです。また,流れるようなインターフェースを作ることは,そんなに簡単なことではなく,長考を余儀なくされます。
動的受信
Fluent Interfaceを紹介するときに必ずと言って良いほど引き合いに出されるのが,Jim Weirich氏が開発したXmlMarkupです。このライブラリは,XMLを生成するRubyのライブラリです。
リスト4XmlMarkup(Ruby)
require 'builder/xmlmarkup'
xml = Builder::XmlMarkup.new(:indent=>2)
puts xml.html {
xml.head {
xml.title "Fluent Interface"
}
xml.body {
xml.comment! "comment"
xml.div {
xml.h1("Header")
xml.p("paragraph")
}
}
}
リスト5リスト4による出力結果(XML)
<html>
<head>
<title>Fluent Interface</title>
</head>
<body>
<!-- comment -->
<div>
<h1>Header</h1>
<p>paragraph</p>
</div>
</body>
</html>
XmlMarkupは,Rubyの“method_missingメソッド”,クロージャーの機能を使って実装されてます。それらの機能を使うことで一般的なプログラムよりも可読性が高く,処理の結果を容易に想像することを可能にしています。
このmethod_missingメソッドは,Rubyなどの動的型付け言語には必ずと言って良いほどあるメソッドです。プログラムから指定したメソッドが存在しないときに,このメソッドが呼ばれます。カスタムクラスは,method_missingメソッドをオーバーライド(再定義)し,独自の機能を追加することができます。
リスト6 method_missingメソッドを使った例(Ruby)
class Greeter
GREETINGS = {
:dutch => 'Hallo',
:english => 'Hello',
:french => 'Bon Jour',
:japanese=> 'こんにちは',
:spanish => 'Hala'
}
def method_missing(method_name, *args)
name = method_name.to_s.split(/_/).first
language = method_name.to_s.split(/_/).last.to_sym
return unless name == 'greet'
puts GREETINGS[language] + '!'
end
end
greeter = Greeter.new
greeter.greet_in_dutch #=> Hallo!
greeter.greet_in_english #=> Hello!
greeter.greet_in_french #=> Bon Jour!
greeter.greet_in_japanese #=> こんにちは!
greeter.greet_in_spanish #=> Hala!
“method_missingメソッド”を使うときに気をつけなければならないことは,存在しない未知のメソッドを動的に受信しているので,注意深く利用する必要があります。そうしなければ,使う人,メンテナンスする人を混乱に陥れることになります。これは,前回使った“eval系のメソッド”にも言えることです。
最後に
DSLは,私達プログラマーの側に存在しています。その技術を賢く利用することで楽しいプログラミングライフを送ることができるのではないでしょうか。前回も書きましたが,DSLは,決して万能ではありません。適材適所で利用することではじめて大きな成果をだすことができます。
今回の連載では,Rubyを使った例が数多く出てきましたが,Ruby以外のプログラミング言語でもその考え方を利用することができます。より良いアプリケーション/ツールを作る1つの手段として,DSLがあることを記憶にとどめていただければ,と思います。

