Conject – Modern Dependency Injection in Ruby

Dependency Injection is relevant in Ruby. I say this because solving problems with highly decomposed systems of collaborating, narrow-purpose objects is still the best way I know, if I want to drive my code with tests and be able to change it later. DI tools help enable this type of design by carrying the burden of object instantiation and locking it outside our actual domain code.

(There’s a strange history of opinions revolving around DI in Ruby, and they’re worth discussing… sometime soon.)

I’ve been having a hard time finding a good DI tool for Ruby. I’m ready to move forward from DIY and enjoy some of the great conveniences that tools like Guice provide, such as automatic instantiation of object trees based on constructor and type info. So I wrote Conject, and though it’s still young, it’s working and showing promise.

  • gem install conject

ObjectContext

Conject’s power hinges on the usage of ObjectContexts: essentially, a home for objects keyed by name, which are accessed via #put, #get and convenient hash-like indexing via #[]. An ObjectContext may have another ObjectContext as a parent; a parent context will be consulted as a fall back if a requested object is not available in the child.

The interesting part is: when an object is not available in an ObjectContext (nor any of its ancestors), a new instance of the requested object will be created, cached and returned. Any dependencies the object declares will be fulfilled at creation time (before #initialize is called) by recursively searching for the required objects within the ObjectContext itself, or in ancestor contexts as needed.

Declare dependencies with .construct_with

Classes may declare their need for collaborators by enumerating them by name, using .construct_with.

class Car
  construct_with :chassis, :engine, :highway
end

car = Conject.default_object_context[:car]

In this example, the default object context will create a new instance of Car, store it with the key :car. A :chassis and :engine will also be created and stored as needed. (Note: objects are cached by default within an ObjectContext; they are “singletons” within their context and repeated reference to a named object via #get, #[], or from .construct_with will return the same instance.)

.construct_with does three things of importance:

  • It formally declares that any instance of the decorated class must be built with a certain set of named objects
  • It provides a pre-constructor that will accept the required components (in the form of a HashMap)
  • It defines private reader methods for each component. In the above example, #chassis and #engine now exist as private methods within the Car class.

If you’re referring to classes who are namespaced beneath a module, eg, Graphics::ChartComponent, your dependency declaration should assume the name for such an object would be "graphics/chart_component", and the internal reader method would be #chart_component. (.construct_with will treat strings and symbols equally.) The next release of Conject should support classes that are themselves in a module refering to companion classes within that same module without having to include the object name prefix and slash characters.

Configuring objects

For many cases, using .construct_with (and retrieving top-level objects from the ObjectContext) will be all you need to do to get your objects talking.

For others, you may occasionally need to do special configuration. The options for configuration are still under development, but so far you can:

# Disable caching for specific objects:
my_context.configure_object engine: {cache: false}

# Custom object construction via proc or lambda:
my_context.configure_object chassis: {construct: lambda do "The Chassis" end}

Subcontexts

Some designs call for the creation of microcosms of objects, which desire to refer to instances by name, and have those references be shared amongst multiple collaborators in the same context, even though many copies of this microcosm may coexist in the system. An example of this would be a Model-View-Presenter triad for an on-screen widget:

class ChartPresenter
  construct_with :chart_model, :chart_view
end

main_context.in_subcontext do |sub|
  sub[:chart_model]
end 

Subcontexts have implicit access to ancestral objects. If an object constructued within a subcontext refers to an object that already exists in an ancestor, the existing object will be provided. For example, if you’re building Cars in a subcontext, you could provide a Highway in the supercontext that all Cars would share a reference to.

Using your context directly

When an object needs explicit access to its owning ObjectContext, it can be constructed with this_object_context. One obvious use for this is writing code that needs to generate subcontexts in reaction to some command or event.

class Galaxy
  construct_with :this_object_context
  def spawn_new_solar_system
    this_object_context.in_subcontext do |subcontext|
      subcontext[:sun].genesis
    end
  end
end

Notable TODOs

  • Implied modules for classes namespaced beneath the same module
  • Explicit Rails support
  • Auto-requiring…?
  • Make it convenient for objects within a Module namespace to refer to each other without specifying the module name each and every time.
Conversation
  • […] Conject for Ruby by Atomic’s own David Crosby […]

  • Comments are closed.