今そこにある“DSL”

第2回 内部DSLへの道

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

内部DSL

一般的なプログラムで実現できなかった,再利用が可能で,データを自然な形で扱うプログラムを考えてみます。

表1の内容を変更し,あらゆるフォーマットのCSVファイルをハンドリングできるようにします。

1つ目の変更箇所は,CSVファイルパスの指定のデフォルトをクラス名から自動的に取得するように変更します(クラス名 + ".csv")⁠

2つ目は,アクセッサメソッドをCSVファイルの1行目に記述されている列名から動的に生成します。このことで,自然な形でデータにアクセスすることができます。

表3 共通する処理

1クラス名から自動的にCSVファイル名を取得する
2CSVファイルの読み込み
3CSVデータの解析
4指定したデータの取得
  • a.先頭データ取得
  • b.最終データ取得
  • c.全データ取得
  • d.ID指定データ取得
  • e.複数ID指定データ取得
5アクセッサメソッドの自動生成

メールアドレスCSVファイルをハンドリングするクラスは,共通する処理以外に実装する機能(処理)がなければ,次の2行で実装は完了です。

class Contact < CSV::Base::CSVRecord
end

次の2つの要件を満たす内部DSLを実装していきます。

要件
  1. クラス名から,自動的にファイル名を取得する
    クラスのコンストラクタ(initializeメソッド)でRubyのリフレクションの機能を利用して,クラス名を取得し,パス名を生成します。
  2. アクセッサメソッドを自動生成する
    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 << object
  end
  ### 以下省略 ###
end

表4 追加されたアクセッサ・メソッド

gettersetter
idid=
last_namelast_name=
first_namefirst_name=
emailemail=

内部DSLで書いたプログラムで,2つの要件を満たすことができました。

Rubyでは,DSLの作成にeval,class_eval,そしてinstance_evalという便利な機能を使う事ができます。evalは,文字列をRubyのステートメントとして評価します。evalはRubyカーネルのメソッドであり,オブジェクト内部や単純なスクリプトの中でも使うことができます。これにより,非常に柔軟なプログラムを作ることができるのです。

しかしながら,evalメソッドを多用すると結果を予想することが難しくなる,という副作用があります。evalメソッドは,ここぞ,という箇所でのみの使用をおすすめします。

まとめ

今回は,Rubyは強力な内部DSLを作ることに適したプログラミング言語である事が理解できました。次回は,さらに一歩進み,文章を書くような流暢なプログラム(Fluent interface)を可能にするDSLを考えていきたいと思います。

今回紹介したサンプルプログラムは以下よりダウンロードできます.

著者プロフィール

原陽亮(はらようすけ)

Computer Programmer,Software Designer,Lifehacker。

コメント

コメントの記入