6 minute read

Injecting Roles into objects in Ruby has been a hot topic in the DCI community. What’s the correct approach when augmenting an object at runtime? I want to explore some techniques around getting objects to behave properly. Jim Coplien, a huge proponent behind the DCI architecture, makes a great point:

“There are a million ways to map object roles to objects, to inject methods into classes, and to make the context available to the methodful object roles.”

I want to objectively pose Role injection options with supporting benchmarks. If there’s any further techniques outside of those discussed or better ways to accomplish them, I would love to hear them in the comments or by email.

Final disclaimer: Objects should be augmented with Roles in the scope of Contexts as demonstrated in The Right Way to Code DCI In Ruby. This article will not express Contexts, but rather focus directly on the Role injection itself.

The Canonical #extend

This form of Role injection has been used widely. It’s the only form of Role injection used by Jim Coplien in his book Lean Architecture.

data_object.extend RoleWithMethods
data_object.injected_method

The concerns around this technique are justified. The primary issue is the inability to #unextend the data_object. As data objects are passed around numerous Contexts or as they play many Roles in a given Context, we could run into naming collisions between methods in those Roles. Further, if we forget to inject a Role, we could be producing logical errors where our data object contains the necessary method but it belongs to the wrong Role. Take the following example:

# Within Context A
data_object.extend RoleWithMethods
data_object.injected_method

# Object enters a different Context
# We forgot to inject the following Role
# data_object.extend RoleWithMethodsTwo
data_object.injected_method #=> Calls injected_method from RoleWithMethods when we expected injected_method from RoleWithMethodsTwo

Logical errors aside, naming collisions using #extend exist in same form as including modules at class definition. That is, two modules included during class definition could suffer from naming collisions as well. The issues of long-lived objects, naming collisions and logical errors can be further alleviated by passing IDs to the Context class instead of the objects themselves, as demonstrated in The Right Way to Code DCI In Ruby. Further, even though methods are overwritten, you’ll always invoke the one you’re looking for. The invoked method will belong to the most recently #extended Role.

The other disadvantage of using #extend is the way it affects an object’s hierarchy. #extend works similar to inheritance; it injects the Role as a singleton ancestor of the object. Any derivation of the object will then contain the #extended Role as an ancestor, something that won’t occur if you include the module at class definition. #extending objects could lead to some hard-to-debug instances where it’s difficult to track the point at which the Role entered the object’s lookup table.

I benchmarked #extend in Benchmarking DCI in Ruby with Ruby 1.9.2. Here’s a rerun:

         user      system     total      real
extend  8.780000   0.010000   8.790000 (  8.785185)

Here’s the same benchmark in Ruby 1.9.3:

         user      system     total      real
extend  2.370000   0.000000   2.370000 (  2.377565)

A significant drop from 1.9.2 to 1.9.3.

Mixology

This technique uses mixology, a library designed to alleviate Ruby’s lack of #unextend. The last commit was in 2009 and the library appears to be effectively dead. It works in 1.9.2 but refuses to compile the native extensions in 1.9.3. 1.9.3 incompatibility aside, here’s how it’s used:

data_object.mixin RoleWithMethods
data_object.injected_method
data_object.unmix RoleWithMethods

This fits the DCI mold better than #extend. It gives the Context the ability to #unmix the modules as the Context comes to a close, ideal for managing scope creep as objects move between Roles and potentially Contexts.

I’ve benchmarked mixology against #extend. I include benchmarks with and without #unmix. Realistically, the benchmark of focus should be with the use of #unmix as it’s the core reason for introducing Mixology.

                   user      system     total      real
extend          8.710000   0.010000   8.720000 (  8.712797)
mixology w/o unmix  8.670000   0.000000   8.670000 (  8.671777)
mixology w/ unmix  12.570000   0.010000  12.580000 ( 12.567442)

It’s clear from these benchmarks that Mixology’s #mixin performs on par with #extend in Ruby 1.9.2. However, #unmixing modules incurs a significant performance hit.

SimpleDelegator, Forwardable, Facades and Wrappers

It’s feasible to wrap data objects in wrappers instead of injecting Roles directly. The beauty of doing so is that it encapsulates the object being modified by #extend. All behavior and the object itself it isolated within the wrapper. This technique can be demonstrated as follows:

class DataObjectDelegator < SimpleDelegator
  def initialize(data_object)
    super data_object.dup.extend(RoleWithMethods)
  end
end

data_object = DataObjectDelegator.new(data_object)
data_object.injected_method

The idea is to make the delegator function exactly like a data object without affecting the object itself. In the initializer, we duplicate the object and #extend it with our Role. We must duplicate the object (potentially having side effects of its own) so we don’t pollute the original object. Without duplication, the original data_object would contain the injected Role after the delegator has been removed. Using Forwardable works in a similar fashion to SimpleDelegator.

A more elegant solution is presented by Chris Bottaro called schizo which uses facades to wrap the data objects. It isolates the object containing the Role and allows you to act on it within block:

data_object.as(RoleWithMethods) do |object_with_role|
  object_with_role.injected_method
end

After the block executes, the data_object returns to normal, without the Role injected.

Wrappers can be a nice way to abstract object construction away from the core DCI implementation. They isolate and protect the extended object but they also add yet another layer of complexity. Managing delegators can feel cumbersome in comparision to natively supported #extend.

I benchmarked SimpleDelegator, Forwardable and Schizo to see how they perform in Ruby 1.9.2:

            user        system    total       real
Delegation  13.100000   0.010000  13.110000 ( 13.099318)
Forwardable 11.770000   0.010000  11.780000 ( 11.772542)
Schizo      52.900000   0.380000  53.280000 ( 53.227285)

Not ideal. Each of these performs quite poor compared to native #extend, especially in 1.9.3.

Wrappers can also come in the form of Role wrappers. In this instance, the data object would be passed as an argument to Role and the Role would then act on the argument rather than “self”. For instance:

class RoleWithMethods
  def initialize(data_object)
    @data_object = data_object
  end

  def injected_method
    @data_object.some_data_method
    # Instead of:
    # self.some_data_method
  end
end

RoleWithMethods.new(data_object).injected_method

All wrapping techniques suffer from the same problem: they’re not DCI. Read on to understand why.

DCI and Object Orientation

A founding concept behind DCI is object-orientation, though, not the object-orientation most of us are used to. Many of us think object-orientation is a means of instantiating classes and calling their methods. However, this is not “true OO”. “True OO” involves manipulating objects at runtime. It involves objects with just enough methods to get the job done. Instantiating a modern-day Rails model means we’re passing around objects that get the immediate job done plus all other jobs the model can perform. Joe Armstrong puts it nicely in Coders at Work:

“The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.”

“True OO” means constructing objects at runtime as needed, not constructing classes and instantiating them to perform a subset of tasks. That’s class-oriented programming. “True OO” means capturing the end-user’s mental model as objects in memory, akin to Simula and Smalltalk. I’ll leave a strong comparison between class-oriented and object-oriented programming to another article.

The reason wrapper techniques can’t fall under the DCI umbrella is they’re not directly manipulating Data objects. Their duplicating and obfuscating objects to work around Ruby’s inability to properly manage object augmentation. Having an #unextend method would alleviate the need for wrappers entirely. It really leads to the question: Is Ruby right for DCI? As it stands, the answer is no. Can we use tools and composition to enhance Ruby so it’ll perform the functions we need for DCI? Yes, but we’re not quite there. Tools in this space need more love, Mixology as a prime example.

Happy injecting!

24 comments