今回は内部DSLの詳細,またその実践としてRubyを例にした実装について解説します。
内部DSLに適した言語 - Ruby
2005年12月にRuby on Railsの正式版のリリース以降,そのフレームワークのみならず,プログラミング言語Rubyを支持,採用するプログラマーが増えてきました。
そのことを端的に表しているのが,達人クラスのプログラマー,そしてアーキテクトの存在です。一人は『達人プログラマー(※1)』『プログラミングRuby(※2)』などの著書で知られているDave Thomas氏です。もう一人は『エンタープライズ アプリケーションアーキテクチャパターン(※3)』などで著名なアーキテクト Martin Fowler氏です。Fowler氏が属している会社 - ThoughtWorks社(※4)の多くのプロジェクトは,Rubyで開発していると聞いてます。
なぜ,彼等は,Rubyを支持しているのでしょうか?
私は,実際に彼等の言葉を聞いたわけではありませんが,Rubyで強力なDSLを作ることができるからではないか,と推測してます。
Dave Thomas氏は,その著書 - 達人プログラマーの§12.専用言語で,「問題領域の解決にはその領域の言語を前提に置くべきであり,そこからプログラミング上の解決策を引き出す事が重要である」と述べてます。そのことから,DSLに造詣が深いことを伺い知ることができます。
Rubyで作られるDSLの多くは,内部DSLです。その主なものとして,Rails ActiveRecord, Rails Validation, Rake(Make Ruby), RSpec(テスティングツール), Capistrano(デプロイツール)などがあります。
リスト1 内部DSLを使って実装されている代表的なもの
a. Ruby on Rails
class Library < ActiveRecord::Base
has_many :books
validates_associated :books
end
b.Rake
task :default => [:test]
task :test do
ruby "test/unittest.rb"
end
c.RSpec
describe Bowling do
it "should score 0 for gutter game" do
bowling = Bowling.new
20.times { bowling.hit(0) }
bowling.score.should == 0
end
end
d.Capistrano
role :libs, "www.gihyo.jp"
task :search_libs do
run "ls -x1 /usr/lib | grep -i xml"
end
また,Rubyと同じように内部DSLに適した言語には,「プログラム可能なプログラミング言語」と言われているLisp,メタプログラミングに適し,文法までも拡張することができるSmalltalkがあります。
その一方で,内部DSLに適さないプログラミング言語には,Java,C#,C++などがあります。Javaでは,XMLを使ってアプリケーションの振る舞いを変更するケースが非常に多くあります。この様な方法を一般的に外部DSLと呼んでます。
- ※1)
- 『達人プログラマー―システム開発の職人から名匠への道』 Andrew Hunt/David Thomas著,村上雅章訳,ISBN10:4894712741
- ※2)
- 『プログラミングRuby 第2版 言語編』/『プログラミングRuby 第2版 ライブラリ編』Dave Thomas/Chad Fowler/Andy Hunt著,まつもとゆきひろ監訳,田和勝訳,ISBN10:4274066428/4274066436
- ※3)
- 『エンタープライズ アプリケーションアーキテクチャパターン』Martin Fowler著,長瀬嘉秀監訳,株式会社テクノロジックアート翻訳,ISBN104798105538
- ※4)
- 『ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション』ThoughtWorks Inc.著,株式会社オージス総研 オブジェクトの広場編集部翻訳,ISBN10:487311389X
Rubyによる,CSVファイル読み込みDSL
「第1回 - DSLとは?」の中でCSVファイルを扱う例をあげ,一般的なプログラムとDSLを使ったメタプログラミングの違いを説明しました。今回は,実際にソースコードを示し説明します。そうすることで,一般的なプログラムと,内部DSLの違いと内部DSLを使う事のメリットを理解することができるからです。
CSVファイルの仕様は,1行目がカラム名,2行目以降がデータであり,整数型のID列を必須とします。また,RFC4180に準拠していることとします。
一般的なプログラム
一般的なプログラムの方法は,2つあります。クラスのメンバ変数に値を保持する方法と,ハッシュ(連想配列)に値を保持する方法です。
その2つの方法で共通する処理を抽出し,“CSVRecordクラス”を作ります。
表1 共通する処理
| 1 | CSVファイルのパスの指定 |
| 2 | CSVファイルの読み込み |
| 3 | CSVデータの解析 |
| 4 | 指定したデータの取得
|
(1)メンバ変数に値を保持する方法
CSVRecordクラスを継承し,メールアドレスが記述されているCSVファイルを専用に扱います。クラス名は,Contactクラスとします。
Contactクラスには,CSVファイルの列をどのメンバ変数に割り当てるかを定義しなければなりません。(CSVファイルの1行目にカラム名が書いてありますが,列番号で管理した方がプログラムとして簡単であるため,この方法では,1行目のカラム名は使ってません)。
クライアントがContactクラスにアクセスし,データを自然な形で取得できるように,アクセッサメソッドをそれぞれ定義します。
表2 CSVファイルの列とContactクラスのメンバ変数との対応表
| 列番号 | 定数 | アクセッサメソッドの定義 |
|---|---|---|
| 1 | COL_ID = 0 | :id |
| 2 | COL_FIRST_NAME = 1 | :first_name |
| 3 | COL_LAST_NAME = 2 | :last_name |
| 4 | COL_EMAIL = 3 |
class Contact < CSV::Base::CSVRecord
COL_ID = 0
COL_FIRST_NAME = 1
COL_LAST_NAME = 2
COL_EMAIL = 3
def set_values(row_num, values)
return if row_num == COLUMN_ROW_NUM
# contact value object - nameless class.
contact_class = Class.new{
attr_accessor :id, :first_name, :last_name, :email
def to_s
str = ""
str << "id:#{self.id}, "
str << "first_name:#{self.first_name}, "
str << "last_name:#{self.last_name}, "
str << "email:#{self.email}"
end
}
contact = contact_class.new
contact.id = values[COL_ID].to_i
contact.first_name = values[COL_FIRST_NAME]
contact.last_name = values[COL_LAST_NAME]
contact.email = values[COL_EMAIL]
@records << contact
end
end
この方法を採用することで生じる問題点は,共通的な処理を再利用可能なカタチにしたとしても限界があるということです。このプログラムは,メールアドレスCSV専用になっているため,他の形式のCSVファイルを扱う事ができません。そして,データフォーマットの変更(列の増減,列の属性の変更)に弱く,変更がある度にプログラムの修正をしなければなりません。
(2)ハッシュ(連想配列)に値を保持する方法
メンバ変数に値を保持する方法と同様に,CSVRecordクラスを継承し,CSVファイルのデータをハッシュに保持するクラスを定義します。2行目以降のデータ取得時に,1行目で取得した列情報をハッシュのキーと,取得した値をセットにしてデータを保持します。
class CSVHash < CSV::Base::CSVRecord
def set_values(row_num, values)
# CSVファイルの1行目の列名は,ハッシュのキーとして利用する.
if (row_num == 1)
@columns = values
else
hash = {}
values.length.times do |i|
# ハッシュに値を設定
hash[@columns[i]] = values[i]
end
@records << hash
end
end
end
この方法を採用する事で生じる問題点は,ハッシュを使っていることから,キーを文字列(または,Rubyのシンボル型)としなければならないため,データを自然な形で扱えない,というデメリットがあります。

