[ANN] MinDI: Minimalist Dependency Injection

MinDI is a DI (dependency injection) or IoC (invesion of control) framework for ruby inspired by Jamis Buck's Needle (http://needle.rubyforge.org)
and Jim Weirich's article on DI in Ruby
(http://onestepback.org/index.cgi/Tech/Ruby/DependencyInjectionInRuby.rdoc).

DOWNLOAD

MinDI is available at:

http://redshift.sourceforge.net/mindi/

Includes examples, tests, and basic docs. It's a proof of concept,
but has enough features for production use.

DESCRIPTION

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.

Advantages:

- Compact implementation.

- Compact syntax.

- Familiar constructs and idioms, like subclassing, module inclusion,
   nested classes, protected and private, all apply.

- Use of classes and methods as containers and services means you can
   apply a standard AOP or debugging lib.

- Services can take arguments, and this permits multiton services.

Disadvantages:

- A container's services live in the same namespace as the methods
   inherited from Kernel and Object, so a service called "dup", for
   example, will prevent calling Object#dup on the container (except in
   the implementation of the dup service, which can use super to invoke
   Object#dup). The MinDI framework itself adds a few methods that
   could conflict with services (#singleton, #generic, etc.).

- No built-in AOP, debugging, or reflection interface.

Notes:

- Supports threaded, deferred, singleton, and multiton service models
   (though these are not yet independent choices). Additional service
   models can be easily added in modules which include Container. The
   "generic" model can be used like "prototype" in Needle, or for
   manual service management.

- Use mixins to build apps out of groups of services that need to
   coexist in one name space.

- Use a nested class for a group of services when you want them to
   live in their own namespace.

EXAMPLES

An example with parametric services:

   class Point < Struct.new(:x, :y); end

   class MyContainer
     extend MinDI::Container

     # A multiton service: a unique value is generated for
     # each requested parameter list.
     point_for_xy { |x,y| Point.new(x,y) }

     rect_with_width { |w| [point_for(0,0), point_for(w,200)] }
   end

The example from Jim Weirich's article would look like this in MinDI:

   class JWApplicationContainer
     extend MinDI::Container

     logfilename { "logfile.log" }
     db_user { "jim" }
     db_password { "secret" }
     dbi_string { "DBI:Pg:example_data" }

     app {
       app = WebApp.new(quotes, authenticator, database)
       app.logger = logger
       app.set_error_handler error_handler
       app
     }

     quotes { StockQuotes.new(error_handler, logger) }
     authenticator { Authenticator.new(database, logger,
                       error_handler) }
     database { DBI.connect(dbi_string, db_user, db_password) }

     logger { Logger.new(logfilename) }
     error_handler {
       errh = ErrorHandler.new
       errh.logger = logger
       errh
     }
   end

   def create_application
     JWApplicationContainer.new.app
   end

Joel VanderWerf wrote:

Disadvantages:

- A container's services live in the same namespace as the methods
  inherited from Kernel and Object, so a service called "dup", for
  example, will prevent calling Object#dup on the container (except in
  the implementation of the dup service, which can use super to invoke
  Object#dup). The MinDI framework itself adds a few methods that
  could conflict with services (#singleton, #generic, etc.).

They can still be called in a wordy way: Object.instance_method(:dup).bind(container).call

Maybe this could be wrapped into a small helper method?

Joel VanderWerf wrote:

MinDI is a DI (dependency injection) or IoC (invesion of control) framework for ruby inspired by Jamis Buck's Needle (http://needle.rubyforge.org)
and Jim Weirich's article on DI in Ruby
(http://onestepback.org/index.cgi/Tech/Ruby/DependencyInjectionInRuby.rdoc\).

Nice work, Joel! It's nice to have a little competition. Drives the quality up, I hear. :slight_smile:

Our recent discussion regarding multitons and parameterized services got me looking harder at Needle's internals, and I discovered that it is almost ridiculously easy to add them.

So, Needle (in CVS) supports parameterized services and a multiton service model, working just like you would expect:

   registry.define.printer( :model => :multiton ) do |c,p,name|
     Printer.new( name )
   end

   p1 = registry.printer( :monochrome )
   p2 = registry.printer( :monochrome )
   assert_same p1, p2

   p3 = registry.printer( :color )
   assert_not_same p1, p3

I'll be pushing this change out on Thursday as version 1.2, in keeping with Needle's weekly release schedule. Thanks to Joel and Christian (Neukirchen) for the impetus to add the feature. :slight_smile:

- Jamis

···

--
Jamis Buck
jgb3@email.byu.edu
http://www.jamisbuck.org/jamis

Forgive me for being dense, but once you've gone this far, is there
any particular reason not to simply write:

class JWApplicationContainer
  def logfilename() "logfile.log"; end
  
  ...

  def quotes
     @quotes ||= StockQuotes.new(error_handler, logger)
  end

  ...
end

and so on? I think the win in simplicity probably outweighs the extra
few characters you have to type...

Avi

···

Joel VanderWerf <vjoel@PATH.Berkeley.EDU> wrote:

The example from Jim Weirich's article would look like this in MinDI:

   class JWApplicationContainer
     extend MinDI::Container

     logfilename { "logfile.log" }

     quotes { StockQuotes.new(error_handler, logger) }

   end

Florian Gross wrote:

Joel VanderWerf wrote:

Disadvantages:

- A container's services live in the same namespace as the methods
  inherited from Kernel and Object, so a service called "dup", for
  example, will prevent calling Object#dup on the container (except in
  the implementation of the dup service, which can use super to invoke
  Object#dup). The MinDI framework itself adds a few methods that
  could conflict with services (#singleton, #generic, etc.).

They can still be called in a wordy way: Object.instance_method(:dup).bind(container).call

Maybe this could be wrapped into a small helper method?

Sure, thanks for the suggestion.

I don't know if it the namespace problem is really so bad though. It happens in the following case:

   class MyContainer
     extend MinDI::Container

     singleton :dup do # or threaded or some other service model
       #...
     end
   end

   cont = MyContainer.new
   cont.dup

But that looks like a design problem to me. Maybe I will feel differently for methods like #hash, though.

The only other problem I can think of is when using the shortcut (via method missing) to define services:

   class MyContainer
     extend MinDI::Container

     foo { Foo.new } # ok
     dup { Dup.new } # not ok - definition not applied
     hash { Hash.new } # not ok
   end

This won't work, of course, but it shouldn't be expected to.

My current feeling is that it's just better to be aware of what already exists in the namespace of MyContainer, just as one does when defining normal classes.

Jamis Buck wrote:

Joel VanderWerf wrote:
Nice work, Joel! It's nice to have a little competition. Drives the quality up, I hear. :slight_smile:

Hear, Hear !!

Our recent discussion regarding multitons and parameterized services got me looking harder at Needle's internals, and I discovered that it is almost ridiculously easy to add them.

<cool stuff snipped>

I'll be pushing this change out on Thursday as version 1.2, in keeping with Needle's weekly release schedule.

You guys, you know, should slow done a little. Even before I can finish groking the previous release, out comes a new one. You give me a huge inferiority complex :wink:

-- shanko

···

That really goes for all the smart people on this ML.

Avi Bryant wrote:

The example from Jim Weirich's article would look like this in MinDI:

  class JWApplicationContainer
    extend MinDI::Container

    logfilename { "logfile.log" }

    quotes { StockQuotes.new(error_handler, logger) }

  end

Forgive me for being dense, but once you've gone this far, is there
any particular reason not to simply write:

class JWApplicationContainer
  def logfilename() "logfile.log"; end
    ...

  def quotes
     @quotes ||= StockQuotes.new(error_handler, logger)
  end

  ...
end

and so on? I think the win in simplicity probably outweighs the extra
few characters you have to type...

I forgive you :slight_smile: That reaction was exactly the initial reaction that DI provoked in me and which I wrote up in [ruby-talk:120214]. The style that you suggest is what I've used in my own code, and it took Jim's article to see that it was really a form of DI.

I'm not yet sure whether a more explicit DI (Arrow, or MinDI) will replace that style for me--I haven't actually used it in an application. It probably depends on how much I use the other service models besides singleton (e.g., #quotes, above) and prototype (#logfilename, in the way you defined it). In that case, DI may be a useful abstraction layer that saves more than typing.

···

Joel VanderWerf <vjoel@PATH.Berkeley.EDU> wrote: