今回は内部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(デプロイツール)などがあります。
また、Rubyと同じように内部DSLに適した言語には、「プログラム可能なプログラミング言語」と言われているLisp、メタプログラミングに適し、文法までも拡張することができるSmalltalkがあります。
その一方で、内部DSLに適さないプログラミング言語には、Java、C#、C++などがあります。Javaでは、XMLを使ってアプリケーションの振る舞いを変更するケースが非常に多くあります。この様な方法を一般的に外部DSLと呼んでます。
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 | 指定したデータの取得
- a.先頭データ取得
- b.最終データ取得
- c.全データ取得
- d.ID指定データ取得
- e.複数ID指定データ取得
|
(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 | :email |
この方法を採用することで生じる問題点は、共通的な処理を再利用可能なカタチにしたとしても限界があるということです。このプログラムは、メールアドレスCSV専用になっているため、他の形式のCSVファイルを扱う事ができません。そして、データフォーマットの変更(列の増減、列の属性の変更)に弱く、変更がある度にプログラムの修正をしなければなりません。
(2)ハッシュ(連想配列)に値を保持する方法
メンバ変数に値を保持する方法と同様に、CSVRecordクラスを継承し、CSVファイルのデータをハッシュに保持するクラスを定義します。2行目以降のデータ取得時に、1行目で取得した列情報をハッシュのキーと、取得した値をセットにしてデータを保持します。
この方法を採用する事で生じる問題点は、ハッシュを使っていることから、キーを文字列(または、Rubyのシンボル型)としなければならないため、データを自然な形で扱えない、というデメリットがあります。
内部DSL
一般的なプログラムで実現できなかった、再利用が可能で、データを自然な形で扱うプログラムを考えてみます。
表1の内容を変更し、あらゆるフォーマットのCSVファイルをハンドリングできるようにします。
1つ目の変更箇所は、CSVファイルパスの指定のデフォルトをクラス名から自動的に取得するように変更します(クラス名 + ".csv")。
2つ目は、アクセッサメソッドをCSVファイルの1行目に記述されている列名から動的に生成します。このことで、自然な形でデータにアクセスすることができます。
表3 共通する処理
1 | クラス名から自動的にCSVファイル名を取得する |
2 | CSVファイルの読み込み |
3 | CSVデータの解析 |
4 | 指定したデータの取得
- a.先頭データ取得
- b.最終データ取得
- c.全データ取得
- d.ID指定データ取得
- e.複数ID指定データ取得
|
5 | アクセッサメソッドの自動生成 |
メールアドレスCSVファイルをハンドリングするクラスは、共通する処理以外に実装する機能(処理)がなければ、次の2行で実装は完了です。
次の2つの要件を満たす内部DSLを実装していきます。
- 要件
- クラス名から、自動的にファイル名を取得する
クラスのコンストラクタ(initializeメソッド)でRubyのリフレクションの機能を利用して、クラス名を取得し、パス名を生成します。
- アクセッサメソッドを自動生成する
class_evalメソッドを使い対象となるクラスにアクセッサメソッドを動的に追加します。これにより、どのようなCSVファイルであろうとも1行目に記述されている列名からアクセッサメソッドを作ることができます。さらに、自然な形でデータにアクセスすることができます。
行データは、配列として取得します。CSVのカラムとアクセッサメソッドは、マッピングされているので、値を自動的に設定することができます(set_valuesメソッド)。
表4 追加されたアクセッサ・メソッド
getter | setter |
id | id= |
last_name | last_name= |
first_name | first_name= |
email | email= |
内部DSLで書いたプログラムで、2つの要件を満たすことができました。
Rubyでは、DSLの作成にeval、class_eval、そしてinstance_evalという便利な機能を使う事ができます。evalは、文字列をRubyのステートメントとして評価します。evalはRubyカーネルのメソッドであり、オブジェクト内部や単純なスクリプトの中でも使うことができます。これにより、非常に柔軟なプログラムを作ることができるのです。
しかしながら、evalメソッドを多用すると結果を予想することが難しくなる、という副作用があります。evalメソッドは、ここぞ、という箇所でのみの使用をおすすめします。
まとめ
今回は、Rubyは強力な内部DSLを作ることに適したプログラミング言語である事が理解できました。次回は、さらに一歩進み、文章を書くような流暢なプログラム(Fluent interface)を可能にするDSLを考えていきたいと思います。
今回紹介したサンプルプログラムは以下よりダウンロードできます.