Posts tagged with dci

DCI: The King of the Single Responsibility Principle

The single responsibility principle (SRP) is one of the most widely followed ideals in object oriented programming. For decades, developers have been striving to ensure their classes take on just enough, but not too much, responsibility. A valiant effort and by far one of the best ways to produce maintainable code.

SRP is hard, though. Of all the SOLID design principles, it is the most difficult to embrace. Due to the abstract nature if its definition, based purely on example instead of directive process, it's hard to concretize. More specifically, it's difficult to define "responsibility", in general or in context. There are some rules-of-thumb to help, like reasons for change, but even these are enigmatic and hard to apply.

Simply put, SRP says a class should be comprised of just one responsibility, and only a single reason should force modification.

Let's take a look at the following class which clearly has three responsibilities and therefore breaks SRP:

class User < ActiveRecord::Base
  def check_in(location); ... end
  def solr_search(term); ... end
  def request_friendship(other_user); ... end
end

This class would require churn for a variety of reasons, some of which include:

1. The algorithm for checking in changes.
2. The fields used to search SOLR are renamed.
3. Additional information needs to be stored when requesting a friendship.

So, based on the rules of SRP, this class needs to be broken out into three different classes, each with their own responsibility. Awesome, we're done, right? This is often the extent of discussions around SRP, because it's extremely difficult to provide solutions beyond contrived, minute examples. Theoretically, SRP is very easy to follow. In practice, it's much more opaque. It's too pie in the sky for my taste; like most OOP principles, I think SRP should be more of a guideline than a hard-and-fast rule.

The real difficulty of SRP surfaces when your project grows beyond 100 lines of code. SRP is easy if you're satisfied with single method classes or decide to think about responsibility exclusively in terms of methods. In my opinion, neither of these are suitable options.

DCI provides more robust guidelines for following SRP, but we need to redefine responsibility. That's the focus of this article.

Identity and Responsibility

OK, let's try to elucidate responsibility, but first, let's talk about object orientation.

The word "object" can be defined as a resource that contains state, behavior and identity. Its state is the value of the class's attributes. Its behavior is the actions it can perform. And its identity is...well...it's object id? It feels strange to narrow the definition of identity into a mere number. I certainly don't identify myself by my social security number. My identity is derived from my name, the things I enjoy doing, and potentially my environment. More importantly, my identity is always changing.

While building a class, when was the last time you thought about the forthcoming object ids of the instances of that class? I hope never. We don't program classes with identity in mind, yet if we're trying to model the world, it's an intrinsic component. Identity means nothing to us while building classes, yet everything to us in the real world.

Therefore, it's appropriate to say that the mental model of the programmer is to map identity to state and behavior, rather than to object id. Object id is a quality of uniqueness.

Identity is closely related to responsibility. As expressed above, I don't identify by my social security number, but by my state and behavior. When we attempt to find the appropriate location for a method definition, we look at the responsibility of the prospective classes. "Does this method fit into this class's single responsibility?" If we consider that identity should truly be a representation of an object's state and behavior, we can deduce that identity is a derivative of responsibility.

An example of this observation is polymorphism; probably the most predominant and powerful object-oriented technique. When we consider the possible duck-typed objects in a scenario, we don't think, "this object will work if its object id is within the following set..." We think "this object will work if it responds to the right methods." We rarely care about the object id. What's important is the true identity of an object: the methods it responds to.

DCI Roles

Object ids mean nothing to programmers. Defining identity by the memory location of an object is very rarely a means for writing effective software. Computers care about memory locations, not programmers. Programmers care about state and behavior, hence, the reason we build classes.

SRP is about acutely defining state and behavior, thus, identity. In DCI, we define state through data objects and behavior through roles. Generally speaking, behavior changes state so the primary focus is on behavior. If we ask, "what can an object do?", we can subsequently ask, "how does its state change?"

We still haven't really defined responsibility. As a human being, my responsibilities change on a regular basis. At one point, I'm responsible for writing an email. At another, I'm responsible for mentoring a developer. Rarely, although occasionally, am I responsible for doing both. Enter role-based programming.

We can reprogram the example class above using DCI data objects and roles:

# Data
class User < ActiveRecord::Base; end

# Roles
module Customer
  def check_in(location); ... end
end

module Searcher
  def solr_search(term); ... end
end

module SocialButterfly
  def request_friendship(other_user); ... end
end

Now, each role has a single responsibility. If we define responsibility by a role played by a data object, it becomes obvious where new methods should go and when you're breaking responsibility. Let's give it a shot.

Say we want to add functionality that allows users to accept a requested friendship. First ask the question in terms of business objectives, "As a social butterfly, can I accept a friendship?" By converting our expectations into English, we can then easily map business rules to code. It would clearly be wrong if we ask, "As a searcher, can I accept a friendship?" Therefore, #accept_friendship should belong in SocialButterfly:

module SocialButterfly
  def request_friendship(other_user); ... end
  def accept_friendship(other_user); ... end
end

By defining responsibility as a role, we can converge on contextual identity, the true essence of object orientation. While building a role, we are building identity, a crucial part of a programmer's mental model. Roles are the inverse abstraction of classes. While classes focus on abstracting away identity, roles focus on highlighting it. It's no wonder it's so difficult to define responsibility when we're programming classes, not object.

The King of SRP

It's hard to define responsibility. It's even harder to program for it. As an artifact, the responsibility of a class often ends up being either too narrowly or too broadly defined. Define responsibility too narrowly, and it's daunting to wrap your head around 1000 classes. Define responsibility too broadly, and it's arduous to maintain and refactor.

By defining responsibility as a role, we have a clear notion of behavioral locality. We can ask questions like, "as a customer, can I add an item to my cart?" If the answer is yes and we've appropriately named our roles, the method belongs in that role. This gives us a means for defining responsibility, and we can refactor accordingly.

Roles won't alleviate potential clutter, but they can give us a structure for defining responsibility. With DCI, we can talk about responsibility in terms of directive process instead of contrived examples.

First, understand the business objectives of the system and subsequently understand the roles. Understand the roles and subsequently understand the responsibilities.

Posted by Mike Pack on 03/12/2013 at 08:28AM

Tags: srp, solid, oop, dci


DCI: The King of the Open/Closed Principle

The open/closed principle (OCP) is a fundamental "run of thumb" in object-oriented languages. It has hands in proper inheritance, polymorphism, and encapsulation amongst other core properties of object-oriented programming.

The open/closed principle says that we should refine classes to the point at which we eliminate churn. In other words, the less times we need to open a file for modification, the better. With DCI, we can compose objects while still following OCP.

Extension is Inheritance

Wikipedia's definition of inheritance:

Inheritance is a way to reuse code of existing objects, or to establish a subtype from an existing object, or both, depending upon programming language support.

By using #extend to modify objects at runtime, we are both reusing code from data objects while also forming a new subtype of the data object.

A typical DCI context might look like this:

app/use_cases/customer_purchases_book.rb

customer = User.find(1) # customer is a data object
customer.extend(Customer) # inject the Customer role
customer.purchase(book) # invoke Customer#purchase

After calling #extend, the user object can be used as both a data object and a purchasing customer. The #purchase method likely uses attributes of the user object to create joins between him and his book. We're reusing code from its former self.

Similarly, the new object is now a subtype of its former self. That is, the Customer version of user can be used polymorphically in place of the data object itself.

The Open/Closed Principle (OCP)

The open/closed principle is often discussed in the context of inheritance; we use inheritance to adhere to the "closed" aspect of OCP. In order to follow OCP, a class can be open for extension, but closed for modification. Let's look at how the principle could be applied with classical inheritance to reimplement the above scenario.

We have a dumb data object:

class User
  # A dumb data object
end

To abide by the "closed" aspect of OCP, we define a subtype of the User class; we do not modify the class itself:

class Customer < User
  def purchase(book)
    # Update the system to record purchase
  end
end

Somewhere else in our codebase, we tell a user to purchase his book:

customer = Customer.new
customer.purchase(book)

This is great, we've accomplished OCP by ensuring that any customer related aspects of a User are neatly tucked away in the Customer class. In order to change the behavior of a user, we formed a new class while leaving the User class alone.

This is the guts of the open/closed principle. We want to structure our classes in such a way as to ensure they never need to change. Guaranteeing the classes don't change is also a function of the method bodies.

DCI and OCP

Following OCP without incorporating the Data, Context and Interaction architecture has proven to lead to looser coupling, stronger encapsulation, and higher cohesion. Just apply it and your world will be rainbows and ponies!

Wrong. While OCP has absolutely helped in producing higher quality code, it's just another lofty object oriented principle. It's very difficult to adhere to all principles, and some may be entirely inappropriate in various scenarios. The SOLID principles (of which OCP is one) are a great frame-of-reference when discussing software design, however, heeding to them 100% of the time is frankly, impossible.

I often find it very difficult to ensure the first iteration of my core software, test suite, and ancillary code meet the qualifications of the SOLID principles. Not because I don't understand or refuse to apply them, but because I'm human and I'm working with frequently-varying business rules. Principles in general end up being this pie-in-the-sky goal; I prefer to just write software.

One of the reasons I love DCI so much is because it forces you to work in an orthogonal way. It breaks the cemented programming models we've seen for over 20 years. Models which, in my opinion, do not lend themselves towards these principles. DCI acts much like a lighthouse: guiding you towards proper object orientation.

DCI enables you to automatically apply many best practice principles in object oriented programming. The open/closed principle is one.

Closed for Modification

The whole point of DCI is to decouple what changes from what remains constant. In DCI, our data objects are strictly persistence related, and as such, do not change frequently. The way in which we use data objects is often what changes.

So, when we build out a data object...

app/models/user.rb

class User < ActiveRecord::Base
  # A dumb data object
end

...it's closed for business.

DCI tells us that if we want to add behavior to this class, we should be doing so within a role. A deliberate effect of this is that our class remains closed. OCP is telling us to optimize our classes so that we never need to modify them. This aspect of OCP is baked into the core of DCI.

Open for Extension

The name says everything. The best way to accomplish DCI in Ruby is to use #extend. We seek to inject roles into objects at runtime to accomplish our behavioral needs. Let's create our Customer role:

app/roles/customer.rb

module Customer
  def purchase(book)
    # Update the system to record purchase
  end
end

We would then join our data and roles within a context:

app/use_cases/customer_purchases_book.rb

class CustomerPurchasesBook
  def initialize(user, book)
    @customer, @book = user, book
    @customer.extend(Customer)
  end

  def call
    @customer.purchase(@book)
  end
end

The open/closed principle states that a class should be open for extension. Within the above context, we extend our user object with the Customer role. Our DCI code adheres to this rule.

OCP talks a lot about extension of classes via inheritance. Demonstrations of OCP are usually forged with classes, instead of objects. In the above paragraph, I say that classes are open for extension, but the user object is extended. When we define a class, it's simply a container in which methods live. That container then becomes part of an object's lookup hierarchy. So, behaviorally speaking, there's no semantic difference between composing an object from scratch with DCI and creating an instance of a class.

In the customer example above, we use #extend as a means of composing the customer object to include its necessary behavior. We do this in lieu of classical inheritance. As I mentioned earlier in this article, extension is inheritance.

The Silver Bullet

By applying DCI, you are ever-so-nicely nudged into following OCP. DCI is a paradigm shift, but it's coated with reward. By simply working in objects and extending them at runtime, you are guided towards many well-respected, object-oriented principles. The strong emphasis DCI puts on decoupling static classes from dynamic behavior means that your classes remain closed for modification.

DCI contexts are naturally built for OCP. Use cases rarely change. If a user is buying a book, the use case of that purchase remains relatively constant. Since contexts act as simple glue between data and roles, if a use case changes, it's likely to be a new context. In this regard, contexts remain closed for modification.

DCI won't help you properly construct your roles, but it does guide you in the right direction. Since roles are actor-based, their methods tend to be use case specific. This means that role methods don't need to accomodate for drastic variations. If variation increases, I tend to reach for service objects to abstract that complexity.

There is no silver bullet to following object-oriented principles. We're always making tradeoffs. Managing complexity is inherently complex. DCI can help you cope by ensuring your objects remain open for extension, yet closed for modification.

Posted by Mike Pack on 12/18/2012 at 08:14AM

Tags: ocp, solid, oop, dci


DCI with Ruby Refinements

TL;DR - Have your cake and eat it too. Ruby refinements, currently in 2.0 trunk, can cleanly convey DCI role injection and performs right on par with #include-based composition. However, there's some serious caveats to using refinements over #extend.

Recently, refinements was added to Ruby trunk. If you aren't yet familiar with refinements, read Yahuda's positive opinion as well as Charles Nutter's negative opinion. The idea is simple:

module RefinedString
  refine String do
    def some_method
      puts "I'm a refined string!"
    end
  end
end

class User
  using RefinedString

  def to_s
    ''.some_method #=> "I'm a refined string!"
  end
end

''.some_method #=> NoMethodError: undefined method `some_method' for "":String

It's just a means of monkeypatching methods into a class, a (still) controversial topic. In the above example, the User class can access the #some_method method on strings, while this method is non-existent outside the lexical scope of User.

Using Refinements in DCI

Refinements can be used as a means of role-injection in DCI, amongst the many other techniques. I personally like this technique because the intention of the code is clear to the reader. However, it has some serious drawbacks which we'll address a bit later.

Let's say we want to add the method #run to all Users in a given context.

Our User class:

class User; end

Our refinement of the User class:

module Runner
  refine User do
    def run
      puts "I'm running!"
    end
  end
end

In the above refinement, we are adding the #run method to the User class. This method won't be available unless we specifically designate its presence.

Our DCI context:

class UserRunsContext
  using Runner

  def self.call
    User.new.run    
  end
end

Here, we're designating that we would like to use the refinement by saying using Runner. The #run method is then available for us to use within the context trigger, #call.

Pretty clear what's happening, yeah?

I wouldn't go as far as saying it carries the expressiveness of calling #extend on a user object, but it gets pretty darn close. To reiterate, the technique I'm referring to looks like the following, without using refinements:

user = User.new
user.extend Runner
user.run

Benchmarking Refinements

I'm actually pretty impressed on this front. Refinements perform quite well under test. Let's observe a few of role injection: using inclusions, refinements and extensions.

I ran these benchmarks using Ruby 2.0dev (revision 35783) on a MacBook Pro - 2.2 GHz - 8 GB ram.

Check out the source for these benchmarks to see how the data was derived.

#include (source)

Example

class User
  include Runner
end

Benchmarks

> ruby include_bm.rb
         user       system     total       real
include  0.560000   0.000000   0.560000 (  0.564124)
include  0.570000   0.000000   0.570000 (  0.565348)
include  0.560000   0.000000   0.560000 (  0.563516)

#refine (source)

Example

class User; end
class Context
  using Runner
  ...
end

Benchmarks

> ruby refinement_bm.rb
        user       system     total       real
refine  0.570000   0.000000   0.570000 (  0.566701)
refine  0.580000   0.000000   0.580000 (  0.582464)
refine  0.570000   0.000000   0.570000 (  0.572335)

#extend (source)

Example

user = User.new
user.extend Runner

Benchmarks

> ruby dci_bm.rb
     user       system     total       real
DCI  2.740000   0.000000   2.740000 (  2.738293)
DCI  2.720000   0.000000   2.720000 (  2.721334)
DCI  2.720000   0.000000   2.720000 (  2.720715)

The take home message here is simple: #refine performs equally as well as #include although significantly better than #extend. To no surprise, #extend performs worse than both #refine and #include because it's injecting functionality into objects instead of classes, for which we have 1,000,000 and 1, respectively.

Note: You would never use #include in a DCI environment, namely because it's a class-oriented approach.

Separation of Data and Roles

What I enjoy most about the marriage of refinements and DCI is that we still keep the separation between data (User) and roles (Runner). A critical pillar of DCI is the delineation of data and roles, and refinements ensure the sanctity of this concern. The only component in our system that should know about both data and roles is the context. By calling using Runner from within our UserRunsContext, we've joined our data with its given role in that context.

An example of when we break this delineation can be expressed via a more compositional approach, using include:

class User
  include Runner
end

The problem with this approach is the timing in which the data is joined with its role. It gets defined during the class definition, and therefore breaks the runtime-only prescription mandated by DCI. Furthermore, the include-based approach is a class-oriented technique and can easily lead us down a road to fat models. Consider if a User class had all its possible roles defined right there inline:

class User
  include Runner
  include Jogger
  include Walker
  include Crawler
  ...SNIP...
end

It's easy to see how this could grow unwieldy.

Object-Level Interactions and Polymorphism

Another pillar of DCI is the object-level, runtime interactions. Put another way, a DCI system must exhibit object message passing in communication with other objects at runtime. Intrinsically, these objects change roles depending on the particular context invoked. A User might be a Runner in one context (late for work) and a Crawler in another (infant child).

The vision of James Coplien, co-inventor of DCI, is tightly aligned with Alan Kay's notion of object orientation:

“I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages.” - Alan Kay

 

So, as roles are injected into data objects, do refinements satisfy the object-level interactions required by DCI? Debatable.

With refinements, we're scoping our method definitions within the bounds of a class. With modules, we're scoping our methods within the abstract bounds of whatever consumes the module. By defining methods within a module, we're essentially saying, "I don't care who consumes my methods, as long as they conform to a specific interface." Further, in order to adhere to Alan Kay's vision of object orientation, our objects must be dynamically modified at runtime to accommodate for the context at hand. The use of modules and #extend ensures our data objects acquire the necessary role at runtime. Refinements, on the other hand, do not adhere to this mantra.

Along similar lines, let's look at how refinements affect polymorphism. Specifically, we want to guarantee that a role can be played by any data object conforming to the necessary interface. In statically-typed systems and formal implementations of DCI, this is particularly important because you would be defining "methodless roles", or interfaces, for which "methodful roles" would implement. These interfaces act as guards against the types of objects which can be passed around. When we work with refinements and class-specific declarations, we lose the polymorphism associated with the module-based approach. This can be conveyed in the following example:

module Runner
  def run
    puts "I have #{legs} and I'm running!"
  end
end

# The Runner role can be used by anyone who conforms to
# the interface. In this case, anyone who implements the
# #legs method, which is expected to return a number.
User.new.extend Runner
Cat.new.extend Runner
Dog.new.extend Runner

# When we use refinements, we lose polymorphism.
# Notice we have to redefine the run method multiple times for each
# possible data object.
module Runner
  refine User do
    def run
      puts "I have #{legs} and I'm running!"
    end
  end

  refine Cat do
    def run
      puts "I have #{legs} and I'm running!"
    end
  end

  refine Dog do
    def run
      puts "I have #{legs} and I'm running!"
    end
  end
end

The really unfortunate thing about refinements is we have to specify an individual class we wish to refine. We're not able to specify multiple classes to refine. So, we can't do this:

module Runner
  refine User, Cat, Dog do # Not possible.
    def run
      puts "I have #{legs} and I'm running!"
    end
  end
end

But even if we could supply multiple classes to refine, we're displacing polymorphism. Any time a new data object can play the role of a Runner (it implements #legs), the Runner role needs to be updated to include the newly defined data class. The point of polymorphism is that we don't really care what type of object we're working with, as long as it conforms to the desired API. With refinements, since we're specifically declaring the classes we wish to play the Runner role, we lose all polymorphism. That is to say, if some other type, say Bird, conforms to the interface expected of the Runner role, it can't be polymorphically interjected in place of a User.

Wrapping Up

Refinements are a unique approach to solving role injection in DCI. Let's look at some pros and cons of using refinements:

Pros

  • #refine provides a clean syntax for declaring data-role interactions.
  • Refinements perform around 500% better than #extend in DCI.
  • The data objects are clean after leaving a context. Since the refinements are lexically scoped to the context class, when the user object leaves the context, it's #run method no longer exists.

Cons

  • We lose all polymorphism! Roles cannot be injected into API-conforming data objects at runtime. Data objects must be specifically declared as using a role.
  • We can't pass multiple classes into #refine, causing huge maintenance hurdles and a large degree of duplication.
  • We lose the object-level, cell-like interaction envisioned by Alan Kay in which objects can play multiple and sporatic roles throughout their lifecycle.
  • Testing. We didn't cover this, but in order to test refinements, you would need to apply the cleanroom approach with a bit of test setup. In my opinion, this isn't as nice as testing the results of method after using #extend.

While there's certainly some benefits to using refinements in DCI, I don't think I could see it in practice. There's too much overhead involved. More importantly, I feel it's critical to maintain Alan Kay's (and James Coplien's) vision of OO: long-lived, role-based objects performing variable actions within dynamic contexts.

After all this...maybe I should wait to see if refinements even make it into Ruby 2.0 .

Happy refining!

Posted by Mike Pack on 08/22/2012 at 09:13AM

Tags: dci, ruby, refinements


DCI Role Injection in Ruby

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!

Posted by Mike Pack on 02/08/2012 at 01:53PM

Tags: delegation, forwardable, extend, architecture, dci


The Right Way to Code DCI in Ruby

Many articles found in the Ruby community largely oversimplify the use of DCI. These articles, including my own, highlight how DCI injects Roles into objects at runtime, the essence of the DCI architecture. Many posts regard DCI in the following way:

class User; end # Data
module Runner # Role
  def run
    ...
  end
end

user = User.new # Context
user.extend Runner
user.run

There's a few flaws with oversimpilfied examples like this. First, it reads "this is how to do DCI". DCI is far more than just extending objects. Second, it highlights #extend as the go-to means of adding methods to objects at runtime. In this article, I would like to specifically address the former issue: DCI beyond just extending objects. A followup post will contain a comparison of techniques to inject Roles into objects using #extend and otherwise.

DCI (Data-Context-Interaction)

As stated previously, DCI is about much more than just extending objects at runtime. It's about capturing the end user's mental model and reconstructing that into maintainable code. It's an outside → in approach, similar to BDD, where we regard the user interaction first and the data model second. The outside → in approach is one of the reasons I love the architecture; it fits well into a BDD style, which further promotes testability.

The important thing to know about DCI is that it's about more than just code. It's about process and people. It starts with principles behind Agile and Lean and extends those into code. The real benefit of following DCI is that it plays nicely with Agile and Lean. It's about code maintainability, responding to change, and decoupling what the system does (it's functionality) from what the system is (it's data model).

I'll take a behavior-driven approach to implementing DCI within a Rails app, starting with the Interactions and moving to the Data model. For the most part, I'm going to write code first then test. Of course, once you have a solid understanding of the components behind DCI, you can write tests first. I just don't feel test-first is a great way of explaining concepts.

User Stories

User stories are an important feature of DCI although not distinct to the architecture. They're the starting point of defining what the system does. One of the beauties of starting with user stories is that it fits well into an Agile process. Typically, we'll be given a story which defines our end-user feature. A simplified story might look like the following:

"As a user, I want to add a book to my cart."

At this point, we have a general idea of the feature we'll be implementing.

Aside: A more formal implementation of DCI would require turning a user story into a use case. The use case would then provide us with more clarification on the input, output, motivation, roles, etc.

Write Some Tests

We should have enough at this point to write an acceptance test for this feature. Let's use RSpec and Capybara:

spec/integration/add_to_cart_spec.rb

describe 'as a user' do
  it 'has a link to add the book to my cart' do
    @book = Book.new(:title => 'Lean Architecture')
    visit book_path(@book)
    page.should have_link('Add To Cart')
  end
end

In the spirit of BDD, we've started to identify how our domain model (our Data) will look. We know that Book will contain a title attribute. In the spirit of DCI, we've identified the Context for which this use case enacts and the Actors which play key parts. The Context is adding a book to the cart. The Actor we've identified is the User.

Realistically, we would add more tests to further cover this feature but the above suites us well for now.

The "Roles"

Actors play Roles. For this specific feature, we really only have one Actor, the User. The User plays the Role of a customer looking to add an item to their cart. Roles describe the algorithms used to define what the system does.

Let's code it up:

app/roles/customer.rb

module Customer
  def add_to_cart(book)
    self.cart << book
  end
end

Creating our Customer Role has helped tease out more information about our Data model, the User. We now know we'll need a #cart method on any Data objects which plays the Customer Role.

The Customer role defined above doesn't expose much about what #cart is. One design decision I made ahead of time, for the sake of simplicity, is to assume the cart will be stored in the database instead of the sesssion. The #cart method defined on any Actor playing the Customer Role should not be an elaborate implementation of a cart. I merely assume a simple association.

Roles also play nicely with polymorphism. The Customer Role could be played by any object who responds to the #cart method. The Role itself never knows what type of object it will augment, leaving that decision up to the Context.

Write Some Tests

Let's jump back into testing mode and write some tests around our newly created Role.

spec/roles/customer_spec.rb

describe Customer do
  let(:user) { User.new }
  let(:book) { Book.new }

  before do
    user.extend Customer
  end

  describe '#add_to_cart' do
    it 'puts the book in the cart' do
      user.add_to_cart(book)
      user.cart.should include(book)
    end
  end
end

The above test code also expresses how we will be using this Role, the Customer, within a given Context, adding a book to the cart. This makes the segway into actually writing the Context dead simple.

The "Context"

In DCI, the Context is the environment for which Data objects execute their Roles. There is always at least one Context for every one user story. Depending on the complexity of the user story, there may be more than one Context, possibly necessitating a story break-down. The goal of the Context is to connect Roles (what the system does) to Data objects (what the system is).

At this point, we know the Role we'll be using, the Customer, and we have a strong idea about the Data object we'll be augmenting, the User.

Let's code it up:

app/contexts/add_to_cart_context.rb

class AddToCartContext
  attr_reader :user, :book

  def self.call(user, book)
    AddToCartContext.new(user, book).call
  end

  def initialize(user, book)
    @user, @book = user, book
    @user.extend Customer
  end

  def call
    @user.add_to_cart(@book)
  end
end

Update: Jim Coplien's implementation of Contexts uses AddToCartContext#execute as the context trigger. To support Ruby idioms, procs and lambdas, the examples have been changed to use AddToCartContext#call.

There's a few key points to note:

  • A Context is defined as a class. The act of instantiating the class and calling it's #call method is known as triggering.
  • Having the class method AddToCartContext.call is simply a convenience method to aid in triggering.
  • The essence of DCI is in @user.extend Customer. Augmenting Data objects with Roles ad hoc is what allows for strong decoupling. There're a million ways to inject Roles into objects, #extend being one. In a followup article, I'll address other ways in which this can be accomplished.
  • Passing user and book objects to the Context can lead to naming collisions on Role methods. To help alleviate this, it would be acceptable to pass user_id and book_id into the Context and allow the Context to instantiate the associated objects.
  • A Context should expose the Actors for which it is enabling. In this case, attr_reader is used to expose @user and @book. @book isn't an Actor in this Context, however it's exposed for completeness.
  • Most noteably: You should rarely have to (impossibly) #unextend a Role from an object. A Data object will usually only play one Role at a time in a given Context. There should only be one Context per use case (emphasis: per use case, not user story). Therefore, we should rarely need to remove functionality or introduce naming collisions. In DCI, it is acceptable to inject multiple Roles into an object within a given Context. So the problem of naming collisions still resides but should rarely occur.

Write Some Tests

I'm generally not a huge proponent of mocking and stubbing but I think it's appropriate in the case of Contexts because we've already tested running code in our Role specs. At this point we're just testing the integration.

spec/contexts/add_to_cart_context_spec.rb

describe AddToCartContext do
  let(:user) { User.new }
  let(:book) { Book.new }

  it 'adds the book to the users cart' do
    context = AddToCartContext.new(user, book)
    context.user.should_recieve(:add_to_cart).with(context.book)
    context.call
  end
end

The main goal of the above code is to make sure we're calling the #add_to_cart method with the correct arguments. We do this by setting the expectation that the user Actor within the AddToCartContext should have it's #add_to_cart method called with book as an argument.

There's not much more to DCI. We've covered the Interaction between objects and the Context for which they interact. The important code has already been written. The only thing left is the dumb Data.

The "Data"

Data should be slim. A good rule of thumb is to never define methods on your models. This is not always the case. Better put: "Data object interfaces are simple and minimal: just enough to capture the domain properties, but without operations that are unique to any particular scenario" (Lean Architecture). The Data should really only consist of persistence-level methods, never how the persisted data is used. Let's look at the Book model for which we've already teased out the basic attributes.

class Book < ActiveRecord::Base
  validates :title, :presence => true
end

No methods. Just class-level definitions of persistence, association and data validation. The ways in which Book is used should not be a concern of the Book model. We could write some tests around the model, and we probably should. Testing validations and associations is fairly standard and I won't cover them here.

Keep your Data dumb.

Fitting Into Rails

There's not a whole lot to be said about fitting the above code into Rails. Simply put, we trigger our Context within the Controller.

app/controllers/book_controller.rb

class BookController < ApplicationController
  def add_to_cart
    AddToCartContext.call(current_user, Book.find(params[:id]))
  end
end

Here's a diagram illustrating how DCI compliments Rails MVC. The Context becomes a gateway between the user interface and the data model.

MVC + DCI

What We've Done

The following could warrant it's own article, but I want to briefly look at some of the benefits of structuring code with DCI.

  • We've highly decoupled the functionality of the system from how the data is actually stored. This gives us the added benefit of compression and easy polymorphism.
  • We've created readable code. It's easy to reason about the code both by the filenames and the algorithms within. It's all very well organized. See Uncle Bob's gripe about file-level readability.
  • Our Data model, what the system is, can remain stable while we progress and refactor Roles, what the system does.
  • We've come closer to representing the end-user mental model. This is the primary goal of MVC, something that has been skewed over time.

Yes, we're adding yet another layer of complexity. We have to keep track of Contexts and Roles on top of our traditional MVC. Contexts, specifically, exhibit more code. We've introduce a little more overhead. However, with this overhead comes a large degree of prosperity. As a developer or team of developers, it's your descretion on whether these benefits could resolve your business and engineering ailments.

Final Words

Problems with DCI exist as well. First, it requires a large paradigm shift. It's designed to compliment MVC (Model-View-Controller) so it fits well into Rails but it requires you to move all your code outside the controller and model. As we all know, the Rails community has a fetish for putting code in models and controllers. The paradigm shift is large, something that would require a large refactor for some apps. However, DCI could probably be refactored in on a case-by-case basis allowing apps to gradually shift from "fat models, skinny controllers" to DCI. Second, it potentially carries performance degradations, due to the fact that objects are extended ad hoc.

The main benefit of DCI in relation to the Ruby community is that it provides a structure for which to discuss maintainable code. There's been a lot of recent discussion in the vein of "'fat models, skinny controllers is bad'; don't put code in your controller OR your model, put it elsewhere." The problem is we're lacking guidance for where our code should live and how it should be structured. We don't want it in the model, we don't want it in the controller, and we certainly don't want it in the view. For most, adhering to these requirements leads to confusion, overengineering, and a general lack of consistency. DCI gives us a blueprint to break the Rails mold and create maintainable, testable, decoupled code.

Aside: There's been other work in this area. Avdi Grimm has a phenominal book called Objects on Rails which proposes alternative solutions.

Happy architecting!

This article is translated to Serbo-Croatian.

Further Reading

DCI: The King of the Open/Closed Principle
DCI With Ruby Refinements
DCI Role Injection in Ruby
Benchmarking DCI in Ruby

Posted by Mike Pack on 01/24/2012 at 12:58PM

Tags: architecture, rails, ruby, dci, testing


Benchmarking DCI in Ruby

I've recently become quite intrigued with the concepts behind DCI (Data Context and Interaction). I won't go too in depth about what DCI is or why you might use it, that's been discussed many times elsewhere. In short, DCI is an architecture which allows us to delineate our domain objects from the actual functions they perform. It mixes-in Roles (functionality) into our Data component when and only when that functionality it needed; when in Context. Most of the value DCI brings to the table derives from the way it forces you to abstract out behavior into testable modules.

What I'd like to do is take a look at the performance implications of using DCI in Ruby applications.

I think it should be said upfront that this is purely academic and may have minimal bearing within the ecosystem of a complex app. For this sake, I won't try to draw any vast conclusions.

How to use DCI in Ruby

DCI can be used in Ruby by augmenting your objects with Roles at runtime so that the necessary interactions are available to that object.

class User
  ...
end

module Runner
  def run
    ...
  end
end
 
user = User.new
user.extend Runner
user.run 

In more traditional, idiomatic Ruby you would normally just include the module while defining the class:

class User
  include Runner
  ...
end

user = User.new
user.run 

Hypothesis

Since every #extend called carries some memory and processing implications, lately I've been wondering if, while using DCI, we could be incurring a performance hit when extending many objects ad hoc. I decided to profile this to understand if we could be blindly degrading performance and whether there are optimization techniques I should be aware of.

My process involves taking the most simplified example (shown in the above snippets) and benchmarking the traditional approach against the DCI-inclined approach.

I'm running these benchmarks on a MacBook Pro - 2.2 GHz - 8 GB memory.

The Runner Module

Here's the Runner module used in the following examples. It's just one method that does some arbitrary calculation.

runner.rb

module Runner
  def run
    Math.tan(Math::PI / 4)
  end
end

Ruby Benchmark

Using Ruby's Benchmark library, we can extrapolate the amount of time taken for these processes to execute. First, we'll benchmark the traditional, idiomatic Ruby way: using an include to augment the class.

include_bm.rb

require 'benchmark'
require './runner'

class IncludeUser
  include Runner
end

Benchmark.bm do |bench|
  3.times do
    bench.report('include') do
      1000000.times do
        user = IncludeUser.new
        user.run
      end
    end
  end
end
$ ruby include_bm.rb
         user       system     total       real
include  0.500000   0.000000   0.500000 (  0.497114)
include  0.500000   0.000000   0.500000 (  0.497363)
include  0.490000   0.000000   0.490000 (  0.497342)

The results of this benchmark tell us that executing 1 million "run" operations results in roughly 0.5 seconds.

Let's look at how this compares to the DCI implementation.

dci_bm.rb

require 'benchmark'
require './runner'

class DCIUser; end

Benchmark.bm do |bench|
  3.times do
    bench.report('DCI') do
      1000000.times do
        user = DCIUser.new
        user.extend Runner
        user.run
      end
    end
  end
end
$ ruby dci_bm.rb
     user       system     total       real
DCI  8.430000   0.000000   8.430000 (  8.429382)
DCI  8.490000   0.010000   8.500000 (  8.486804)
DCI  8.450000   0.010000   8.460000 (  8.447363)

Quite a difference! It's probably safe to say at this point that calling extend 1 million times is a lot less performant than including the module one time as the class is defined. The reasoning is simple. Including the module once injects it in the user objects' lookup hierarchy. When the run method is called, the hierarchy is traversed and the method is fetched. In the traditional (include) approach, the module never leaves or reenters the hierarchy after it's been defined. Conversely, in DCI, the module enters the hierarchy each time extend is called.

Let's profile these two approaches and discover why they're so different.

perftools.rb

Assuming the same class/module structure as above, let's use perftools.rb to profile their execution. Using perftools.rb is a two-step process. First, generate the profile: a summary of where the code is spending it's time. Second, display the profile in the designated format. To visualize the components, we'll genereate graphs using the GIF format. You'll need the dot tool in order to generate graphs. Check out this presentation for more info on using perftools.rb.

Let's first observe the traditional approach:

include_profile.rb

require 'perftools'
require './runner'

class IncludeUser
  include Runner
end

PerfTools::CpuProfiler.start('/tmp/include_profile') do
  1000000.times do
    user = IncludeUser.new
    user.run
  end
end
$ ruby include_profile.rb
$ pprof.rb --gif /tmp/include_profile > include_profile.gif

include_profile.gif

The above graph tell us that most of the execution time is happening in the iteration of our test loop. Barely any time is spent creating the objects or executing the arbitrary math calculation. More info on reading the pprof.rb output can be found here.

Now let's take a look at the DCI approach:

dci_profile.rb

require 'perftools'
require './runner'

class DCIUser; end

PerfTools::CpuProfiler.start('/tmp/dci_profile') do
  1000000.times do
    user = DCIUser.new
    user.extend Runner
    user.run
  end
end
$ ruby dci_profile.rb
$ pprof.rb --gif /tmp/dci_profile > dci_profile.gif

dci_profile.gif

The above results tell us that almost half the time is spent extending objects at runtime through Module#extend_object. In this example, the time spent iterating over our test case is dwarfed against the time taken to extend objects. So, after profiling we can verify that extending the object is indeed taking up most of our time.

ObjectSpace.count_objects

Let's compare how the number of objects in memory stack up with the two implementations. Ruby 1.9 provides us with the ObjectSpace.count_objects method to inspect all objects currently initialized in memory. It's important to turn off garbage collection as it may be invoked mid-test, skewing the results. Here is the module used to inspect the number of objects currently in memory. It's a modified version of Aaron Patterson's implementation.

allocation.rb

module Allocation
  def self.count
    GC.disable
    before = ObjectSpace.count_objects
    yield
    after = ObjectSpace.count_objects
    after.each { |k,v| after[k] = v - before[k] }
    GC.enable
    after
  end
end

This method turns off the garbage collector, records the number of objects pre-benchmark, runs the benchmark, records the number of objects post-benchmark, then compiles the difference between the two. Let's gather more information by exracting object allocations.

include_space.rb

require './runner'
require './allocation'

class IncludeUser
  include Runner
end

p(Allocation.count do
  1000000.times do
    user = IncludeUser.new
    user.run
  end
end)
$ ruby include_space.rb
{:TOTAL=>2995684, :FREE=>-4344, :T_OBJECT=>1000000, :T_CLASS=>0, :T_MODULE=>0, :T_FLOAT=>2000000, :T_STRING=>27, :T_REGEXP=>0, :T_ARRAY=>0, :T_HASH=>1, :T_BIGNUM=>0, :T_FILE=>0, :T_DATA=>0, :T_COMPLEX=>0, :T_NODE=>0, :T_ICLASS=>0}

Most of the keys in the printed hash refer to Ruby types. The ones we're interested in are :TOTAL, :T_CLASS, :T_ICLASS. The meaning of these keys isn't very well documented but the Ruby source hints at them. Here's my understanding:

:TOTAL is the total number of objects present in memory.
:T_CLASS is the total number of classes that have been declared (in memory).
:T_ICLASS is the total number of internal use classes, or iClasses, used when modules are mixed in.

The DCI approach:

dci_space.rb

require './runner'
require './allocation'

class DCIUser; end

p(Allocation.count do
  1000000.times do
    user = DCIUser.new
    user.extend Runner
    user.run
  end
end)
$ ruby dci_space.rb
{:TOTAL=>4995536, :FREE=>-4492, :T_OBJECT=>1000000, :T_CLASS=>1000000, :T_MODULE=>0, :T_FLOAT=>2000000, :T_STRING=>27, :T_REGEXP=>0, :T_ARRAY=>0, :T_HASH=>1, :T_BIGNUM=>0, :T_FILE=>0, :T_DATA=>0, :T_COMPLEX=>0, :T_NODE=>0, :T_ICLASS=>1000000}

Let's looks at the significant differences between the two:

# Include
{:TOTAL=>2995684, :FREE=>-4344, :T_CLASS=>0, :T_ICLASS=>0}

# DCI
{:TOTAL=>4995536, :FREE=>-4492, :T_CLASS=>1000000, :T_ICLASS=>1000000}

These results expose a few facts about the two implementations.

  • The DCI approach uses roughly 40% more objects in memory.
  • The DCI approach initializes many more classes and internal (mixed-in) classes.

Wrapping It Up

As I said at the beginning, it's quite hard to determine how, if at all, using DCI will affect the performance of a real world application. Certainly, a single web request will never invoke 1 million inclusions of a module at runtime.

What this does show us is that Ruby is not optimized for this architecture. In idiomatic Ruby, modules are usually included during the class definition. It's possible that languages, like Scala, with built-in tools to extend objects ad hoc perform better than Ruby. Scala's traits provide high-level support for this type of functionality and are optimized for use with DCI.

I'm still quite interested in DCI. Specifically, in optimizations for Ruby. I'm also quite interested in running these benchmarks against a production app, something that'll just have to wait.

All the code used here can be found on github.

Happy benchmarking!

Posted by Mike Pack on 01/17/2012 at 12:46PM

Tags: dci, ruby, rails, performance, benchmarking, profiling, architecture