本ドキュメントで述べられているアプローチは、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ディレクトをきれいにする。