DCI (Data-Context-Interaction) – what is it, and when should I use it?
If you’re a hardcore, 10-hours-a-day Rails developer, you already know about DCI. For the rest of you, here’s a gentle introduction.
Rails developers (sorry, I just can’t force myself to use the word “Rubyists”) are an interesting lot. They care deeply about design and architecture. Deeply. In terms of application characteristics, they have a place at the head table alongside things normal folks care about – things like stability and functional fitness for duty.
So, while most of the world has been contentedly adhering to design principles and patterns like MVC and Model-Driven Design, Rails developers have been searching for The Next Big Thing. Some people feel like MVC drives a person to include too much code in a controller. Others tend to worry about too much code in models.
Before introducing DCI, I’ll offer an opinion of it and the role it should play in your bag of design tools. DCI is a sound, common-sense design solution – a way to keep an application’s implementation sane (full of concise, loosely-coupled, easily-understood classes) when scope and complexity might begin to be a challenge in a straight-up MVC design. That said, if you run across a colleague in the hallway who emphatically claims that DCI ought to be the new way all Rails applications are built, just give him a simple smile, pat him on the head, and move along, taking heart in your more measured and wise view of all things Rails.
Now then. DCI. Data-Context-Interaction. (Good acronyms, BTW, always use three letters.) The core concept of DCI is a recognition that entities – in Rails, models – do not exhibit a static set of behaviors or responsibilities. Rather, those behaviors and responsibilities depend on the context in which the entity is acting. Consider a model instance you’re probably most familiar with. You. If you were a Rails model, you might have the following method definitions in your internal wiring:
def wake
def wash
def brush
def pour
def drink
def chew
def swallow
def read
def walk
def board_train
def sit
def exit_train
def explain_dci_to_colleague
def think_abstractly
def write_test
def write_code
def run_tests
You get the picture. You are a complex example of a model. You do thousands, perhaps millions of things (I guess that number depends on the degree of abstraction in your “things” taxonomy, and perhaps on the number of hours a day you sit in front of the TV). If one were to include the code that implements all of these methods in one class, you’d have a pretty ugly, monolithic implementation.
How then, do we begin to decompose such complex model behavior into a set of cohesive, loosely-coupled, easily-understood classes? If you ponder the above list of behaviors long enough, you’ll notice something interesting. Certain behaviors have affinities with one another. Wake, wash, brush – the morning routine. Pour, drink, chew and swallow? Obvious. Walk, board_train, sit, exit_train? Commuting. The remainder? Software engineering, of course.
In DCI, we formally recognize that behaviors often belong to roles that a model object can play, and that those roles are always played within one or more contexts. In our little example, the “Eater” role might be played in three contexts each day (breakfast, lunch and dinner). We might, then, move all of the behavior related to food and beverage consumption into a class that represents the “Eater” role, the software design and development behavior into a “Software Engineer” class, etc.
If you’re with me so far, and you really do implement the “Software Engineer” behavior, you might have a forward-leaning question in your head about now: “If we move role-specific behavior out of the model class, how do we access it when we need it?” Great question. This is where dynamic languages like Ruby come to the rescue.
Instead of moving our role-specific behavior into separate classes, we can move the behavior into modules. Modules that can be “mixed in” at runtime using the extends keyword to put all that wonderful behavior where it belongs – on our Person class – but only when it really needs to be there.
Which brings us to the ‘C’ in DCI – Context. A Person needs to behave as an Eater only in the Context of a meal. A Person needs to behave as a Software Engineer only in the context of Development Project. In DCI, we formalize these contexts as classes, which see to the extension of models with appropriate roles, and then invoke the appropriate behaviors. In addition to simply adorning models with context-specific behavior, contexts can also orchestrate behavior of related models (people and trains, for example). For Gang of Four pattern afficionados, a Context looks a bit like a Command – there are expectations as to its structure (its constructor triggers its implemented behavior, for example), but it’s free to do anything it wants within that behavior. In that sense, this is a Structural pattern (as opposed to a Behavioral pattern).
One nice side effect of the full DCI pattern is that models are extended within the scope of a context; when the work of that context is done, that scope is exited, and all model instances discarded. This means there is no need to worry about removing temporarily-assigned behavior from model classes that might be used later – they are born and die within the cozy confines of a context.
If you want to go deeper and take a look at a simple example complete with Ruby code, Mike Pack has written an informative blog post that does just that. Just remember as you go that DCI, like any design pattern, is but a tool in your kit, and that one must always remember the Agile advice to “do the simplest thing that works.” In any Rails application, there are likely to be models that are used very simply (basic CRUD operations). Basic MVC and direct implementation of behavior on those models is perfectly adequate. Those models that represent the “crown jewels” of your application, on the other hand, are likely to exhibit complex behavior, and DCI might be a great way to wrestle that complexity to the ground.
The Declining Role of the Design Pattern
With the ascension over the last half-decade of Ruby into the upper echelon of programming languages for business applications, there has been ever-increasing demand for Ruby developers. Which means there are a lot of folks who cut their OO teeth on languages like Java and C# who now find themselves swimming in Ruby’s somewhat unfamiliar, crimson-tinted waters.
Those of us who have been working in strongly-typed OO languages for a very long time – since, say, the GOF Design Patterns book was first published – learned not only OO concepts and the syntactic idiosyncrasies of our language, but also design patterns – commonly-accepted ways of solving recurring problems in our applications.
If you’ve been with Java over the years – say, from Java 3 to 4 to 5, you’ve seen the language evolve to include features formerly relegated to the domain of the design pattern. Java’s for-each loop is one such example – once that became a language feature, it was suddenly less important to know about the more generalized Iterator pattern.
Enter Ruby (and dynamic languages in general). It turns out that many of the design patterns in that grand old Gang of Four book were situational – they apply generally to strongly-typed languages. If there is one “metapattern” that describes most of the original design patterns, it is this: “Use composition rather than inheritance in order to introduce runtime flexibility into your designs.” Quite simple: object composition – relationships between objects – can be changed at runtime in a strongly-typed language like Java. Inheritance – the only other way of introducing externally-defined capabilities to a class – cannot.
In Ruby, the very nature of the language makes much of that line of thinking obsolete. Through its dynamic nature, Ruby classes can be modified programmatically, at runtime. This statement often scares the daylights out of a Java developer when it hits them for the first time. Upon closer inspection, though, this language feature can be used to great effect. Behavior and associated information can be ascribed to specific instances of classes based on the situation in which they find themselves. Imagine a Person object who happens to walk into a busy bank and gets into the back of a queue. In a Ruby system describing this Person, we could blend in methods like start_waiting and elapsed_wait_time? – and those methods could then be discarded later when the Person finishes its transaction and leaves the bank.
How might we have done this in Java? After all, situationally-appropriate behavior is not unique to Ruby – it’s a generalized concept that could be expressed in any language. In Java, where class behavior is fixed at compile time, we use a specialized form of composition. The Gang of Four called it the Decorator pattern. So we have a situation where an inherent language feature of one language (Ruby) completely obliterates the need for a design pattern that helped legions of OO problem-solvers in earlier languages.
It turns out that the evolution away from patterns toward language features is something that has been noted by many others. Suggested reading for those interested:
- http://designpatternsinruby.com/section01/article.html
- http://lukeredpath.co.uk/blog/decorator-pattern-with-ruby-in-8-lines.html
