# 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