The Benefits of Dependency Injection in a Web Framework
Jamis Buck ([email protected]), 1 Feb 2005, rev 3
The approach described in this document was considered at one point for inclusion in Rails. However, David decided to implement solutions separately to each of the points described here, rather than introduce a dependency on an external DI framework, and truth be told, his solutions mesh better with Rails than the Needle-based approach below. Where applicable, the existing Rails solution to each problem described below will be demonstrated.
Dependency injection (DI, also known as “inversion of control”, or IOC) is a powerful design pattern that encourages the creation of many small, agile components, coupling them loosly together. This pattern has been described elsewhere1 in sufficient detail, so this article will not discuss the general benefts of DI. Instead, this article will focus on what benefits DI brings to web application frameworks, and specifically to Ruby-on-Rails2 (or, more simply, “Rails”).
Although there are several DI implementations available for Ruby, this article will use Needle3.
Rails in its current implementation (which, as of this writing, is at version 0.8) supports a means of specifying data that define any of several runtime configurations. These configurations are called environments, and the supporting data comprises primarly the database connection information.
In the current incarnation, environments are implemented as simple Ruby scripts that set properties on a few different classes and load the appropriate database configuration for each environment. Thus, for an application to use the “production” environment, it simply does a require
on the “production.rb” script.
The following proof-of-concept implementation encapsulates the functionality of the environment in a singleton object. It also adds methods to the various components of Rails so that a Rails component (be it model, controller, or view) will always have convenient access to the environment’s DI container.
This POC implementation consists of two files, both of which would be part of the code that ties all of Rails together (i.e., the “railties” distributable).
The first file is used to enhance the Base
classes of the ActiveRecord
, ActionController
, ActionView
, and ActionMailer
modules. They are enhanced to include a reference to a registry.
base-enhance.rbrequire 'active_record' require 'action_controller' require 'action_mailer' require 'action_view' # A module that defines the behavior of a class that may contain a registry # instance. module RegistryContainer # Return the registry instance in effect for this class and all subclasses. def registry self.class.registry end # The #registry=, #registry, and #register methods must be dynamically # added by this method because they use class variables. 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 # Automatically add this class as a service to the class's registry. The # service will be an underscored, demodulized, symbolized version of the # class name. 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
In particular, note that the ActiveRecord::Base
class is enhanced so that every subclass is automatically registered with the registry. Although this particular enhancement is not necessary, it is convenient. Otherwise, if a developer wanted to add an AR in the registry, they would have to do so manually, like this:
register-manually.rbclass MyActiveRecord < ActiveRecord::Base register :my_active_record ... end
This is the approach that would be taken, for instance, if a controller or view wished (for whatever reason) to register itself with the DI container.
Note, also, the self.service
method. This allows developers to specify (by name) that a service is going to be used, and then use a constant to access the service. Consider a hypothetical situation where some email notification service has been put in the registry under the name :email_notification
. A controller (for instance) could then access that service by doing the following:
constants-as-services.rbclass FooController < ActionController::Base # declare service service :email_notifier def save if something_failed # consume service via constant EmailNotifier.send_notification( "something failed!" ) raise "something failed!" end ... end ... end
See the next section, “Environment”, for more convenience methods related to declaring and consuming services.
The next piece of the proof-of-concept is the implementation of the environment singleton itself. This is a bit more sophisticated than the last file. It does the following things:
Needle::Registry
(Rails::Registry
).env.rbrequire 'singleton' require 'active_record' require 'action_controller' require 'action_mailer' require 'action_view' require 'yaml' require 'needle' require 'base-enhance' module Rails # A simple subclass of the Needle registry class, to make it easier to add # component-specific namespaces. class Registry < Needle::Registry # If a namespace does not exist with the given name, create it. Regardless, # return the namespace. def component_space( name ) unless has_key?( name ) namespace name end self[ name ] end end # A singleton class encapsulating the management of environments in Rails. class Environment include Singleton # The array of additional load paths to add. ADDITIONAL_LOAD_PATHS = %w{app/models app/controllers app/helpers config lib vendor} # A reference to the registry to use. attr_reader :registry # Create a new Environment. Since this is a singleton class, this cannot # be called directly, and will only be invoked once when the singleton # instance is created. def initialize # Create a hash of the load paths so that we can easily update the $: # variable by doing a #replace on each of the hash values. (See # 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 } # Register the root directory of the application. This defaults to the # current working directory. @registry.register( :application_root ) { Dir.pwd } # Load the database configurations into a service, so they can be # referenced and rereferenced without reloading the file. This also allows # clients to override how database configurations are defined, simply by # redefining the database_configurations service. @registry.register :database_configurations do |c,p| YAML::load(File.open("#{c.application_root}/config/database.yml")) end # return the current database configuration. Use the 'prototype' model, # so that the block is executed on every request. Otherwise, if the # current_environment changed, the database_configuration service would # never reflect the change. @registry.register( :database_configuration, :model => :prototype ) do @registry.database_configurations[ @registry.current_environment ] end # return the current system log file location. Use the 'prototype' model, # so that the block is executed on every request. Otherwise, if the # current_environment changed, the system_log_file service would # never reflect the change. # # this allows clients to change where logs are written to, simply by # registering a system_log_file service that replaces this one. @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 # Select a new environment, by name. The only restriction is that, by default, # there must be an identically named database configuration, or this will # fail. # # The +app_root+ parameter, if specified, is the directory that is at the root # of this project. All application components will be referenced relative to # this directory. 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 # A convenience method for setting the environment. This allows you to do: # # Environment.set "production" # # instead of # # Environment.instance.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 # to do: # # Environment.registry # # instead of # # Environment.instance.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
Notice the various services it registers automatically, for every application:
:application_root |
This specifies the root directory of the application. It defaults to the current working directory, but you can specify a different one when you call Environment#set . |
:database_configurations |
This is a hash of all known database configurations, loaded from the config/database.yml file (relative to the application root directory). |
:database_configuration |
This is the currently selected database configuration, based on the currently selected environment. |
:system_log_file |
This is the name of the log file that is being written to. |
:current_environment |
This is the name of the currently selected environment. |
In particular, the :application_root
service can let applications easily discover their root directory without having to mess with workarounds like File.dirname(__FILE__)
.
By having the :application_root
default to the current working directory, Environment
plays nicely with irb
. Just fire up irb
, require 'env'
, and then use Rails::Environment.set
to set the environment you wish to use. As long as you started irb
in the projects root, you don’t have to worry about setting a root directory explicitly, or adding directories explicitly to the load path.
Lastly, note the BaseConvenienceMethods
module at the bottom of the file. This module is then included in each of the primary components of Rails, so that you can more easily access model and mailer components. Consider a hypothetical application that displays pedigree information:
pedigree_example.rbrequire 'active_record' require 'pedigree' class PedigreeController < ActionController::Base model :pedigree def show @pedigree = Pedigree.load( @params['id'], @params['generations'].to_i ) end ... end
This makes it easy (and more importantly, backwards compatible) to reference model services as constants.
Once the previous two files are in place, you can make everything backwards-compatible by creating production.rb
and test.rb
scripts in the application’s config/environments
directory. For example, here’s the complete text of the production.rb
script:
production.rbrequire 'env' Rails::Environment.set "production", File.dirname(__FILE__)+"/../.."
All it does is select the “production” environment and explicitly set the application root. (In this way, things will be guaranteed to work correctly in a live web environment.) The test.rb
script would look similar, just replacing “production” with “test”.
The implementation described in the previous section provides several benefits. Specifically, ease of testing, flexibility in backend implementation, service sharing, method interception, and flexible configuration.
Rails already provides a powerful framework for unit testing all areas of an application, including models, controllers, and views. However, because the current implementation encourages a tight coupling between controller and model, the controllers cannot be unit tested without also executing code and routines from the model, which will be largely irrelevant to the functionality being tested.
Note: Rails does not any longer encourage a tight coupling between model and controller. It now supports a model
keyword, as described below, that allows mock objects to be easily injected into a controller during test.
By decoupling the model from the controller, a developer is free to substitute a mock AR for a real one at unit test time. The mock AR only needs to implement the subset of functionality required by the controller, and can return hard-coded (or even scripted) data, making testing more consistent and less dependent on unnecessary routines.
Consider the following example. It represents an online application for recording books. This particular model/controller snippet implements the functionality to display a list of authors known to the application.
author.rbclass Author < ActiveRecord::Base ... end
authors_controller.rbclass AuthorsController < ActiveController::Base model :author def list @authors = Author.find_all end ... end
In the above example, the only interface of the Author
AR that is used by the controller is #find_all
. Thus, for unit testing, a developer only needs a single object that implements that method and returns an array of mock Authors. (You can’t use real Authors, because AR requires a connection to instantiate an AR, and that kind of defeats the purpose here.)
authors_controller_test.rbclass 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( # Test many books, one book, and no books [ 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
This could, of course, be made even simpler if some kind of scaffolding infrastructure were written to make the creation and registration of mock AR’s easier. As it is, the creation of the fixtures looks a bit messy, but it is certainly functional.
Note: in the current Rails, you can use a mock model object in a unit test simply by placing the mock implementation in the test/mocks/testing
directory. The Rails test framework will pick up and use the mock object automatically.
Although most Rails applications will employ ActiveRecord as the model implementation, it may be that a developer would like to support both a SQL backend (via ActiveRecord), as well as a non-SQL backend (ie, a flat-file datastore). Supposing the application were architected to never employ SQL outside of the model, all it would require is swapping the model implementation in and out.
The registry becomes an ideal level of abstraction for allowing this to happen. Instead of registering, for instance, the ActiveRecord model classes with the registry, you would register the non-SQL backend model implementations. The controller remains blissfully ignorant, since all it does is interact with an interface, sending messages to an object that it assumes knows how to handle them.
Consider the following snippet, which is mostly pseudocode for implementing the flat-file version of the Author class. (The FlatFileSystem::Base
class is an imaginary class, and is assumed to mimic some of the basic functionality of ActiveRecord::Base
. The register_self_as
method simply registers self
as the named service in the model registry.)
author_flat_file.rbrequire '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
Notice that all that matters is the interface. As long as it behaves like Author
, the controller won’t care what actually implements it.
Using Needle as a service locator, services may be easily shared between all levels of an application. One kind of service that might be shared thus is a logger, for writing log messages. In fact, Needle comes with an integrated logging subsystem for just this purpose. You query the :logs
service to get a handle to a logger.
book.rbclass Book < ActiveRecord::Base ... private def after_save log.info "Book #{name} was just saved" end def log @log ||= registry.logs.get( "Book" ) end ... end
book_controller.rbclass 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
The logs
service is instantiated once and that single instance is then shared, transparently, among all the different components. (The single instantiation is due to the fact that Needle enforces a singleton multiplicity constraint on all services, by default.)
Note: logging is a bad example here, because Rails already has very good support for logging. Furthermore, recent Rails releases support a service
keyword, for declaring and using system-global services in the same manner described here.
Another benefit of using a DI container like Needle is that developers can add AOP-like advice to the methods of any service in the registry. This can be useful for, among other things, debugging, in order to obtain a trace of the methods that are being invoked, and with what parameters they have.
Such constructs are called, in Needle, interceptors. For example, to add a logging interceptor to a service (in order to log the invocation of each of its methods, including parameter values), you would just do the following someplace in your code:
interceptor_logging.rb... Rails::Environment.registry. intercept( :author ). with { |c| c.logging_interceptor } ...
That’s it! Once that has been added, any invocation of any method of the :author
service will be logged, complete with parameter values, return value, and a description of any exceptions that are raised.
Method interception also allows you do to things like restrict access to methods based on user credentials, act as an adaptor that massages input and output into expected formats, or even redirect method invocations to a completely different receiver if an analysis of the arguments indicates that the message should be handled elsewhere.
It is easy to store any kind of object in the registry, including “static” objects like strings, arrays, and hashes. This makes the registry an ideal place to store configuration information that you would like to have be accessible throughout the application.
One such use of the registry as a configuration location is the database configuration for an application. Any application can look at the :database_configuration
service to get a hash of the values used to connect to the database.
What is more, by putting this configuration in the registry, any part of the application can easily contribute to the configuration. For instance, an object can add itself to an array of parties that are interested in being notified of some event. The primary observer of the events then reads that array from the registry, and sends notifications to each object in the array as events happen.
Another kind of configuration information that could be stored in the registry is the locations of such things as template files, log files, controller implementations, and so forth.
Note: Rails uses the environments to store this kind of information.
As was mentioned, the implementation given in this article is only a proof-of-concept. There is much more that could be done, than has been done.
For instance:
Not all of these may be desirable—the point is not that they should done, but that while they would have been tricky to implement before, the advent of dependency injection and service location makes them more feasible.
More thought could also go into the config/environments
directory, to determine if there is an even better way to specify the current environment to an application, in a more data-driven way. For instance, it might be desirable to allow the specification of a three-fold development/staging/production environment system. Having to change the dispatcher code to look at the correct environment each time you promote the code to the next level would be annoying, not to mention bug-prone.
Dependency injection adds a great deal of convenience in building multi-component applications. Rails applications are no exception, in spite of the fact that the design of Rails scales nicely to complex situations.
The proposed implementation is such that if a developer does not wish to use it, it would not get in the way, whereas those developers that do want to use it, can do so with a minimum of inconvenience.
The proposed implementation also cleans up the config/environments
directory, although (as mentioned previously) more thought should go into this.
1 Martin Fowler, Inversion of Control and the Dependency Injection Pattern. Jim Weirich, Dependency Injection in Ruby.
2 Ruby-on-Rails, http://www.rubyonrails.org
3 Needle Dependency Injector for Ruby, http://needle.rubyforge.org