/wirer

A lightweight dependency injection framework to help wire up objects and modularise larger ruby codebases

Primary LanguageRuby

# 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