Forum: Ruby MinDI 0.3

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Joel VanderWerf (Guest)
on 2006-05-03 21:10
(Received via mailing list)
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/Depende...).

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 <tt>method_missing</tt>, a
service may invoke other services without having explicit setter or
constructor references to those services.

=== Synopsis

<i>Using the BasicContainer module for constructor injection:</i>

  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]]

<i>Using the InjectableContainer module for "dynamic" or "fallback"
injection, using <tt>method_missing</tt>:</i>

  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.
Christian N. (Guest)
on 2006-05-03 21:11
(Received via mailing list)
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?
Joel VanderWerf (Guest)
on 2006-05-03 23:54
(Received via mailing list)
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 ;)

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?
Christian N. (Guest)
on 2006-05-04 01:28
(Received via mailing list)
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 ;)

*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.
This topic is locked and can not be replied to.