This article is a practical introduction to DCI (Data Context Interaction). I’m not going to explain all theory behind it. Instead, I’m going to show you what kind of problems DCI is trying to solve and how it can be implemented in Ruby.
What OO Does Well
Let’s start by looking at some problems where traditional object oriented programming does a good job.
Object oriented programming is very good at capturing state. Classes, fields, and properties are powerful features allowing you to define state and work with it explicitly. Since we have these means of expressing state built into programming languages, reasoning about an object’s state is straightforward both at compile time and runtime. At compile time we can look at an object’s class definition. At runtime we can ask an object about its fields.
Another problem that is solved quite well by all object oriented languages is expressing operations associated with an object’s state. Such operations don’t involve any collaborations. They are local to the owning object. We express local operations by defining methods for a class. When we define a method for a class and create an object of the class, we know that the object will have that method. It’s pretty descriptive.
String is a good example of an object that has only local operations. Every String has an array of bytes or characters, and all operations work with that array. This object is self-contained; no collaborations with other objects are required. I don’t think anyone had any problems understanding how to use a String. This is the kind of problem OO languages were built to solve.
What OO Fails to Do
What object oriented programming fails to do is express collaborations between objects. To show you exactly what I mean, let’s take a look at two system operations (two use cases) requiring the same group of objects collaborating with each other.
Use Case 1
Here we have a system operation with four objects talking to each other.
Use Case 2
Here we have another use case, another system operation using the same group of objects. But you can see the collaboration pattern is different. The messages are different.
System Operations Aren’t Represented In The Code
We saw two system operations implementing Use Case 1 and Use Case 2. How do we represent them in the code? Ideally, I’d like to have the ability to open one file and figure out the collaboration pattern of the use case I’m working on. If I’m working on Use Case 1, I don’t want to know anything about Use Case 2. That’s what I’d consider a successful representation of system operations in the code.
Unfortunately, the traditional object oriented programming doesn’t give us any means of doing that. It gives us some tools to express the state of objects, and attach local behavior to those objects. But we don’t have any good ways to describe how objects communicate at runtime to execute a use case. Therefore, system operations aren’t represented in the code.
Source Code != Runtime
In the end, we still write code and program system operations somehow. How do we do it? We split them into lots of small methods that we put into lots of different objects.
What we see here is all the methods required by all the use cases are jammed into these objects. The methods required to perform the first use case are green, the second use case are red. In addition, these objects have some local methods. These local methods are used by the green and red methods.
The problem this picture illustrates is that the source code doesn’t reflect what happens at runtime. The source code tells us about four separate objects with a whole lot of methods in each of them. The runtime tells us that we have the four objects talking to each other and only a small subset of those methods is relevant to a particular use case. This mismatch makes programs hard to understand. The source code tells us one story, the runtime tells us a completely different story.
On top of that, there is no way to define system operations (use cases) explicitly, so we have to trace all method calls to get an idea of what is going on. There is no file we can open to figure out a particular use case. Even worse, since all classes contain methods for lots of different use cases, we have to spend a lot of time filtering them out.
DCI to the Rescue
DCI is a paradigm invented by Trygve Reenskaug (the inventor of the MVC pattern) to solve these problems.
Use Case 1 (DCI)
Let’s take at a look at the first use case implemented in the DCI style.
What we have here is the separation of the stable part of the system, containing only data and local methods, from the use case. All traditional object oriented techniques can be used to model the stable part. In particular, I’d recommend using domain driven design techniques such as aggregates and repositories. But there is no contextual behavior there, and no interactions – only local methods.
How Do We Model Interactions?
We have a new abstraction for describing interactions: the context. It’s a class including all roles for a given use case. Every role is a collaborator in the interaction and it is played by an object. As you can see, the contextual behavior is concentrated in the roles. The context just assigns the roles to the objects and after that triggers the interaction.
Use Case 2 (DCI)
The second use case implemented in the DCI style:
I’d like to point out that our objects (Object A-D) stay the same. We didn’t have to add any methods to support the second use case. All the methods we have there are fundamental, self-contained, and local. All use cases specific behavior was extracted into the contexts and roles.
Another thing is, we don’t see the red and green methods at the same time. Every context contains only methods required to execute itself.
It may sound too abstract, so let’s take a look at a code example to see how it can be implemented in Ruby.
This is a hello world example for DCI. Everyone interested in DCI starts by transferring money from one account to another.
I realize the example I’m about to show is oversimplified. And since it’s so simple, it can be successfully implemented using services, regular entities, or functions. So look at this example as an illustration of how you would structure your code.
As we’re talking about transferring money from one account to another, we’ll need to store information about accounts somehow. The Account class is responsible for doing this. It stores an account’s balance and list of transactions.
class Accountdef decrease_balance(amount); enddef increase_balance(amount); enddef balance; enddef update_log(message, amount); enddef self.find(id); endend
As you can see, all the methods here are local and context independent. Account knows nothing about transferring money. It’s only responsible for increasing and decreasing its balance. The logic of transferring money is in the context:
class TransferringMoneyinclude Contextdef self.transfer source_account_id, destination_account_id, amountsource = Account.find(source_account_id)destination = Account.find(destination_account_id)TransferringMoney.new(source, destination).transfer amountendattr_reader :source_account, :destination_accountdef initialize source_account, destination_account@source_account = source_account.extend SourceAccount@destination_account = destination_account.extend DestinationAccountenddef transfer amountin_context dosource_account.transfer_out amountendend...end
I’m fetching two accounts from the database, then I’m instantiating the context, and then calling ‘transfer’. You may have noticed I’m passing the two accounts to the constructor and the amount to the
transfer method. By doing that I’m trying to communicate which objects are actors in this interaction and which are just data. The accounts are actors, they have behavior. The amount is data.
Next, I’m assigning the roles to the account objects in my constructor. I’m teaching these data objects how to be a source account and a destination account.
Finally, I’m triggering this interaction by calling “transfer_out” on the source account. In this example, the context just triggers an interaction, but in some complicated cases it can also coordinate actors.
Now let’s take a look at how the roles are implemented:
class TransferringMoneyinclude Context...def transfer amount...endmodule SourceAccountinclude ContextAccssordef transfer_out amountraise "Insufficient funds" if balance < amountdecrease_balance amountcontext.destination_account.transfer_in amountupdate_log "Transferred out", amountendendmodule DestinationAccountinclude ContextAccssordef transfer_in amountincrease_balance amountupdate_log "Transferred in", amountendendend
First, I’m checking that the source account has enough money. Then, I’m decreasing the balance. After that, I’m getting the destination account through the context variable to tell it to receive the money.
There are a few interesting things here:
* The separation between the stable behavior and contextual behavior. Account is a dump class that only knows how to manipulate data. All the checks, all the business logic is in the context.
* The roles access other collaborators through the context variable. Once again, it’s done to separate actors from data. If I pass everything as an argument, how will I know what is an actor and what is not? Therefore, all the actors are accessed through the context, and all data objects are passed as arguments.
The context and both roles:
class TransferringMoneyinclude Contextdef self.transfer source_account_id, destination_account_id, amountsource = Account.find(source_account_id)destination = Account.find(destination_account_id)TransferringMoney.new(source, destination).transfer amountendattr_reader :source_account, :destination_accountdef initialize source_account, destination_account@source_account = source_account.extend SourceAccount@destination_account = destination_account.extend DestinationAccountenddef transfer amountin_context dosource_account.transfer_out amountendendmodule SourceAccountinclude ContextAccssordef transfer_out amountraise "Insufficient funds" if balance < amountdecrease_balance amountcontext.destination_account.transfer_in amountupdate_log "Transferred out", amountendendmodule DestinationAccountinclude ContextAccssordef transfer_in amountincrease_balance amountupdate_log "Transferred in", amountendendend
What We Got
The problem with the traditional object orientation is that the algorithm gets scattered across many different files. It’s solved by DCI. When you want to know how a particular use case is implemented you need to open only one file.
A context contains only the methods that are part of the use case it represents. So you don’t have to scan through dozens (or even hundreds) of methods that have nothing with the problem you are working on.
“What the system is” and “What the system does”
“What the system is” is all data objects and their local methods. Usually, this part of the system is super stable. “What the system does” is contextual behavior that changes rapidly. Separating stable parts from rapidly changing ones is vital for building stable software. And DCI provides this separation:
* A DCI Class says everything about the inside of an object and nothing about its neighbors (“What the system is”).
* A DCI Context says everything about a network of communicating objects and nothing about their insides (“What the system does”).
Source Code == Runtime
Another thing is, the source code matches the runtime. The runtime tells us that there are two accounts and an amount. That’s what you see when you open the context.
Roles are explicit
The greatest thing DCI brings is explicit roles. A lot of designers agree that objects by themselves don’t have responsibilities – roles do. For instance, take me as an example of an object. I have the following properties: I’s born in Russian; my name is Victor; my weight is about 65kg. Do these properties really imply some high level responsibilities? They don’t. But when I come home and start playing the role of a husband, I become responsible for all that husband’s stuff. So objects play roles. The fact that roles aren’t first class citizens in the traditional object orientation is just wrong.
If you think this idea is interesting, you should check out the following resources:
If you prefer reading books, these are three books I can recommend:
- Clean Ruby by Jim Gay. This book is filling the need for a practical introduction to DCI for Rubyists. It’s still work in progress, but it looks very promising.
- Lean Architecture: for Agile Software Development by James O. Coplien and Gertrud Bjørnvig.
“This is not only the market’s first book on Lean Architecture and Agile development, but it clarifies the difference between these two powerful approaches and shows how they can be combined. It is also the first book to present Trygve Reenskaug’s new software architecture called DCI: Data, Context, and Interaction. DCI is to the programmer as the classic MVC architecture is to the end user: a software approach that puts people first.”
- Object Design: Roles, Responsibilities, and Collaborations by Rebecca Wirfs-Brock and Alan McKean. As this book was published in 2002, it doesn’t cover DCI. But it’s a great material on object design, using roles, and modelling collaborations. These topics are closely related to the core ideas behind DCI.