# Wirer
#
# A lightweight dependency injection framework to help wire up objects in Ruby.
#
# Some usage examples for now:
container = Wirer do |c|
# SHOWING THE CONTAINER HOW TO CONSTRUCT AN EXISTING CLASS:
# This is registered as providing class Logger.
# It will be constructed via Logger.new('/option_for_logger.txt')
c.add Logger, '/option_for_logger.txt'
# This is registered as providing class Logger, and also providing feature :special_logger
# which can then be used to request it in particular situations
c.add :special_logger, Logger, '/special_log.txt'
# You can supply a custom block for constructing the dependency if you want;
# specifying the class upfront means it still knows what class is provided by the block
c.add(:other_special_logger, Logger) do
Logger.new(foo, bar, baz)
end
# You don't actually have to specify the class that's provided; it will just
# provide_class Object by default. In this case you really need to specify
# a feature name for it, or else you'll have no way to refer to it:
c.add(:mystery_meat) do
rand(2) == 0 ? Mystery.new : Meat.new
end
# add_new_factory is the more explicit but verbose way to do this.
# note in this case you need to specify a :method_name separately if you want a method defined on
# the container for it.
c.add_new_factory(:class => Foo, :features => [:foo, :bar], :method_name => :foo) {Foo.new(...)}
c.add_new_factory(:class => Logger, :features => [:logger], :method_name => :logger, :args => ['/arg_for_logger.txt'])
# SPECIFYING DEPENDENCIES (which will then automatically get constructed and passed into your constructor)
# This will be constructed via LogSpewer.new(:logger => logger)
c.add LogSpewer, :logger => Logger
# however since two Loggers are available, we might want to specify
# a particular one, by making it depend on the feature :special_logger
# provided by only one of them.
c.add :special_log_spewer, LogSpewer, :logger => :special_logger
# You can specify a combination of class/module and feature name requirements for a
# dependency:
c.add :fussy_log_spewer, LogSpewer, :logger => [SpecialLogger, :providing, :these, :features]
# USING DEFAULTS and PREFERRED FEATURES TO CHOOSE FROM MANY AVAILABLE DEPENDENCIES
# If there are many Loggers available, and you have a dependency on a Logger, how does it
# decide which one to give you?
#
# Answer: It will never make an arbitrary choice for you. If there are multiple matching
# factories and it has no way to differentiate between them, it will raise an error complaining
# about it and let you fix the issue.
#
# You can either refine the dependency to a *particular* logger, as in the example above
# where we asked for a :special_logger.
#
# But it would also be nice if you could nominate one logger as the default to use in the
# case of multiple loggers, without having to specifically request it in each case:
c.add Logger, :default => true
# which is shorthand for:
c.add Logger, :features => [:default]
# this will then be chosen over any other options when resolving a dependency.
# (if more than one 'default' is available, it will still complain).
# Defaults are actually implemented under the hood using 'preferred features'.
# These are extra feature names in addition to the required features for a dependency,
# which you'd kinda like if possible but if not then no worries.
# eg:
c.add ColourfulLogSpewer, :logger => {:class => Logger, :prefer => :colour_capable_logger}
# If there are multiple matches for the logger dependency here, it will prefer one which
# provides the :colour_capable_logger feature.
# (if there are multiple :colour_capable_loggers, it will still complain).
# By default, dependencies come with a preferred feature of :default, as though they
# were constructed via:
c.add LogSpewer, :logger => {:class => Logger, :prefer => :default}
# You can even have multiple preferred_features, in which case it'll try to pick the
# dependency providing the greatest number of them. However if you need more advanced
# logic to choose the particular dependency you want
# MULTIPLE AND OPTIONAL DEPENDENCIES
# intended to be useful for extension points in plugin systems - you can have for example a
# multiple dependency on 'anything interested in listening to me' or 'anything interested
# in plugging in to this extension point'.
# You can specify cardinality options on dependencies via a longer argument form:
# by default one and only one dependency is required, but you can make it
# :multiple to get an array of all matching dependencies.
# This will be constructed as NoisyLogSpewer.new(:loggers => [logger1, logger2, ...])
c.add :i_spew_to_all_logs, NoisyLogSpewer, :loggers => {:class => Logger, :multiple => true}
# if you don't mind getting a nil if there dependency isn't available, you can make it :optional
c.add :i_spew_to_a_log_if_present, HesitantLogSpewer, :logger => {:class => Logger, :optional => true}
# or maybe you want as many are as available but don't mind if that number is zero:
# if you don't mind getting a nil if there dependency isn't available, you can make it :optional
c.add :i_spew_to_what_i_can_get, HesitantLogSpewer, :loggers => {:class => Logger, :multiple => true, :optional => true}
# a particularly complicated dependency example:
c.add :complicated, LogSpewer, :loggers => {:class => Logger, :features => [:foo, :bar], :multiple => true, :optional => true}
# CUSTOM ARGS OR CONSTRUCTOR BLOCK
# By default, dependencies are passed to the class's new method as a hash argument.
# you can customize this with a block though:
c.add(:foo, Foo, :logger => Logger) do |dependencies|
Foo.new(dependencies[:logger])
end
# And you can specify initial :args which will be passed before the dependencies hash.
# in this case it'll be constructed as
# Foo.new('initial arg', :logger => logger)
c.add(:foo, Foo, 'initial arg', :logger => Logger)
# If you need to specify any other keyword arguments for the factory, :dependencies need to be supplied separately, eg:
c.add(:foo, Foo, :dependencies => {:logger => Logger}, :args => ['initial arg'], :features => [:extra, :feature, :names])
# SETTER DEPENDENCIES AND TWO-PHASE INITIALIZATION
# Sometimes you need depdendencies to be supplied after the object has been constructed.
# Eg if you need to break a cyclic dependency.
# These kinds of dependencies can be specified as :setter_dependencies.
# An example:
c.add(Foo, :setter_dependencies => {:bar => Bar})
c.add(Bar, :setter_dependencies => {:bar => Foo})
# this situation will be wired up like so:
#
# foo = Foo.new
# bar = Bar.new
# foo.send(:bar=, bar)
# bar.send(:foo=, foo)
# foo.send(:post_initialize) if foo.respond_to?(:post_initialize)
# bar.send(:post_initialize) if bar.respond_to?(:post_initialize)
#
# Note you can get a post_initialize callback once your entire dependency graph
# is wired up and ready for action.
#
# Note that the setters and post_initialize hook used for this purpose
# can be private, if you want to limit them only to use by the container
# during two-phase initialization.
# If you need precise control over two-phase initialization, you can add your own
# Factory provided it implements Wirer::Factory::Interface.
#
# The factory implementation can, if it wants, override the default mechanism for
# injecting dependencies into instances created from it, and the default mechanism
# for post_initializing them.
#
# It can also make the setter_dependencies requested conditional on the particular
# instance constructed, which may be useful if they vary depending on arguments to
# the constructor.
add_factory_instance(my_custom_factory, :method_name => :my_custom_factory)
# ADDING AN EXISTING GLOBAL OBJECT
# Useful if you're using some (doubtless third-party ;-) library which has
# hardcoded global state or singletons in a global scope, but you want to add them
# to your container anyway so they at least appear as modular components for use by
# other stuff.
# this will work provided the global thing is not itself a class or module:
c.add :naughty_global_state, SomeLibraryWithA::GLOBAL_THINGUMY
# or this is more explicit:
c.add_instance SomeLibraryWithA::GLOBAL_THINGUMY, :method_name => :naughty_global_state
# the object will be added as providing the class of which it is an instance,
# together with any extra feature name or names that you specify.
# here multiple feature names are specified
c.add :instance => SomeLibraryWithA::GLOBAL_THINGUMY, :features => [:foo, :bar]
# NON-SINGLETON FACTORIES
# So far every factory we added to our container has been a singleton in the scope of the container.
# This is the default and means that the container will only ever construct one instance of it, and
# will cache that instance.
#
# You can turn this off it you want though, via eg:
c.add :foo, Foo, :singleton => false
# The container will then construct a new instance whenever a Foo is required.
#
# Factories which are added as singletons can also support arguments, eg:
# container.foo(args, for, factory)
#
# These will then be passed on as additional arguments to the constructor
# block where you supply one, eg:
c.add(:foo, Foo, :singleton => false, :dependencies => {:logger => Logger}) do |dependencies, *other_args|
Foo.new(other_args, dependencies[:logger])
end
# Where you only supply a class, by default they'll be passed as additional
# arguments to the new method before the dependencies hash.
# If the last argument is a hash, dependencies will be merged into it. eg:
#
c.add(:foo, Foo, :singleton => false, :dependencies => {:logger => Logger})
# here,
# c.foo(:other => arg)
# will lead to
# Foo.new(:other => arg, :logger => logger)
# and
# c.foo(arg1, arg2, :arg3 => 'foo')
# to
# Foo.new(arg1, arg2, :arg3 => 'foo', :logger => logger)
#
# If you don't like this, just make sure to supply a constructor block.
# Note that the singleton-ness or otherwise, is not a property of the factory itself, rather
# it's specific to the context of that factory within a particular container.
# Note that when using non-singleton factories, all bets are off when it comes to wiring up
# object graphs which have cycles in them - since it can't keep constructing new instances
# all the way down.
#
# Similarly, if the same dependency occurs twice in your dependency graph,
# where a non-singleton factory is used for it, you'll obviously get multiple distinct instances
# rather than references to one shared instance.
#
# I considered providing more fine-grained control over this (eg making things a singleton in the
# scope of one particular 'construction session', but able to construct new instance for each
# such construction session) but this is out of scope for now.
end
# GETTING STUFF OUT OF A CONTAINER
# Pretty crucial eh!
# Things added via 'add' with a symbol method name as the first argument, are made available via
# corresponding methods on the container:
container.special_logger
container.mystery_meat
# You can also specify this via an explicit :method_name parameter (and in fact you need to
# specify it this if you use the slightly-lower-level add_factory / add_new_factory / add_instance
# calls)
# You can also ask the container to find any kind of dependency, via
# passing dependency specification arguments to []:
container[Logger]
container[:special_logger]
container[Logger, :multiple => true]
container[SomeClass, :and, :some, :features]
container[SomeModule]
container[SomeModule, :optional => true]
# unless you specify :optional => true, it'll whinge if the dependency can't be fulfilled.
# DSL FOR EXPRESSING DEPENDENCIES FOR A PARTICULAR CLASS
#
# This is really handy if you're writing classes which are designed to be
# components that are ready to be wired up by a Wirer::Container.
#
# Using the DSL makes your class instance itself expose Wirer::Factory::Interface,
# meaning it can be added to a container without having to manually state
# its dependencies or provided features. The container will 'just know'.
#
# (although as we shall see, you can refine the dependencies when adding it to a
# container, to override the defaults within that particular context if you need to;
# you can also specify extra provided features within the container context)
class Foo
wireable # extends with the DSL methods (Wirer::Factory::ClassDSL)
# Declare some dependencies.
dependency :logger, Logger
dependency :arg_name, DesiredClass, :other, :desired, :feature, :names, :optional => true
dependency :arg_name, :class => DesiredClass, :features => [:desired, :feature, :names], :optional => true
# to avoid cyclic load-order dependencies between classes using this DSL, you can specify a class or module
# name as a string to be resolved later. In this case you need to use the explicit :class => foo args style.
dependency :something, :class => "Some::Thing"
# you can declare extra features which the class factory provides:
provides_feature :foo, :bar, :baz
# and setter dependencies
setter_dependency :foo, Foo
# (by default this will also define a private attr_writer :foo for you, which is
# what it will use by default to inject the dependency)
# you can also override the factory methods which the class has been extended with.
# the most common case would be where you want to customize how instances are
# constructed from the named dependencies (and any other args), via eg:
def self.new_from_dependencies(dependencies, *other_args)
new(dependencies[:foo], dependencies[:bar], *other_args)
end
# you could also add extra instance-specific setter dependencies, eg via:
def self.setter_dependencies(instance)
result = super
result[:extra] = Wirer::Dependency.new(...) if instance.something?
result
end
# Or to customize the way that setter dependencies are injected:
def self.inject_dependency(instance, arg_name, dependency)
instance.instance_variable_set(:"@#{arg_name}", dependency)
end
end
class Bar < Foo
# when using Wirer::Factory::ClassDSL, subclasses inherit their superclass's dependencies
# and features, but you can add new ones:
dependency :another_thing, Wotsit
provides_feature :extra
# or override existing dependencies
dependency :logger, :special_logger
# or if you don't want this inheritance between the class factory instances of subclasses,
# you can just extend with Wirer::Factory::ClassMixin instead of using the DSL, or you
# can override constructor_dependencies / setter_dependencies / provides_features class
# methods, or both.
end
# Adding these classes into a container is then quite simple:
Wirer do |c|
# It will see that Foo.is_a?(Wirer::Factory::Interface) and add it directly as a factory
# taking into account its dependencies etc
c.add Foo
# You can *refine* the dependencies of an existing factory when adding it, eg:
c.add Foo, :logger => :special_logger
# its original dependency was just on a Logger, but now it's on a Logger which also
# provides_feature :special_logger.
#
# This allows you to customize which particular instance of a given dependency this
# class gets constructed with. It will be added using a Wirer::Factory::Wrapped around
# the original factory.
# You can also specify extra features when adding a factory, which then give you a handle
# by which to refer to it when you want it passed to some other thing. Eg to provide the
# special logger above:
c.add :special_logger, Logger
# or both at once: adding an existing factory with some extra features and some refined
# dependencies within this container's context.
c.add Foo, :features => [:special_foo], :dependencies => {:logger => :special_logger}
# (if you want to specify other arguments for Factory::Wrapped, the dependency refining
# arguments need to go in their own :dependencies arg)
# then could then eg
c.add Bar, :foo => :special_foo
# If you have an existing factory which takes arguments, you can wrap it with specific
# (initial) arguments, allowing it to be added as a singleton, eg:
c.add Foo, 'args', 'for', 'foo', :logger => Logger
# or to be more explicit:
c.add Foo, :args => ['args', 'for', 'foo'], :dependencies => {:logger => Logger}
end