今そこにある“DSL”

第2回 内部DSLへの道

この記事を読むのに必要な時間:およそ 2 分

今回は内部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 共通する処理

1CSVファイルのパスの指定
2CSVファイルの読み込み
3CSVデータの解析
4指定したデータの取得
  • a.先頭データ取得
  • b.最終データ取得
  • c.全データ取得
  • d.ID指定データ取得
  • e.複数ID指定データ取得
(1)メンバ変数に値を保持する方法

CSVRecordクラスを継承し,メールアドレスが記述されているCSVファイルを専用に扱います。クラス名は,Contactクラスとします。

Contactクラスには,CSVファイルの列をどのメンバ変数に割り当てるかを定義しなければなりません。(CSVファイルの1行目にカラム名が書いてありますが,列番号で管理した方がプログラムとして簡単であるため,この方法では,1行目のカラム名は使ってません)。

クライアントがContactクラスにアクセスし,データを自然な形で取得できるように,アクセッサメソッドをそれぞれ定義します。

表2 CSVファイルの列とContactクラスのメンバ変数との対応表

列番号定数アクセッサメソッドの定義
1COL_ID = 0:id
2COL_FIRST_NAME = 1:first_name
3COL_LAST_NAME = 2:last_name
4COL_EMAIL = 3:email
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のシンボル型)としなければならないため,データを自然な形で扱えない,というデメリットがあります。

著者プロフィール

原陽亮(はらようすけ)

Computer Programmer,Software Designer,Lifehacker。

コメント

コメントの記入