今回は内部DSLの詳細、
内部DSLに適した言語 - Ruby
2005年12月にRuby on Railsの正式版のリリース以降、
そのことを端的に表しているのが、
なぜ、
私は、
Dave Thomas氏は、
Rubyで作られる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による、CSVファイル読み込みDSL
「第1回 - DSLとは?」
CSVファイルの仕様は、
一般的なプログラム
一般的なプログラムの方法は、
その2つの方法で共通する処理を抽出し、
1 | CSVファイルのパスの指定 |
2 | CSVファイルの読み込み |
3 | CSVデータの解析 |
4 | 指定したデータの取得
|
(1)メンバ変数に値を保持する方法
CSVRecordクラスを継承し、
Contactクラスには、
クライアントがContactクラスにアクセスし、
列番号 | 定数 | アクセッサメソッドの定義 |
---|---|---|
1 | COL_ | :id |
2 | COL_ | :first_ |
3 | COL_ | :last_ |
4 | COL_ |
class Contact
この方法を採用することで生じる問題点は、
(2)ハッシュ(連想配列)に値を保持する方法
メンバ変数に値を保持する方法と同様に、
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
この方法を採用する事で生じる問題点は、
内部DSL
一般的なプログラムで実現できなかった、
表1の内容を変更し、
1つ目の変更箇所は、
2つ目は、
1 | クラス名から自動的にCSVファイル名を取得する |
2 | CSVファイルの読み込み |
3 | CSVデータの解析 |
4 | 指定したデータの取得
|
5 | アクセッサメソッドの自動生成 |
メールアドレスCSVファイルをハンドリングするクラスは、
class Contact < CSV::Base::CSVRecord
end
次の2つの要件を満たす内部DSLを実装していきます。
- 要件
- クラス名から、
自動的にファイル名を取得する
クラスのコンストラクタ(initializeメソッド) でRubyのリフレクションの機能を利用して、 クラス名を取得し、 パス名を生成します。 - アクセッサメソッドを自動生成する
class_evalメソッドを使い対象となるクラスにアクセッサメソッドを動的に追加します。これにより、 どのようなCSVファイルであろうとも1行目に記述されている列名からアクセッサメソッドを作ることができます。さらに、 自然な形でデータにアクセスすることができます。
行データは、配列として取得します。CSVのカラムとアクセッサメソッドは、 マッピングされているので、 値を自動的に設定することができます (set_ valuesメソッド)。
- クラス名から、
# CSVRecordクラス - 一部抜粋
class CSVRecord
CSV_SUFFIX = ".csv"
COLUMN_ROW_NUM = 1
COL_ID = "id"
def initialize()
@columns = []
@records = []
# 1. クラス名からファイルを取得.
@path = self.class.to_s.concat(CSV_SUFFIX)
@klass = Class.new
self.load
end
protected
def generate_accessors(columns)
# 2. カラム名からアクセッサーメソッドを動的に生成.
@columns = columns
@columns.each do |col|
@klass.class_eval %{
def #{col}
@#{col}
end
def #{col}= (value)
@#{col} = value
end
}
end
end
# 3. CSVファイルから取得したデータを保持する
def set_values(values)
object = @klass.new
values.length.times do |i|
if @columns[i] == COL_ID
object.instance_variable_set("@#{@columns[i]}", values[i].to_i)
else
object.instance_variable_set("@#{@columns[i]}", values[i])
end
end
@records
getter | setter |
---|---|
id | id= |
last_ | last_ |
first_ | first_ |
email= |
内部DSLで書いたプログラムで、
Rubyでは、
しかしながら、
まとめ
今回は、
今回紹介したサンプルプログラムは以下よりダウンロードできます.