MinDI 0.3


#1

http://raa.ruby-lang.org/project/mindi/

This version of MinDI adds a new container type, InjectableContainer,
which lets you hook up services via method_missing. See the second
example below. I’d be interested if anyone knows the correct term for
this kind of DI. It’s definitely not setter or constructor injection.
(The first example below is constructor injection.) Maybe “fallback
injection”, or “duck interface injection” ?

An excerpt from the intro.txt file:

== MinDI

MinDI is Minimalist Dependency Injection for Ruby. It is inspired by
Jamis B.'s Needle (http://needle.rubyforge.org) and Jim W.'s
article on DI in Ruby
(http://onestepback.org/index.cgi/Tech/Ruby/DependencyInjectionInRuby.rdoc).

MinDI is minimalist in that it attempts to map concepts of DI into basic
ruby
constructs, rather than into a layer of specialized constructs. In
particular, classes and modules function as containers and registries,
and methods and method definitions function as service points and
services. There are some inherent advantages and disadvantages to this
approach, discussed below.

MinDI builds on this minimal DI container by adding the
InjectableContainer concept, which is a kind of DI available only in
dynamic languages: through the magic of method_missing, a
service may invoke other services without having explicit setter or
constructor references to those services.

=== Synopsis

Using the BasicContainer module for constructor injection:

require ‘mindi’

class SimpleContainer
include MinDI::BasicContainer

greeting { "Hello, world\n" }

point_at { |x,y| [x,y] }

stuff { [greeting, point_at(100,200)] }

end

cont = SimpleContainer.new

p cont.stuff # ==> [“Hello, world\n”, [100, 200]]

Using the InjectableContainer module for “dynamic” or “fallback”
injection, using method_missing:

require ‘mindi’

class Transformer
def transform string
string.gsub(pattern, &replacement)
end
end

class TransformerContainer
include MinDI::InjectableContainer

pattern     { /foo/ }
replacement { proc {|match| match.upcase } }
transformer { Transformer.new }
transform   { |str| transformer.transform(str) }

end

cont = TransformerContainer.new
s1 = cont.transform(“fo foo fee”)
s2 = cont.transform(“fo foo fee”)
p s1 # ==> “fo FOO fee”
p s1.equal?(s2) # ==> true

Note that the Transformer class is written without explicitly linking up
to services (either in initialize or in setters). It just assumes that
the services will be defined in the container.

Note also that the #transform service is a multiton service, and (like
singleton services) it caches its value for each argument.


#2

Joel VanderWerf removed_email_address@domain.invalid writes:

s1 = cont.transform(“fo foo fee”)
s2 = cont.transform(“fo foo fee”)
p s1 # ==> “fo FOO fee”
p s1.equal?(s2) # ==> true

Note that the Transformer class is written without explicitly linking up
to services (either in initialize or in setters). It just assumes that
the services will be defined in the container.

So, if I get that right… the Transformer instance gets extended with
the TransformerContainer automatically? Or how does it know about
“pattern” and “replacement”?

Isn’t that a clear and obvious violation of encapsulation?
And won’t it hide lots of bugs due to typos or doubly defined methods?


#3

Christian N. wrote:

end

So, if I get that right… the Transformer instance gets extended with
the TransformerContainer automatically? Or how does it know about
“pattern” and “replacement”?

The Transformer instance is not extended with the TransformerContainer.
It’s extended with a simpler module that just has a #method_missing.
More precisely…

Here’s what happens when the service is instantiated (i.e. the “{
Transformer.new }” block is called in response to the #transformer
method on the container):

The service object (the Transformer instance returned by the block) is
given an instance variable, @injectable__object, whose value is set
to the container.

Also, the service object is extended, but only with a fairly minimal
module called Injected:

module Injected
def method_missing(*args, &block)
@injectable__object || super
@injectable__object.send(*args, &block)
rescue NoInjectedMethodError
super
end
end

The NoInjectedMethodError is raised by the container’s own
method_missing. So the method search order for the service object is:

  1. the service object itself (the Transformer instance)

  2. other services in the container (this is how “replacement” and
    “pattern” are resolved)

  3. method_missing, as defined in the ancestry of the service object

Isn’t that a clear and obvious violation of encapsulation?

Yes. Better to be clear and obvious about it :wink:

I’m not completely convinced by this style of DI. It may be no worse
than others, though. In (say) setter injection DI there is a coupling
between the container and the service, because the two need to agree on
some list of accessors that the container will write to and the service
will read from. In this proposed style of DI, the coupling is via
references in the service to as-yet-undefined methods. It’s sort of like
defining a class that will have Enumerable mixed into it, but not
providing any methods other than #each. Or like delegating and assuming
that the target will define certain methods.

And won’t it hide lots of bugs due to typos or doubly defined methods?

If the container defines some method that is already defined in the
service and which the service is not expecting the container to provide,
the service will not be affected–it will continue to use its own
method, as it should.

The service and the container just need to agree (implicitly, at least)
on the list of methods the service expects the container to provide.

A typo in the service could invoke a container method (i.e., another
service) by mistake.

I suppose the Injected class could, instead of defining #method_missing,
provide just a class method, called #uses or #uses_services, which lets
you specify explicitly what services it uses (and defines a method for
each one):

class Transformer
extend SomeModuleThatDefinesTheUsesServicesMethod
uses_services :pattern, :replacement
def transform string
string.gsub(pattern, &replacement)
end
end

(This approach would still need the @injectable__object hackery.)

But I’m not sure that typos are a serious enough danger to require this
much explicitness. It would be that much harder to subclass Transformer
and add mocked versions of pattern and replacement, for example, or to
mix in a module that provided pattern and replacement in some other way.
I’d prefer the service not to have to know it is going to be used in a
container and only to know that certain methods will be defined somehow.

“Duck services” anyone?


#4

Joel VanderWerf removed_email_address@domain.invalid writes:

Isn’t that a clear and obvious violation of encapsulation?

Yes. Better to be clear and obvious about it :wink:

g

I’m not completely convinced by this style of DI. It may be no worse
than others, though.

My DI library, Dissident, has a style that works similarily, but
services that should be injected need to be declared… personally
I like it.

class Transformer
  inject :pattern
  inject :replacement

  def transform string
    string.gsub(pattern, &replacement)
  end
end

(Note that this easily can be decoupled by adding “alias inject
attr_accessor”
somewhere.)

That would work as you describe, I think:

end

end

(This approach would still need the @injectable__object hackery.)

But I’m not sure that typos are a serious enough danger to require this
much explicitness. It would be that much harder to subclass Transformer
and add mocked versions of pattern and replacement, for example, or to
mix in a module that provided pattern and replacement in some other way.
I’d prefer the service not to have to know it is going to be used in a
container and only to know that certain methods will be defined somehow.

“Duck services” anyone?

IMO, PicoContainer-style constructor injection based on argument name
would be very nifty to have… to bad we can’t inspect those in Ruby.