本ドキュメントで述べられているアプローチは、Railsに取り込むかどうかが検討されていた。しかしながらデイビッドは、外部のDIフレームワークへの依存性を高めるのではなく、ここで記述されている各ポイントを分けて、ソリューションを実装することに決めた。また、彼のソリューションは、下記のNeedle
ベースのアプローチよりはRailsにマッチする。下記で議論されている各問題に対する既存のRailsのソリューションをお見せしよう。
依存性注入(Dependency Injection,略してDI,他に、"制御の反転"(inversion of control、略してIOCとして知られる)は、数多くの小さくアジャイルでお互いに疎結合なコンポネントの生成を促す強力なデザインパターンである。このパターンは、別のところ(*1)で詳細に記述されているので、本稿では、一般的なDIの利点については議論しない。代わりに、本稿ではDIがウェブアプリケーションフレームワーク、特にRuby on Rails(*2)(あるいはもっと単純に、"Rails")にもたらす利点について注目する。
Ruby用に実装されたDIはいくつか存在するが、本稿ではNeedle(*3)を扱う。
Railsの現在の実装(これを書いている時点ではバージョン0.8)では、実行時のコンフィグを定義するデータを特定する手段を提供する。このコンフィグはenvironments
といい、プライマリのデータベース接続情報が含まれている。
現時点の実装では、environments
は、2,3の異なるクラスにプロパティを設定し、各環境ごとに適切なデータベース設定をロードする簡単なRubyスクリプトとして実装されている。よって、"production"環境を用いるアプリケーションには、単に"production.rb"スクリプトが必要になるだけだ。
下記の概念実証のための実装ではenvironment
の機能はシングルトンオブジェクトでカプセル化してある。また、いくつかのRailsコンポネントにメソッドを追加している。というのは、Railsコンポネント(モデル、コントローラ、あるいはビュー)は、常にenvironment
のDIコンテナへ便利なアクセスがあるからだ。
この概念実証の実装には2つのファイルが含まれていて、両方ともRailsの全てを結ぶ(つまり、配布可能な"railties")コードの一部になる。
最初のファイルは、ActiveRecord
、ActionController
、ActionView
、そしてActionMailer
モジュールの基本クラスを拡張したものだ。これらはレジストリへの参照をインクルードするように拡張されている。
require 'active_record' require 'action_controller' require 'action_mailer' require 'action_view' # レジストリインスタンスを含むクラスの振る舞いを定義するモジュール module RegistryContainer # このクラスや全サブクラスに影響するレジストリインスタンスを返す def registry self.class.registry end # #registry=, #reistry, #register メソッドはクラス変数を使うので、 # このメソッドによって、動的に追加されなければならない def self.included( mod ) super mod.class_eval <<-EOF, __FILE__, __LINE__+1 def self.registry=( reg ) @@registry = reg end def self.registry @@registry end def self.register( name ) registry.register( name ) { self } end def self.service( *args ) base = registry base = args.shift unless String === args.first || Symbol === args.first if args.empty? raise ArgumentError, "specify a service to reference" end args.each do |model| const_set Inflector.camelize(model.to_s), Needle::Lifecycle::Proxy.new { base[model.to_sym] } end end EOF end end class ActiveRecord::Base include RegistryContainer class << self alias :old_inherited :inherited # 自動的にこのクラスをサービスとしてクラスのレジストリに追加する # このサービスは、クラス名にunderscoreして、demodulizeしてsymbolizeしたもの def inherited( klass ) old_inherited klass klass.register( Inflector.underscore( Inflector.demodulize( klass.name ) \ ).to_sym ) end end end class ActionController::Base include RegistryContainer end class ActionMailer::Base include RegistryContainer end class ActionView::Base include RegistryContainer end
特に、ActiveRecord::Base
クラスは全てのサブクラスが自動的にレジストリに登録されるように拡張されている。この拡張は必要なものではないが、便利なので。でも、もし開発者がレジストリのARに追加したいのであれば、下記のように手動で行う必要がある:
class MyActiveRecord < ActiveRecord::Base register :my_active_record ... end
これは、例えば、もしコントローラやビューが(いかなる理由であれ)DIコンテナで自分自身を登録したい場合に採られるアプローチだ。
self.service
メソッドにも注目してほしい。これにより開発者は使おうとしているサービスを(名前で)特定することができ、サービスにアクセスするのに定数を利用することができる。ここで、あるメール通知サービスが:email_notification
という名前でレジストリに登録されているという状況を想定してみよう。そしてあるコントローラが(たとえば)以下を行うことでこのサービスにアクセスできた。
class FooController < ActionController::Base # サービスを宣言する service :email_notifier def save if something_failed # 定数をつかってサービスを起動させる EmailNotifier.send_notification( "something failed!" ) raise "something failed!" end ... end ... end
次のセクション、"Environment"ではサービスの宣言と起動に関するもっと便利なメソッドをみてみよう。
概念実証の次は、environment
シングルトンそのもののの実装だ。これはさっきのファイルより若干複雑になる。これは以下のようなものだ。
Needle::Registry
(Rails::Registry
)のサブクラスを定義する。
environment
を選択する手段を提供するrequire 'singleton' require 'active_record' require 'action_controller' require 'action_mailer' require 'action_view' require 'yaml' require 'needle' require 'base-enhance' module Rails # Needleレジストリクラス簡単なサブクラス # コンポネントに特化した名前空間を簡単に追加できるようにする class Registry < Needle::Registry # 指定した名前の名前空間が存在しなければ生成する。 # とにかく名前空間を返す def component_space( name ) unless has_key?( name ) namespace name end self[ name ] end end # Railsのenvironment管理をカプセル化するシングルトンクラス class Environment include Singleton # 追加するロードパスを加える配列 ADDITIONAL_LOAD_PATHS = %w{app/models app/controllers app/helpers config lib vendor} # 利用するレジストリへのリファレンス attr_reader :registry # 新規のEnvironmentを生成する。シングルトンクラスなので、 # 直接は呼び出せない。シングルトンインスタンスが生成されたら1回しか起動できない。 def initialize # ハッシュの各値に対し#replaceを実行することにより、$:変数を簡単に # 更新できるようにロードパスのハッシュを生成する # (Environment#setを参照のこと) @load_paths = ADDITIONAL_LOAD_PATHS.inject( Hash.new ) do |h,k| $:.unshift( h[k] = "" ) h end @registry = Rails::Registry.new :logs => { :device => STDOUT } # アプリケーションのルートディレクトリを登録する。 # デフォルトは、CWD @registry.register( :application_root ) { Dir.pwd } # ファイルを再ロードしなくても参照できるようにデータベース設定をサービスにロードする。 # これにより、クライアントがデータベース設定サービスを再定義するだけで、 # データベース設定をオーバライドできるようにする @registry.register :database_configurations do |c,p| YAML::load(File.open("#{c.application_root}/config/database.yml")) end # カレントのデータベース設定を返す。'prototype'モデルを使うので、、 # ブロックは各リクエストに対し実行される。さもないと # current_environmentが変更されても、database_configurationサービスに # 変更が反映されない @registry.register( :database_configuration, :model => :prototype ) do @registry.database_configurations[ @registry.current_environment ] end # カレントのシステムログファイルの位置を返す。'prototype'モデルを使うので、 # ブロックは各リクエストに対し実行される。さもないと、 # current_environmentが変更されても、system_log_fileサービスに変更が反映されない # # これにより、クライアントはログが吐かれる場所をsystem_log_fileサービス # を登録するだけで変更できるようになる @registry.register( :system_log_file, :model => :prototype ) do |c,p| "#{c.application_root}/log/#{c.current_environment}.log" end ActiveRecord::Base.registry = @registry.component_space :model ActionController::Base.registry = @registry.component_space :controller ActionMailer::Base.registry = @registry.component_space :mailer ActionView::Base.registry = @registry.component_space :view ActiveRecord::Base.logger = @registry.logs.get "[active-record]" ActionController::Base.logger = @registry.logs.get "[action-controller]" ActionMailer::Base.logger = @registry.logs.get "[action-mailer]" end # 新規のenvironmentを名前により選択する。唯一の制限は、デフォルトで # 指定された名前のDBのコンフィグが存在しないといけないこと、 # ないと失敗してしまう。 # # +app_root+ パラメータの指定あれば、プロジェクトのルートディレクトリである。 # 全アプリケーションコンポネントはこのディレクトリからの相対パスで参照される。 def set( environment_name, app_root=nil ) @registry.application_root.replace( app_root ) if app_root @load_paths.each do |k,v| @load_paths[k].replace "#{@registry.application_root}/#{k}" end ActionController::Base.template_root = ActionMailer::Base.template_root = "#{@registry.application_root}/app/views/" @registry.register( :current_environment ) { environment_name } @registry.logs.write_to(@registry.system_log_file) ActiveRecord::Base.establish_connection(@registry.database_configuration) true end # environmentを設定するための便利なメソッド。 # # Environment.instance.set "production" の代わりに # # # Environment.set "production" みたいなことができる。 # def self.set( environment_name, app_root=nil ) instance.set( environment_name, app_root ) end # A convenience accessor for accessing the Rails registry. This allows you # Railsレジストリにアクセスするための便利なアクセサ。 # # Environment.instance.registry の代わりに # # Environment.registry みたいなことができる。 # def self.registry instance.registry end end module BaseConvenienceMethods def self.included( mod ) super mod.extend ClassMethods end module ClassMethods def model( *args ) service registry[:model], *args end def mailer( *args ) service registry[:mailer], *args end end end class ActiveRecord::Base include BaseConvenienceMethods end class ActionController::Base include BaseConvenienceMethods end class ActionView::Base include BaseConvenienceMethods end class ActionMailer::Base include BaseConvenienceMethods end end
全てのアプリケーション用にいろんなサービスを自動で登録することに注意するように。
:application_root | これはアプリケーションのルートディレクトリを指定する。デフォルトはCWDだが、Environment#set を呼べば、別のディレクトリを指定できる。 |
:database_configurations | これはわかっているデータベースコンフィグレーションのハッシュであり、(アプリケーションルートディレクトリからの相対パスで)config/database.yml ファイルからロードされる。 |
:database_configuration | これは、その時点で選択されたデータベース設定であり、その時点で選択されているenvironment に基づいている |
:system_log_file | これは書き出されているログファイルのファイル名である。 |
:current_environment | これはその時点で選択されているenvironment 名である。 |
特に、:application_root
サービスによりアプリケーションはFile.dirname(__FILE__)
みたいなことをしなくても自分のルートディレクトリを発見することができる。
:application_root
のデフォルトをCWDに設定することでEnvironment
はirbでうまく動く。単にirbを起動して、require 'env'
して、Rails::Environment.set
を使って使いたい環境を設定する。プロジェクトのルートでirbを起動するのと同じように、ルートディレクトリを明示的に設定したり、ディレクトリを明示的にロードパスに追加する心配をする必要はない。
最後に、ファイルの最後の方のBaseConvenienceMethod
モジュールにも注目してほしい。このモジュールは、Railsの各プライマリコンポネントにインクルードされるので、modelコンポネントやmailerコンポネントへ簡単にアクセスできる。系統情報を表示するようなアプリケーションを想定してほしい。
require 'active_record' require 'pedigree' class PedigreeController < ActionController::Base model :pedigree def show @pedigree = Pedigree.load( @params['id'], @params['generations'].to_i ) end ... end
これで簡単に(もっと大事なことは、後方互換性を保ちつつ)モデルサービスを定数のように参照することができる。
先述の2つのファイルをいったんいっしょにしてしまえば、config/environments
ディレクトリにproduction.rb
とtest.rb
を作れば、全部後方互換性を保持できる。例えば、これは、production.rb
スクリプトの完全版だ。
require 'env' Rails::Environment.set "production", File.dirname(__FILE__)+"/../.."
ここでやってることは"production"環境を選ぶことと、明示的にアプリケーションルートを設定してるだけだ。(これで、実際のウェブ環境で正しく動作する。)test.rb
スクリプトは"production"が"test"に置き換わっただけでほとんど同じものだ。
前章で触れた実装にはいくつかの利点がある。特に、テストが簡単、バックエンドの実装が柔軟にできる、サービスが共有できる、メソッドいんたせぷしょん、そして設定n柔軟性だ。
Railsではすでに、モデル、コントローラ、そしてビューを含む、アプリケーション中の全てのパーツのための強力なユニットテストのためのフレームワークが提供されている。しかしながら、今の実装では、コントローラとモデル間は密接に絡み合っているので、別の実行コードやモデルからルーチンを使わないでコントローラのユニットテストを実施することはできない。それらはテストされる機能とは大部分は無関係にある。
注:Railsでは、もはやモデルとコントローラ間を密接につなげることはしない。後述するように、コントローラをテストする際に簡単にモックオブジェクトを注入することができるmodel
キーワードをサポートしている。
モデルをコントローラから切り離すことにより、開発者はユニットテストを行う際に、実際のモデルの代わりにモックのARを代用するすることができる。モックARはコントローラで必要となる機能のサブセットを実装する必要があるだけで、ハードコーディング(あるいはスクリプティング)されたデータを返すことができ、テストにより一貫性をもたらし、必要ないルーチンへの依存性を減らすことができる。
下記の例を検討してみよう。これは、本を記録するオンラインアプリケーションだ。これはアプリケーションが知っている著者のリストを表示する機能を実装するあるモデル/コントローラの断片である。
class Author < ActiveRecord::Base ... end
class AuthorsController < ActiveController::Base model :author def list @authors = Author.find_all end ... end
上述の例では、コントローラで使われるAuthor AR
のインタフェースは#find_all
だけだ。したがって、ユニットテストでは、開発者はこのメソッドを実装してAuthor
のモックを返すようなオブジェクトが1つ必要なだけだ。(実際のAuthor
は使えない。というのは、ARをインスタンス化するにはコネクションを必要とするからだ。また、こういったことを必要なくすことがこの目的だ)。
class AuthorsControllerTest < Test::Unit::TestCase MockAuthorService = Struct.new( :find_all ) MockAuthor = Struct.new( :name, :books ) def setup @controller = AuthorsController.new @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new AuthorsController.registry.namespace_define! :model do author do MockAuthorService.new( # 複数のbook,1つのbook, bookがない場合をテストする。 [ MockAuthor.new( "Robert Jordan", [ "Eye of the World", "The Great Hunt" \ ] ), MockAuthor.new( "Roger Zelazny", [ "Nine Princes in Amber" ] ), MockAuthor.new( "Katherine Kurtz", [] ) ] ) end end end def test_list ... end end
もし、モックARを作って登録するのにある種のscaffoldのインフラを書けば、これはもっと簡単にできるだろう。このように、フィクスチャを作るのは少しやっかいだけど、機能をを確実にする。
注:今のRailsでは、モックの実装をtest/mocks/testing
ディレクトリに置けばユニットテストで簡単にモックモデルオブジェクトを使うことができる。Railsのテスティングフレームワークは自動的にモックオブジェクトを見つけ出して利用する。
ほとんどのRailsアプリケーションではActiveRecord
をモデルの実装として利用するが、開発者は非SQLなバックエンド(つまり、フラットファイルへのデータストア)と、(ActiveRecord
を介した)SQLバックエンドを両方とも扱いたいと思うだろう。
アプリケーションでは、モデル以外ではSQLを使わないように設計されていると想定されているが、求められているのは、モデルの実装をアプリケーションの内外に取り外す、ということだ。
レジストリはこれができるようにするための理想のレベルの抽象になる。例えば、レジストリにあるActiveRecord
のモデルクラスを登録する代わりに、非SQLバックエンドのモデル実装を登録しても、コントローラにはそれがわからないので、インタフェースと対話することしかせず、処理方法がわかっているとみなすオブジェクトにメッセージを送る。
下記のコード断片を検討してみよう。これは、先ほどのAuthor
クラスのフラットファイルバージョンを実装する擬似コードだ。(このFlatFileSystem::Base
クラスは想像上のクラスで、ActiveRecord::Base
の基本的な機能のうちのいくつかを真似たもの、と仮定している。register_self_as
メソッドは単に自分自身を名前付きのサービスとしてモデルレジストリに登録する。)
require 'yaml' class AuthorFlatFile < FlatFileSystem::Base register_self_as :author def self.find_all authors = [] Dir["#{file_root}/authors/*.yml"].each do |file| attrs = YAML.load(File.read(file)) authors << new( attrs ) end authors end def initialize( attrs={} ) ... end end
ここで重要なのは、インタフェースだけだという点には注目してほしい。Author
のように振舞う限り、このコントローラは実際にどのように実装されているかについては気にしない。
Needle
をサービスロケータとして使えば、アプリケーションの全てのレベル間でサービス簡単に共有されるかもしれない。このように共有されるサービスの1つはログメッセージを書き込むためのロガーだ。実際、Needle
はこの目的のために統合されたロギングサブシステムに付随する。ロガーへのハンドルを取得するには、:logs
サービスを問い合わせる。
class Book < ActiveRecord::Base ... private def after_save log.info "Book #{name} was just saved" end def log @log ||= registry.logs.get( "Book" ) end ... end
class BookController < ActionController::Base ... def delete unless has_access_to_delete log.warn "Someone tried to delete a book without authorization!" raise InvalidAuthorization, "you can't do that!" end ... end private def log @log ||= registry.logs.get( "BookController" ) end ... end
このlogs
サービスは一度インスタンス化され、その1つのインスタンスが、透過的に異なるコンポネント間でその後共有される。(インスタンス化されるのが1つだけというのは、はNeedle
がデフォルトですべてのサービスに対するシングルトンの多重化一貫性に従うということによる)。
注:ロギングは悪い例だ。というのは、Railsではロギングがちゃんとサポートされているからだ。さらに、最近のRailsのリリースでは、ここで述べたのと同じ方法でsystem-global
サービスを宣言し使うために、service
キーワードがサポートされている。
Needle
のようなDIコンテナを使う他の利点は、AOP風のアドバイスをレジストリ内の任意のサービスに追加できるようになる点だ。これにより、デバッグでメソッドを叩いたときにどんなパラメータを持っているかトレースが取得するなど他にも役に立つ。
Needle
でこのような構築はインタセプタと呼ばれる。例えば(各メソッドの起動をパラメータの値と合わせてロギングするために)ロギングインタセプタをサービスに追加するにはあなたのコードのどこかで以下のようにするだけでよい。
... Rails::Environment.registry. intercept( :author ). with { |c| c.logging_interceptor } ...
たったこれだけだ。一度これを追加してしまえば、:author
サービスにある任意のメソッドの任意の起動はパラメータ値、返り値、発生した例外の記述のすべてがロギングされるようになる。
メソッドインターセプションにより、ユーザのクリデンシャルに基づいてメソッドへのアクセスを制限する、のようなことができるようになる。メッセージを期待したフォーマットで入出力を行うアダプタのように動く、あるいは引数を分析したら、全く別のところでメッセージを処理すべきあである場合に全く異なるレシーバへメッセージの起動をリダイレクトするといったこともできるようになる。
文字列や配列、ハッシュなどのような「スタティックな」オブジェクトを含め、どんな種類であれオブジェクトでもレジストリに投入するのは簡単だ。なので、レジストリはアプリケーションのどこからでもアクセスできるようにしたいコンフィグレーション情報を格納するのに理想的な場所なのである。
このようにレジストリをコンフィグレーション置き場として使ったものがアプリケーションのためのコンフィグレーションデータベースである。どんなアプリケーションでも、データベースへ接続するのに使う値のハッシュを取得するのに:database_configuration
サービスを参照するようにすることができる。
このコンフィグレーションをレジストリに入れるようにすることで、アプリケーションのどんな部分でも簡単にコンフィグレーションを切り出すことができるようになる。例えば、あるオブジェクトは自分自身をあるイベントの通知に関心のあるグループの配列に追加することができる。するとそのイベントの主たるオブザーバはその配列をレジストリから読み込み、イベントが発生すると、配列に格納されている各オブジェクトそれぞれにノーティフィケーションを送信する。
レジストリに格納することが可能なこれとは別の種類のコンフィグレーション情報は、テンプレートファイルや、ログファイル、コントローラの実装のようなものの場所だ。
注:Railsではこのような情報を格納するのにenvironments
を使う。
これまで述べてきたように、本稿で取り上げた実装は概念の実証にすぎないし、ここで取り上げただけでなくもっといろんなことが可能だ。
例えば以下のようなものがある。
ActiveView
を切り離し、(デフォルトだけど)オプションのコンポネントにできるようにする。これにより他のテンプレートフレームワークをActiveView
の代わりに使えるようにする。ここに挙げたすべてが望ましいわけではない。ポイントは、こうすべきであるということではなく、これまでトリッキーに実装されてきたが、依存性注入の到来とサービスロケーションにより、実現可能になっている、ということである。
よりデータドリブンな方法でカレントの環境をアプリケーションに指定するためのよりよい方法があるかどうかを判断するためにconfig/environments
ディレクトリにより多くの考慮を含めることができるようになった。例えば、development/staging/production
の3つの環境システムを指定することができるようになったことは望ましいことだと思う。次のレベルにコードを更改するたびに毎回カレントの環境を参照するディスパッチャコードを変更しないといけないとなるとバグの温床にならないように気をつけないといけない。
依存性注入は、いろんなコンポネントを使ったアプリケーションを構築するのに大変便利なものだ。Railsは複雑な状況にうまくスケールするように設計されているが、Raisアプリケーションも例外ではない。
ここでおススメした実装方法は、もし開発者が使いたくないのであれば、邪魔にはならないし、使いたいのであれば、不便さを解消する、そういうものである。
また、この実装では、(前述したように)考慮すべきことがらは増えるが、config/environments
ディレクトをきれいにする。