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.