Rtidi is a project mainly written in Ruby, based on the MIT license.
RTiDI is a Ruby Tiny Dependency Injection library.
= RTiDI
== Description
RTiDI is a simple library that implements a dependency injection pattern that can be used in applications to avoid usage of highly coupled dependencies.
More information on the topic of DI:
== Requirements
=== Dependencies
none
=== Extra Development Dependencies
== Installation
=== Gem Installation
The preferred method of installation is through the Gem file. For this you will need to have the RubyGems library installed.
You can install the application and all its dependencies with the following command:
gem install rtidi
=== Manual Installation
You can clone, download or whatever else to get the library package from the GitHub.
== Library Usage
So let's start from a simple example.
We will define some common services and assets for a sample application that deals with a task of report generation.
require 'rtidi'
di :directories do asset :rc do
File.prepare_directory('~' / '.apprc') # String#/ is an extension
# that just wraps the
# File#expand_path method
end
asset :logs do
File.prepare_directory(di(:directories)[:rc] / 'logs')
end
end
di :files do asset :log do di(:directories)[:logs] / 'log.txt' end
asset :options do
di(:directories)[:rc] / 'options.yml'
end
end
di :services do service :logger do on_creation do require 'logger'
log_shift_options = di(:parameters)[options][:log_shift_age],
di(:parameters)[options][:log_shift_size]
logger = Logger.new(di(:files)[:log], *log_shift_options)
logger.level = Logger::INFO
logger
end
interface :log do |instance, message|
instance.info(message)
end
end
service :report_generator do
on_creation do
case di(:parameters)[options][:view_processor]
when :erb
require 'erb'; ERB
when :haml
require 'haml'; Haml
end
end
interface :render do |instance, text|
case instance
when ERB
instance.new(text).result()
when Haml
instance::Engine.new(text).render()
end
end
end
end
di :parameters do asset :options => { :log_shift_age = 10, :log_shift_size = 1048576, :view_processor = :erb }, :file => di(:files)[:options] end
require 'lib/context'
logger = di(:services)[:logger]
logger.log('The application was initialized')
#
di(:services)[:logger].info('Starting...')
data = ...
result = di(:services)[:report_generator].render(data)
Here we have created several DI containers that store some common assets and contains some basic services for logging and dealing with a markup processors.
Assume that you have decided that the logger from the Ruby standard library is too much for this small application and the +STDOUT+ stream will do just fine here. In order to switch from the logger to the +STDOUT+ stream instance you need just to correct the +:logger+ service definition in the +:services+ DI container.
service :logger do on_creation do STDOUT end
interface :log do |instance, message|
instance.puts(message)
end
end
That's all!
Now the +logger.log('The application was initialized')+ expression will write a line of text to the standard output instead of outputting it to the +~/.apprc/logs/log.txt+ through the logger from the standard Ruby library.
As you can see in the application logic we were not using the instances directly rather we were working with them through a proxy that was routing method calls to appropriate instances.
So a container can store services or assets. What are they?
A service is a unit that stores an initialization logic of a specific dependency and can also contain some additional defined operations for it that are called interfaces. If a user wants to use a specific type of library in the project and does not want to be attached to a specific implementation, he can create a DI container, define a service, describe an initialization logic for a specific library, define a set of interfaces through which he will use this library and, of course, use the library in the application through this set of interfaces. Thus he will not bound himself to a specific implementation and it case of changes he will need only to change the initialization and usage logic in the container.
An asset is a service that stores only an instance of the dependency or an initialization logic for it. An asset can not contain interfaces. An asset can be serialized to YAML and can be automatically loaded from it. Assets are useful to outline simple elements of the application. In the first example assets were used to prepare (create) and to store an expanded path to the application directories and to the log file. An asset was also used to serialize and load application parameters.
A life cycle of services and assets is the following:
Services and assets are defined in the DI container with the +service+ or +asset+ methods.
When a service or asset is accessed for the first time the initialization block is called (if defined). If the block was not provided, a raw instance will be returned (only possible in assets). If the block was given, a result from the block invocation is returned and stored in the service or asset instance.
All subsequent access calls will return the initialized instance that was saved in the asset or service unless an +:init+ option was passed with the access method call.
A DI container can be defined with a +Kernel#di+ method. If the name for the container was specified, the initialized container is added to the +DI::CONTAINERS+ hash and can be later accessed through it. The +Kernel#di+ also allows to access a specific named containers from the +DI::CONTAINERS+ if an initialization block was not provided in the method call. A DI container can also be created with the use of the +DI::Container+ class with a basic Ruby class initialization syntax. In this case the newly created container is not added to the +DI::CONTAINERS+ hash.
Container definition
di :container_name do # An instance will be added
end
container = di do # An instance will NOT be added
end
container = DI::Container.new do # The same thing
end
Container access
container = di :container_name # This will try to get a container from the
container = di(:container_name) # The same thing container = DI::CONTAINERS[:container_name] # ...
Access to services and assets
container = di :container_name
service = container[:service_name] # The first call will initialize
service = container[:service_name] # The second call will return
service = container[:service_name, :init] # The :init option will initialize
asset = container[:asset_name]
Services definition
di :sample_container do service :service_name do # Defines a service on_creation do # Defines an initialization block that will be
end
# Note that the first parameter represents
# an instance of the wrapped object
interface :interface_name do |instance, arg, ...| # Defines a new
instance.do_something_with(arg) # method for the
# ... # service
end
# Several interfaces can be defined at the same time
interface :first_interface, :second_interface do |instance, arg|
# ...
end
end
end
Asset definition
di :sample_container do asset :asset_name do # Defines an asset
# used only during the first access call
end
# Defines an asset that stores an instance that does not
# need much of the initialization logic
asset :another_asset_name => instance
# Asset serialization with YAML
#
# If the file exists, the container will try to load it and return on
# receiving an access call.
#
# If the file does not exist, the container will try to serialize the
# value returned from the initialization block or an actual instance (if
# an alternative hash form was used in the asset definition) to the
# specified file.
#
# Changes in the definition of the +DI::Service+ instance that represents
# a specific asset will lead to the +DI::Service#update+ call that will try
# to serialize the service again reflecting the new definition.
#
# Assets with serialization are useful for maintenance of the application
# configuration files.
asset :asset_that_will_be_serialized, :file => '~/.apprc/options.yml' do
{ :first_param => true,
:second_param => false }
end
end
Best practises
Use DI container services only for dependencies that can possibly be changed in the future. Do not bloat your code with tons of abstractions.
It is recommended to define all application dependencies in external ruby source files (even though it is not strictly required).
== Development
=== Source Repositories
Scenario is currently hosted at GitHub.
The RubyGems page
The RubyForge page
The GitHub web page
The public git clone URL
== Contact Information
Author:: Toksaitov Dmitrii [email protected]
== License
(The MIT License)
Copyright (c) 2010 Dmitrii Toksaitov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.