Ruby Library Report 【第 1 回】 IoC コンテナ

著者・編集者:Rubyist Magazine RLR 担当 (立石 孝彰)

はじめに

Ruby Library Report について

Ruby Library Report (RLR) では、以下のサイトに登録されているライブラリや ツールなど、開発において役立ちそうなものについて順次紹介していきます。

上記サイトなどで様々なライブラリを見つけることができますが、あまり聞かない ものなども多くあります。しかし、それぞれのライブラリを少しでも知っていれば、 開発において必要で適切なライブラリを探すという苦労や、自作するという苦労を 省けます。 また、実装方法や思想などの違いなどから、目的は同じでも複数のライブラリが 存在する場合があります。このような場合、どれを使っても同じというわけでは なく、また、有名なものを使えば良いというわけではないと考えています。 自分が目標とする成果物に適したライブラリを選択し、再利用することが重要です。 RLR は、そのような場合に補助的な参考資料として、少しでも役に立つことを考 えて始めました。全く知らないよりは、少しでも知っている方が、開発方針に幅 を持たせることができると思います。

ライブラリの目的は、簡単には、方法やアイデアを実現し、再利用可能とするこ とです。再利用するためには、その前提条件や技術的背景を知る必要があります。 そこで、前提知識が必要と判断したものについては、必要な範囲で説明を行うつ もりです。しかし、本レポートは,個々の技術の説明が主テーマではないため,説 明が不十分であることが多くなると思います。 そのような場合には,ruby-list メーリングリストで質問をするか、以下の Web サイ トにアクセスすることで情報を得ることができる場合があります。

これらのサイトは、Ruby 関連の情報を提供するサイトとして良く知られています。

また、紹介文を読むだけでは著者の感覚も伝わらないと思いますし、著者とは異な る感想も持つはずです。そのため、簡単なチュートリアルなどを通して紹介を行う ことがあります。 是非、読者の皆様自身でも利用し、その経験を Rubyist Magazine に寄稿して頂 ければ著者としてこれ以上の喜びはありません。また、次には読者の皆様自身が、 RLR の執筆者となって頂ければと考えています。

さて、RLR 第 1 回では、IoC コンテナ [1,3] である Copland、Rico、tudura 1を紹介します。 今回のレポートでは、初めに IoC コンテナについての簡単な説明を記載します。 IoC をご存知のユーザは、読み飛ばしても支障はないと思います。 その次に、Copland、Rico、tudura を続けて紹介します。

IoC と軽量コンテナについて

IoC とは Inverse of Control (制御の逆転) の略記です。各コンポーネントや コンテナ2 との依存性をコードから排除し、コンテナによって依存性の解決を行うというパタ ーンです。 ここで、依存性とはどのようにコンポーネントを獲得するかということに関わって きます。つまり、クラスやオブジェクトという用語を用いるのであれば、オブジェ クトの生成方法を、コンポーネントを利用するコードから分離したいということが IoC の目的です。 使用範囲を絞れば、プラグインを一般化した機構を IoC だと想像して頂ければ良 いと思います。また、コンテナがオブジェクトを生成する方法として、シリアライ ゼーションの機能などを用いても良いでしょう。

実際に、コンポーネント開発者から見ると、必要なコンポーネントはコンテナによ って提供されているものとして開発を行います。例えば、Ruby の場合、コンテナ がそれぞれのインスタンス変数にオブジェクトを代入するということを想像してくだ さい。以下の例では、インスタンス変数 @attr を持つ Bar クラスを実装してお り、obj は、コンテナが与えるオブジェクトです。

 class Bar
   def initialize(obj)
     @attr = obj
   end
   ...

ここで着目したいことは、@attr に代入されるべきオブジェクトは、Bar クラス の外側、つまり、コンテナが決定するという点です。 このように、Foo クラスから Foo オブジェクトを生成し、そのオブジェクトを @attr に代入する、つまり、initialize の引数に渡すということが、IoC コンテナ の仕事です。 そして、そのような組み立て方を指示する設定が、それぞれのコンポーネントの実装 とは分離して記述できることによって、各コンポーネント間の依存性が低くなります。 コンテナを用いない場合、次のようなコードを記述することでしょう。

 class Bar
   def initialize()
     @attr = Foo.new(...)
   end
   ...

このようなコードを書くと、Bar クラスでは、Foo クラスのインスタンスの獲得方法 を明示的に記述しています。このため、Bar クラスを利用するためには、Foo クラス も同時に利用することになります。 もちろん、Ruby の動的な機能や、他のパターンを利用して、依存しないようなコード を書くことはできますが、そのようなコードを何度も書く必要があり、アプリケーショ ン全体で一貫性を保つためには、多少の手間が必要です。 その手間を請け負うものが IoC コンテナです。

Martin Fowler の著作 [1] によれば、IoC については三つのタイプがあります。 それは、Constructor Injection、Setter Injection、Inteface Injection です。 Constructor Injection では、コンストラクタによって各コンポーネントに対して 別のコンポーネントを関係付けます。また、Setter Injection では、Setter を 用いることで別のコンポーネントを関係付けます。 Interface Injection も Setter Injection と同様に特別なメソッドを経由して型 を頼りに属性にコンポーネントを関係付けます。 先の Bar クラスの例は、Constructor Injection におけるコンポーネントの例です。 一方で、Rod Johnson の書籍 [3] によれば IoC は Dependency Lookup と Dependency Injection に分けるようです。 そして、Dependency Injection のタイプ分けとして、Constructor Injection と Setter Injection を挙げています。 本レポートで紹介する Rico は Constructor Injection を利用しており、Copland と Tudura は、Setter Injection を利用しています。

また、軽量コンテナという用語を、IoC コンテナと同時に見かけることがあります。 軽量コンテナとは、コンテナに期待される基本的な機能を備えつつ、コンポーネント がコンテナにほとんど依存せず、デプロイ3 に必要以上の手間がかからない手軽なコンテナのことを指すようです。 そして、コンポーネントの依存性を低くするために IoC を用いているように思います。 私も IoC コンテナや、軽量コンテナの厳密な定義は定かではありませんが、IoC コン テナにとって重要なことは、コンポーネントの依存性を排除するための仕組みです。

IoC コンテナを用いると、コンポーネントの依存性を排除することによって、個々の コンポーネントの独立性が高まり、単独でテストを行うことが少しは容易になります。 また、アプリケーションを超えたコンポーネントの再利用を促進できます。従来のフ レームワークは、再利用の対象がフレームワークそのものであったのに対して、コン ポーネント指向の開発では、再利用の対象がコンポーネントです。 そのため、IoC はコンポーネント指向開発の一つの解となるでしょう。 最近では、IoC ではなく、DI と呼ぶこともあります。これは、Dependency Injection (依存性注入) の略記です。一般的に、IoC と DI は区別されて使われ ておらず、IoC という名前があまりに包括的であり、何を反転させるのかという混乱 を招くために、DI という用語が使われるようになったようです。

IoC コンテナは、アプリケーションを超えた再利用ができるなど、一見素晴 らしい道具のように見えるかもしれませんが、IoC コンテナを利用すべきかどうかは、 作成するアプリケーションによります。例えば、IoC コンテナを利用すると、コンポ ーネントの関係が設定時に決定されてしまうため、動的な構成変更を必要とするソフ トウェアには不向きです。

試用レポート – Copland

登録データ

概要

Copland は、Java 版 IoC コンテナである HiveMind [2] のデザインを真似て作成 された Ruby 用の IoC コンテナです。 Ruby らしいところは、各コンポーネントの依存性を記述する設定ファイルを、 YAML [4,5,6] によって記述するという点です。以降では、まず作者の Jamis Buck 氏 のコメントを紹介し、次に、サンプルを通して簡単に Copland を紹介します。

作者からの声

作者の Jamis Buck 氏にコンタクトをとったところ以下の文面を頂いたので 紹介します。

It seems very few Rubyists have done much with Inversion of Control, it being a domain most explored in the Java world. I was first introduced to IoC about a year ago, when my office moved to Java and started using HiveMind to manage our application services. As an attempt to better understand how HiveMind worked its magic, I began work on an IoC container of my own. Ruby was the natural choice for the implementation language. Copland has since grown to include nearly the functionality of HiveMind itself, and is currently undergoing heavy development in preparation for RubyConf (where I will be presenting on it). Hopefully, Copland will be a means for Ruby programmers to become more familiar with IoC and its benefits.

Jamis 氏は、どうやら HiveMind をより詳しく知ろうとするために、Copland の作成を始めたようです。著者としては、今後は、Ruby ならではの IoC コン テナの利点なども追求してもらいたいと思っています。

サンプル

IoC コンテナでは、オブジェクトの生成を支援する仕組みが備わっています。 まずは、文字列を出力するだけの簡単なクラス RLR::Display を作成 して、そのオブジェクトを生成してみます。

 # copland/display.rb
 module RLR
   class Display
     attr_accessor :filter

     def print(*args)
       if( @filter )
         $stdout.print(@filter[*args])
       else
         $stdout.print(*args)
       end
     end
   end
 end

このクラスを含むファイルを copland/display.rb とします。次に、このクラス の生成方法を Copland に示すために、設定ファイル copland/sample1.yml を書 きます。 Copland では、このファイルをパッケージと呼んでいます。

 # copland/sample1.yml
 ---
 id: sample1
 description: sample1.rb のパッケージ

 service-points:
   Display:
     implementor: display/RLR::Display

id を使って、このパッケージを識別するために、一意な名前を与えます。 これは、Copland が複数のパッケージを扱う上で必要となります。サービス ポイント (service-points) は、コンポーネント4 を利用するコードが、その コンポーネントをコンテナから獲得するときに用いるキーです。 Copland では、コンポーネントのことをサービスと呼ぶため、サービスポイ ントという名前を使っているようです。 sample1.yml では、Display というコンポーネントを、display.rb の RLR::Display と関連付けています。 つまり、Display というコンポーネントが RLR::Display のインスタンスで あることを登録しています。 このコンポーネントを利用するコードは、次の通りです。

 # copland/sample1.rb
 require 'copland'
 registry = Copland::Registry.build(".", :yaml_package_file => "sample1.yml")
 display = registry.service("sample1.Display")
 display.print("sample1.Displayのサンプル.\n")

まず、Copland::Registry.build によって先程の YAML で書いた設定ファイル を読み込み、レジストリと呼ぶコンテナを作成しています。 第一引数には、パッケージのあるディレクトリを指定し、第二引数にはオプシ ョンを与えます。オプションとして、シンボルと値から構成される Hash オブ ジェクトを与えます。ここでは、パッケージのファイル名として sample1.yml を指定しています。引数はそれぞれ省略可能ですが、その場合、第一引数にはカ レントディレクトリが指定されたことと同じです。また、省略時のパッケージの ファイル名は package.yml です。 次に、レジストリからコンポーネントを獲得します。ここで得るコンポーネント が、パッケージにより記述したサービスのことです。

それでは次に、@filter にオブジェクトを設定します。このためにまず、次の ようなフィルタのためのクラスを二つ用意することにしましょう。

 # copland/filter.rb
 module RLR
   class Filter
     def [](*args)
       args.join()
     end
   end

   class LabelFilter < Filter
     attr_accessor :label

     def [](*args)
       super("[", @label, "] ", *args)
     end
   end
 end

RLR::Filter には、受け取った文字列を連接して返すだけの簡単なメソッド があり、RLR::LabelFilter には、ラベルを文字列の頭に付けて返すメソッド があります。 まずは、RLR::Filter をコンポーネントとして登録し、そのサービスを先ほど の RLR::Display の @filter と結びつけることにします。 そのためのパッケージが、次の sample2.yml です。

 # copland/sample2.yml
 ---
 id: sample2
 description: this is the package for sample2.rb

 service-points:
   Filter:
     implementor: filter/RLR::Filter
   Display:
     implementor:
       factory: copland.BuilderFactory
       class: display/RLR::Display
       properties:
         filter: !!service Filter

Filter コンポーネントについては、sample1.yml の Display コンポーネント と同様です。 しかし、Display コンポーネントの記述量が増えたことに気づくと思います。 まず、implementor によって直接クラスを指定するのではなく、どのファクトリ を利用してコンポーネントを生成するかということを factory という命令に よって与え、どのクラスから生成するのかということを class で与えます。 次に、そのクラスにおけるプロパティと、どのコンポーネントを関連付けるか ということ、つまり、インスタンス変数にどのオブジェクトを与えるのかとい うことを properties によって指定します。 上記の場合、filter というプロパティに、Filter コンポーネントを関連付け ています。 これは、Ruby 上では、@filter というインスタンス変数に Filter コンポーネント 、つまり RLR::Filter オブジェクトをセットしていることに相当します。 sample2.yml の Display コンポーネントを利用するコードは次の sample2.rb の 通りです。

 # copland/sample2.rb
 require 'copland'
 registry = Copland::Registry.build(".", :yaml_package_file => "sample2.yml")
 display = registry.service("sample2.Display")
 display.print("sample2.Displayのサンプル.\n")

ここで、この sample2.rb を変更することなく、Display コンポーネントの filter プロパティに関連するコンポーネントを変更することができます。 そのためには、sample2.yml の filter プロパティに与えるコンポーネント を変更するだけです。 例えば、Display コンポーネントの properties の記述を次の通りに書き 換えると、LabelFilter コンポーネントが Display コンポーネントの filter プロパティに設定されます。

   properties:
     filter: !!service LabelFilter

さらに、次の sample3.yml のパッケージを用いると、LabelFilter コンポーネ ントの label プロパティ、つまり、RLR::LabelFilter オブジェクトの @label というインスタンス変数に文字列を与えることもできます。 このときには、!!service の代わりに !!string を使います。

 # copland/sample3.yml
 ---
 id: sample3
 description: this is the package for sample3.rb

 service-points:
   Filter:
     implementor:
       factory: copland.BuilderFactory
       class: filter/RLR::LabelFilter
       properties:
         label: !!string label
   Display:
     implementor:
       factory: copland.BuilderFactory
       class: display/RLR::Display
       properties:
         filter: !!service Filter

このようにして、複数のコンポーネント (オブジェクト) の組み立てを行う ことができますが、コンテナの機能として、メソッドをフックすることがで きるようになっています。これを Interceptor と呼びます。例えば、 sample3.yml において、Display コンポーネントに次の interceptors 命令 を加えてみます。conpland.LoggingInterceptor は、include で指示され た正規表現に一致するメソッドをフックして、そのログを copland.log に 出力するものです。

 Display:
   interceptors:
     - service: copland.LoggingInterceptor
       include:
         - print.*
   ...

以上で、Copland の紹介は終わりですが、copland を用いてイベントリスナー を登録することもできます。 今回のサンプルでは、イベントリスナーについてのサンプルは掲載しませんでし たが、Copland のマニュアルやチュートリアルに記載されていますので体験して みてください。

感想

Copland は !!service でコンポーネントを指定する以外に、Ruby における基 本的なオブジェクトを直接扱うことができます。例えば、sample3.yml で示した ように !!string を用いることができます。また、YAML を利用しているための特 徴として、YAML で扱える Ruby のオブジェクトであれば何でも記述することがで きます。そのまま label: label と記述しても支障はありません。 試しに、以下のような Struct なども与えてみてください。

 properties:
   label: !ruby/Struct:Article
     author: Takaaki Tateishi
     title: xxxxxxxxx

今回のサンプルでは、面白い結果を得ることはできませんが、直接 Ruby のオブ ジェクトを指定できるところが非常に手軽です。

また本稿の執筆に際して、途中までは、copland-0.6.0 を使っていましたが、Ruby に は Java における Interface に対応するものがないため、メソッドの存在を保証 できないことが多少不満でした。 そこで、パッケージを使ってメソッド名の付け替えを行えないかという提案をした ところ、この提案に賛成を頂くことができました。そして、0.7.0 においてメソッ ド呼び出しをリダイレクトする機能とブロックする機能を Interceptor の一種と して追加して頂きました。 電子メールで数回コンタクトをかとっただけですが、copland の開発は積極的 に行われているという印象を受けました。

試用レポート – Rico

登録データ

概要

Rico は、PicoContainer [8] と呼ばれる Constructor Injection を採用した IoC コン テナをベースにしています。 初期の頃の実装であり、著者が触れた限りでは、機能的には copland よりも洗練され ておらず、Ruby ならではの利点となる特徴もないように思います。 もし、Copland よりも Setter Injection が読者の好みに合うのであれば、Rico を改 良して Copland に並ぶ IoC コンテナを開発しても良いでしょう。 また、残念ながら、作者へのコンタクト先などが明記されていないため、コンタクトを とることは遠慮しました。

サンプル

copland の場合と同様に、Display クラスと Filter クラスを定義するところから 始めます。copland とは違い、Rico は Constructor Injection を採用している ため、コンストラクタによってコンテナから与えられるオブジェクトを受け取ります。 Ruby の場合、コンストラクタに相当するものは initialize ですので、initialize の引数によってオブジェクトを受け取ります。

 # rico/display.rb
 module RLR
   class Display
     def initialize(filter = nil)
       @filter = filter
     end

     def print(*args)
       if( @filter )
         $stdout.print(@filter[*args])
       else
         $stdout.print(*args)
       end
     end
   end
 end
 # rico/filter.rb
 module RLR
   class Filter
     def [](*args)
       args.join()
     end
   end

   class LabelFilter < Filter
     def initialize(label)
       @label = label
     end

     def [](*args)
       super("[", @label, "] ", *args)
     end
   end
 end

Rico では、Copland におけるパッケージのような設定ファイルを使わず、コード によってコンポーネント間の関係を決定します。このレポートでは、このようなも のを設定コードと呼ぶことにします。以下のものが copland/sample1.yml に相当 する設定コードです。

 # rico/sample1.rb
 require 'rico/container'
 require 'display'
 container = Rico::Container.new
 container.register_component_implementation(:Display, RLR::Display)

まず、Rico::Container.new によってコンテナを生成しています。 次に、register_component_implementation によって :Display というコンポ ーネントを登録し、このコンポーネントが RLR::Display クラスから生成される ということを定義しています。このコンポーネントを利用するコードは次の通りです。

 # rico/sample1.rb 続き
 display = container.component_instance(:Display)
 display.print("This message is generated by sample1.Display.\n")

component_instance によって :Display として登録されたコンポーネントを獲得 しています。

次に、RLR::Display のインスタンス変数 @filter に Filter オブジェクトを与える 設定コードを書くと、次の通りです。

 # rico/sample2.rb
 require 'rico/container'
 require 'display'
 require 'filter'
 container = Rico::Container.new
 container.register_component_implementation(:Filter, RLR::Filter)
 container.register_component_implementation(:Display, RLR::Display, [:Filter])

まず、:Filter というコンポーネントを登録します。 次に、:Display の登録ですが、このとき第三引数に [:Filter] という配列を与えて います。ここで、:Filter というのは先ほど登録したコンポーネントですが、実体は RLR::Filter オブジェクトです。そのため、RLR::Filter オブジェクト一つから構成 された配列であると見ることができます。:Display というコンポーネント、つまり RLR::Display オブジェクトを生成するときに、この配列の要素が initialize の引数 として与えられます。 この :Display コンポーネントを利用するコードは先ほどと同じなので割愛します。

copland/sample3.yml と同様の設定コードを、以下の rico/sample3.rb として 作成します。:LabelFilter コンポーネントを登録するには、上記の rico/sample2.rb において Filter としていたところを LabelFilter とします。 また、LabelFilter にはラベルの初期値として文字列 “label” を与えます。 この文字列は、String クラスのインスタンスであるため、 register_component_instance メソッドを用いてインスタンスであることを明示して コンテナに登録します。

 # rico/sample3.rb
 require 'rico/container'
 require 'display'
 require 'filter'
 container = Rico::Container.new
 container.register_component_instance(:label, "label")
 container.register_component_implementation(:Filter, RLR::LabelFilter, [:label])
 container.register_component_implementation(:Display, RLR::Display, [:Filter])
 display = container.component_instance(:Display)

最後に、Interceptor をコンテナに登録する方法を以下の rico/sample4.rb を使って 紹介します。Interceptor を登録するには、つまり、メソッドにフックをかけるためには、Rico::Container クラスの代わりに Rico::InterceptingContainer を使い、イン スタンス作成時に Proc オブジェクトを与えます。 ここでは、メソッドコールのログを出力するための簡単な Proc オブジェクトを与えて います。ブロック引数の args には、レシーバーとその引数が配列として格納されてい ます。

 require 'rico/container'
 require 'rico/chainedcontainer'
 require 'rico/interceptingcontainer'
 require 'display'
 require 'filter'
 container = Rico::InterceptingContainer.new(Rico::Interceptor, Proc.new{|*args| p args})
 container.register_component_implementation(:Filter, RLR::Filter)
 container.register_component_implementation(:Display, RLR::Display, [:Filter])
 display = container.component_instance(:Display)

ここで、本来はメソッドコールの前と後ろのどちらにフックをかけるかということを 指定できるべきですが、Rico ではまだ対応できていないようです。

感想

Rico は、非常にシンプルに IoC コンテナを実装しています。しかし、interceptor を Proc オブジェクトとして与えることができる以外は、際立って Ruby らしいところは見 当たりません。 また、ロードするライブラリは、コンポーネントを利用するコードとコンポーネントの 設定コードの双方で必要となりますが、必要なライブラリをロードするコードを設定コ ードとし、利用コードからその設定をロードすることによって、利用コードと設定コー ドを区別することができると思います。

試用レポート – Tudura

登録データ

概要

tudura は、nihohi という blog ツールのために開発されている IoC コンテナです。 Setter Injection を採用しており、コンポーネント間の関係は、XML 形式で記述しま す。以降では、まず作者の TAKAI Naoto 氏のコメントを紹介し、次に、サンプルを通し て簡単に tudura を紹介します。

作者の声

tudura の作者である TAKAI Naoto 氏より以下のコメントを頂きましたので 紹介します。

私が tudura をつくるきっかけは、そのときにつくっていた小さなウェブログツー ルのためのコンポーネントを柔軟にまとめあげるコンテナが欲しいとおもった からです。

私が tudura をつくろうとしたとき、Ruby での Dependency Injection の実装は、 調べた限りで Rico しかありませんでした。 Rico はコンストラクタ・インジェク ションを採用したコンテナですので、動的型言語である Ruby ではあまり便利な ものではありませんでした。というのも、静的型付言語ならばコンストラクタ の型によって、自動的に挿入するインスタンスを決定することができますが、 動的型言語ではそれをすることができないからです。

そこで、Spring Framework に似たセッター・インジェクションのコンテナを作 成してみようとおもいたったのです。セッター・インジェクションを採用する と、外部設定ファイルに記述されるインスタンスと、頭の中に描かれるインス タンスとの対応がとりやすいため、モノ感にあふれる Ruby にはぴったりだと考 えました。

とはいえ、tudura は一通りの動作ができるところまでは実装したものの、飽 きっぽい性格が災いして、その後の開発をやめてしまいました。残念ながら、 ソフトウェア開発に自分の時間を割くことが、あまりできないようになってし まいましたので、tudura のこれ以上の発展はないかとおもっています。Ripper などのパーサを利用して、変数名にもとづくインジェクションを行うなど、 色々とアイデアはあるのですが……。

私は Dependency Injection を、単なるデザインパターンのうちのひとつとい うよりも、オブジェクト指向の思想を推し進める手法のひとつであると考えて います。ぜひ、皆さんの道具箱のなかに、Dependency Injection を加えてみて ください。Dependency Injection を利用することによって得られるテスタビリ ティやフレキシビリティは、私たちプログラマーにとって、きっと強力な武器 になるはずです。

残念ながら、tudura の開発は現在のところ止まっているようです。しかし、 SourceForge.JP にソース などが一式揃っていますので、SourceForge.JP のアカウントがあれば、すぐにで も他の方が開発を引き継ぐということも可能だと思います。その際には、TAKAI 氏 のアイデアも引き継いで頂ければと思います。

サンプル

tudura は、Setter Injection を採用しているので、Copland のサンプルで用いた display.rb と filter.rb を用いることにします。 以下は、このうち display.rb に定義されている Display クラスからオブジェクトを 獲得するための設定ファイルです。

 <?xml version="1.0" encoding="UTF-8" ?>
 <context>
   <object id="Display" class="RLR::Display" />
 </context>

id 属性が、コンポーネントを識別する名前になります。この設定ファイルの場合、 RLR::Display オブジェクトに対して Display というコンポーネント名を与えています。 これを利用するコードは、次の通りです。

 require 'tudura'
 require 'tudura/xmlconfig'
 require 'display'
 xml = File.open("sample1.xml"){|f| f.read()}
 container = Tudura::XMLContext.new(xml).configure
 display = container.fetch(:Display)
 display.print("This message is generated by sample1.Display.\n")

まず、変数 xml に sample1.xml の内容をロードしています。この段階では、設定 ファイルは、まだ Tudura によって解釈されていません。 Tudura::XMLContext.new(xml).configure というコードによって設定ファイルが 解釈され、コンテナを作成しています。作成したコンテナから Display コンポーネン トを獲得するには、fetch メソッドを使います。

次の XMLファイルは、Filter オブジェクトを Display オブジェクトの @filter に 関係付けるための設定ファイルです。

 <?xml version="1.0" encoding="UTF-8" ?>
 <context>
   <object id="Display" class="RLR::Display">
     <property name="filter"><ref object="Filter" /></property>
   </object>
   <object id="Filter" class="RLR::Filter">
   </object>
 </context>

これを利用するコードは次の通りです。

 # tudura/sample1.rb
 require 'tudura'
 require 'tudura/xmlconfig'
 require 'display'
 require 'filter'
 xml = File.open("sample2.xml"){|f| f.read()}
 container = Tudura::XMLContext.new(xml).configure
 display = container.fetch(:Display)
 display.print("This message is generated by sample2.Display.\n")

Copland とは異なり、設定ファイル中でどのファイルに対して require を行うかとい うことは記述していませんので、利用するコード側で display.rb と filter.rb を ロードする必要があります。

次に、LabelFilter を用いる場合の設定ファイルは次の通りです。 LabelFilter のインスタンス変数 @label に与える文字列は、<value>label</value> として与えています。

 <?xml version="1.0" encoding="UTF-8" ?>
 <context>
   <object id="Display" class="RLR::Display">
     <property name="filter"><ref object="Filter" /></property>
   </object>
   <object id="Filter" class="RLR::LabelFilter">
     <property name="label"><value>label</value></property>
   </object>
 </context>

これを利用するコードは以下の通りで、tudura/sample1.rb と変わりはありません。

 # tudura/sample2.rb
 require 'tudura'
 require 'tudura/xmlconfig'
 require 'display'
 require 'filter'
 xml = File.open("sample3.xml"){|f| f.read()}
 container = Tudura::XMLContext.new(xml).configure
 display = container.fetch(:Display)
 display.print("This message is generated by sample3.Display.\n")

最後に、Interceptor を使ったサンプルを挙げます。Interceptor を実現するため には Tudura::InterceptorProxy というプロキシーを用います。つまり、これまでの Display というコンポーネントのクラスを、Tudura::InterceptorProxy とし、 interceptors という属性に Logger と DisplayImpl を登録し、これらを順に 呼び出すようにします。 ここで、DisplayImpl が、これまでのサンプルで Display としていたものです。

 <?xml version="1.0" encoding="UTF-8" ?>
 <context>
   <object id="Logger" class="Logger" />
   <object id="DisplayImpl" class="RLR::Display">
     <property name="filter"><ref object="Filter" /></property>
   </object>
   <object id="Display" class="Tudura::InterceptorProxy">
     <property name="interceptors">
       <array>
         <ref object="Logger" />
         <ref object="DisplayImpl" />
       </array>
     </property>
   </object>
   <object id="Filter" class="RLR::LabelFilter">
     <property name="label"><value>label</value></property>
   </object>
 </context>

ここでは、Logger というクラスは、利用する側のコードとまとめて定義することに しますが、別ファイルで定義を行い、require を行っても良いでしょう。 そのため、上記の設定ファイルを利用するコードは次の通りです。

 require 'tudura'
 require 'tudura/xmlconfig'
 require 'tudura/interceptor'
 require 'display'
 require 'filter'

 class Logger
   include Tudura::Interceptor
   def intercept(name, args, chain)
     p [name, args, chain]
     chain.proceed(name, args)
   end
 end

 xml = File.open("sample4.xml"){|f| f.read()}
 container = Tudura::XMLContext.new(xml).configure
 display = container.fetch(:Display)
 display.print("This message is generated by sample4.Display.\n")

Logger では、intercept メソッドを定義しています。このメソッドは、先程のプロ キシーに対して呼ばれたメソッド名、その引数、次の Interceptor への参照のような ものを渡されます。 tudura では、この参照を InterceptorChain と呼んでいます。 chain.proceed(name,args) は、InterceptorChain を用いて、次の Interceptor の同じメソッドを、同じ引数を用いて呼び出すコードです。このようにして、メソッド を順次呼び出し、利用するコードから見ると、あたかもメソッドの実装にフックがか かったような動作を行います。

感想

tudura は、nihohi という blog ツールのために書かれた IoC コンテナです。 nihohi のプロジェクトホームページ が、 nihohi を用いて構築されているようです。このため、実際に動く Web アプリケー ションのデモがあり、現実的に使えるレベルの仕上がりになっていることと思います。

tudura では、設定は XML ファイルで行うものの、その設定ファイル内で用いた クラスは、利用コード側で再び require を行わなければならないようです。 このため、利用コード内に、設定要素が含まれてしまい、コンテナの機能として 利用コードと設定の二つを明確に分けることが難しくなっていると思います。 このことは、一貫性を保つ手間が増し、依存性の分離という IoC の利点を守れてい ないと著者は考えます。

まとめ

今回は、IoC コンテナである Copland、Rico、tudura を紹介しました。

必要なライブラリ (クラスを定義したファイル) のロードの仕方については、 tudura は、利用コード側に require を明示的に記述しなければならず、利用 するオブジェクト (コンポーネント) に応じて適切なライブラリを予めロードしてお く必要があります。このため、設定ファイルと利用コードの間で一貫性をとる必要が あります。 Rico も同様に require を行うライブラリをコード中に記述しなければなりません が、設定コードと利用コードを、ユーザが区別して記述していれば、設定コードの中 で一貫性をとることができるでしょう。 一方、Copland では、設定ファイル中で、require すべきファイル名なども含めて コンポーネントを指定するので、設定ファイル内で一貫性をとることができます。

動作の実績という観点では、 Copland と tudura では、RAA:webrick などとの連携もテストされ ている様子があり、Rico に比べて利用実績があると言えると思います。 今号には、「WEBrick によるコンテンツフィルタ」という記事もありますので、 そちらを参考にして、フィルタをコンポーネント化し、再利用可能かつ組み合わせ も可能なようにすると面白いでしょう。そして、編集の立場からのお願いですが、 機会があれば、その開発内容を寄稿してください。

Interceptor の使い勝手については、tudura と Rico が簡単です。 tudura では、明示的に Proxy オブジェクトを作成しますが、このようなメカニ ズムは本来は IoC ライブラリとして隠蔽すべきだと著者は考えます。また、Rico では Proc オブジェクトを用いることによって簡単に実現していますが、メソッド コール前にフックをかけることしかできません。 Copland の Interceptor は、自ら定義するには多少面倒でしたのでサンプルに 含めず用意されていたものを使いました。予め必要なものを Copland 自身で提供 するという方針なのかもしれません。 いずれの IoC コンテナにおいても、Interceptor と同様の機能は、AOP を支援 する RAA:AspectR などを用いても実現できますので、すでに AspectR の経 験がある人はこちらの方が簡単で使い勝手が良いかもしれません。

また、それぞれの IoC コンテナ自身は、分散化やデータアクセスなどの機能を提供 していません。 しかし、dRuby や SOAP4R などと連携を図り、開発者自身でそれらを制御すること が可能です。先に書いたように、WEBrick と連携すれば、自分で Web アプリケーシ ョンサーバを作ることもできます。 すでに完成したアプリケーションサーバを用いるよりも手間は多少かかると思いま すが、コンポーネントへの制約に束縛されることがなくなります。

終わりに

さて、初回の RLR はいかがでしたか? このくらいのことでよければ情報を提供したいという方がいらっしゃれば是非寄稿 をお願いします。私でよければ調査や校正のお手伝いなどをします。 RLR に対する私個人の趣向としては、それぞれのライブラリを深く観察するのでは なく、広く浅くサーベイを行うことを考えています。もちろん、広く深くというの は大歓迎ですし、一つのマイナーなライブラリを深くという内容もご相談ください。 寄稿などの連絡は、編集部 へお送りください。

また、本レポートで用いたサンプルソースコードは、 0002-RLR-0001-SRC.zip にありますので、必要があればダウンロードして使ってください。

参考資料

[1] IoC についての概要をより詳しく知りたい方は、 『Inversion of Control Containers and the Dependency Injection pattern』 (原文) (和訳) を読むと良いでしょう。

[2] HiveMind は、Coplandが参考にした実装です。

[3] ‘『expert one-on-one – J2EE Development without EJB』’ は、EJB が解決対象とする問題を、EJB を用いずに実現する方法を考察しています。

[4] YAML は、プログラム言語のデータ型に、シームレスに 対応したマークアップ言語です。

[5] YAML4R は、Ruby 版の YAML の 実装です。ruby-1.8 では、標準添付ライブラリとなっています。

[6] YAML Cookbook は、 YAML でどのようなものが記述可能か知ることができるでしょう。

[7] AspectR は、 AOP を簡単に実現するための Ruby のライブラリです。

[8] PicoContainer は、 Setter Injection を採用した Java 版 IoC コンテナです。Ruby 以外にも、いくつ かの言語に移植されています。

[9] IPSJ SIGSE パターンワーキンググループ 第 8 回勉強会 の資料では、DI と GoFデザインパターン との関連性を 考察しています。

[10] Seaser は、オープンソース の Java 版 IoC コンテナです。開発が日本を中心に行われているため、比較的 日本語の情報が得やすいでしょう。

[11] 『コンテナを使わないオブジェクト・リレーション・マッピング』 は、Hibernateと IoC コンテナを含む Spring フレームワーク を使ってトランザクショナル・パーシ スタンス・レイヤーの開発方法を簡単に説明します。

[12] 『Springフレームワークの紹介』 (和訳) (原文) では、Spring フレームワークの中核技術として、IoC コンテナを説明しています。

著者について

著者 (立石) は、ソフトウェアに関する研究開発職に従事しています。

Ruby Library Report 連載一覧


  1. 作者の意向により、tudura はすべて小文字表記です。 

  2. コンポーネントとコンテナが良く分からない方は、コンテナを、実行環境やアプリケーションサーバ、コンポーネントを、その上で動くソフトウェア部品、つまり単独では意味を持たないが、他のコンポーネントやコンテナと組み合わさることで役割を発揮するオブジェクトを想像してください。 

  3. コンテナにコンポーネントを導入することです。 

  4. コンポーネントとオブジェクトという用語には、明確に区別は無いと思いますが、本レポートでは、設定ファイルや設定コードのコンテキストではコンポーネントという用語を用いて、通常のプログラミングのコンテキストではオブジェクトを用いています。