Too often, the abbreviation “DCI” began to appear in Western blogs and Twitter. I was surprised by the fact that there is almost no information on this topic, only in
Ruby NoName Podcast S04E09 was mentioned about it. Curiosity took it up, and I decided to find out more about this mysterious word. In the process of searching, I came across a good article written in English by my countryman, Victor Savkin. This article, without abundant theory, shows with practical examples what DCI is. Further, the story will go on behalf of Victor.
Advantages of OO
Let's start with an overview of the problems where traditional object-oriented programming has proven itself from the best side.
Object-oriented programming has shown itself well in managing the state of the object. Classes, fields, and properties are powerful tools that allow you to identify and operate on a state. Since we have these state display tools built into programming languages, we can conclude about it, both at compile time and runtime. At compile time, we can look at the definition of a class object, and in runtime we can interrogate an object about its fields.
')
Another problem that is solved quite well by all object-oriented languages is the mapping of operations related to the state of the object. Such operations are not involved in any kind of joint work. They are local to the owning object. We express local operations by defining class methods. When we define a method for a class and create an instance of it, we know that it will have this method. This is pretty obvious.
A good example of an object that has only local operations is the Ruby class
String . Each
String has an array of bytes or characters. All his operations work with this array. This object is independent - it does not need to interact with other objects. I doubt that anyone may not understand how to use a
String . To solve such problems, OO languages were built.
OO drawbacks
OO loses in the interaction display of objects. In order to show what I mean, let's take a look at two system operations (two usage scenarios) of objects from the same group that interact with each other.
The first use case

The picture shows the system operation of four objects that communicate with each other.
Second use case

And here another usage scenario is presented, another system operation using the same group of objects. But as you can see, the model of interaction between objects and messages is different.
The code does not display system operations.
We have seen two system operations expressed in
Use Case 1 and
Use Case 2 . How do we display them in code? In the ideal case, I would like to be able to open one file and understand the object interaction model in the use case I am working on. If I'm working on
Use Case 1 , I don't want to know anything about
Use Case 2 . This is what I consider to be a successful mapping of usage scenarios in code.
Unfortunately, traditional object-oriented programming does not give us any opportunity to do this. It gives us some tools to display the state of objects and assign them local behavior. But we don’t have any good ways to describe the interaction of objects with each other in runtime to execute the use case. Therefore, system operations are not displayed in the code.
Source Code! = Runtime
In the end, we still write code and program system operations in some way. How do we do it? We divide them into many small methods and assign them to a bunch of different objects.

Here we see that all the methods needed for all usage scenarios are trapped in these objects. The methods for executing the first scenario are highlighted in green, for the second - red. In addition, these objects have some local methods. They are used by blue and red methods.
The problem that is illustrated in the picture is that the source code does not display what is happening in runtime. The source code tells us about four separate objects, with a bunch of methods in each of them. Rantaim tells us that there are four objects that communicate with each other, and only a small subset of these methods is relevant to this use case. This discrepancy complicates the understanding of programs. The source code shows us one story, and runtime a completely different one.
In addition, there is no way to define system operations (usage scenarios) explicitly, so we have to track all the method calls in order to get an idea of what is happening in the program. We do not have a file that we could open to understand this usage scenario. Even worse, since all classes contain methods for lots of usage scenarios, we have to spend a lot of time filtering out the methods we need.
DCI to the rescue!
DCI (Data, context, interaction) is a paradigm created by Trygve Reenskaug (by MVC template) to solve these problems.
First use case (DCI)
Let's take a look at the first usage scenario expressed in DCI style.

Here we see the separation of the unchanged part of the system, containing only data and local methods, from this use case. All traditional object-oriented techniques can be used to model this fixed part. In particular, I would recommend using problem-oriented design techniques (Domain-driven design), such as aggregates and repositories, but there are no context-dependent behavior and interaction, there are only local methods.
How do we model the interaction?
We have a new abstraction to describe the interaction - context. A context is a class that contains all the roles for a given use case. Each role is a co-worker in the interaction and is played up by some object. As you can see, context-sensitive behavior is concentrated in roles. The context only assigns roles to objects and then initiates interaction.
Second Use Case (DCI)

I would like to point out that our Objects (Object AD) remain the same. We did not have to add any methods for the second use case. All presented methods are fundamental, self-sufficient and local. All scenarios for the use of individual behaviors were highlighted in context and roles.
It is also worth noting that we do not see the red and green methods at the same time. Each context contains only the methods necessary for its execution.
It may sound too abstract, so let's take a look at the sample code in Ruby.
Code example
Here is an example of
Hello World only for DCI. Anyone interested in DCI starts by transferring money, from one account to another.
I understand that this example is too simplified, and since it is so simple, it can be successfully expressed through services (services), regular objects (regular entities) or functions. So take a look at this example as an illustration of how you should structure your code.
Since we are talking about transferring money from one account to another, we will need to store information about accounts in some way. The
Account class is responsible for this. It stores information about the balance and the list of transactions.
class Account def decrease_balance(amount); end def increase_balance(amount); end def balance; end def update_log(message, amount); end def self.find(id); end end
As you can see, all methods here are local and context-independent. The account does not know anything about the transfer of money. He is only responsible for replenishing the balance and reading money from him. The logic of money transfer is in context:
class TransferringMoney include Context def self.transfer source_account_id, destination_account_id, amount source = Account.find(source_account_id) destination = Account.find(destination_account_id) TransferringMoney.new(source, destination).transfer amount end attr_reader :source_account, :destination_account def initialize source_account, destination_account @source_account = source_account.extend SourceAccount @destination_account = destination_account.extend DestinationAccount end def transfer amount in_context do source_account.transfer_out amount end end ... end
I request two accounts from the database, then create an instance of the context and call the
transfer method. You probably noticed that I transfer two accounts to the constructor, and the transfer amount to the
transfer method. By this, I want to show which objects are actors in this interaction, and which are only data. Accounts are actors, they have a behavior, and the transfer amount is already data.
Next, I assign roles to account objects in my constructor. I train these data objects to be a source account and a recipient account.
At the end, I initiate the interaction by calling the
transef_out method on the source account. In this example, the context only initiates the interaction, but in some difficult cases it can also coordinate the actors.
And now let's take a look at the implementation of roles:
class TransferringMoney include Context ... def transfer amount ... end module SourceAccount include ContextAccssor def transfer_out amount raise "Insufficient funds" if balance < amount decrease_balance amount context.destination_account.transfer_in amount update_log "Transferred out", amount end end module DestinationAccount include ContextAccssor def transfer_in amount increase_balance amount update_log "Transferred in", amount end end end
First, I check that the source account has enough money, then I deduct the amount of the transfer from the balance. After that, I call the recipient account through a context variable in order to inform him to get money.
Pay attention to a few interesting things:
- Separation of unchanged behavior and context-sensitive. The account class only knows how to manipulate its data. All checks, all business logic in context.
- Roles refer to other co-workers through a context variable. Once again, this is done to separate the actors from the data. If I conveyed everything as an argument, how would I know who was an actor and who wasn’t? For this reason, the actors should be accessed through a context variable, and all data objects should be passed as arguments.
Context and both roles:
class TransferringMoney include Context def self.transfer source_account_id, destination_account_id, amount source = Account.find(source_account_id) destination = Account.find(destination_account_id) TransferringMoney.new(source, destination).transfer amount end attr_reader :source_account, :destination_account def initialize source_account, destination_account @source_account = source_account.extend SourceAccount @destination_account = destination_account.extend DestinationAccount end def transfer amount in_context do source_account.transfer_out amount end end module SourceAccount include ContextAccssor def transfer_out amount raise "Insufficient funds" if balance < amount decrease_balance amount context.destination_account.transfer_in amount update_log "Transferred out", amount end end module DestinationAccount include ContextAccssor def transfer_in amount increase_balance amount update_log "Transferred in", amount end end end
What we have
Locality
DCI solves the problem of traditional object-oriented programming, when the algorithm is spread among many different files. If you want to know how a usage scenario is implemented, you will need to open only one file.
Focusing
The context contains only methods that are part of the execution script that it represents. So you do not have to scan dozens or even hundreds of methods that have nothing to do with the problem you are working on.
“What is the system” and “What is the system doing”
All data objects and their local methods express “What is the system”. Usually, this part of the system is unchanged. Context-sensitive, rapidly changing behavior is “What the system does”. Separation of unchanged parts from rapidly changing ones is one of the necessary conditions for building stable software. And DCI provides this separation:
- The data in DCI shows us everything about the contents of the object and nothing about its neighbors (“What is the system”)
- The context in DCI shows us everything about the network of interacting objects and nothing about their contents (“What the system does”)
Source Code == Runtime
Also, the source code coincides with runtime. Runtime shows us that we have two accounts and the amount of transfer. Here is what you see if you open the context.
Explicit roles
The main advantage of DCI is clearly defined roles. Quite a lot of designers agree that the objects themselves have no responsibilities, this is a matter of roles. For example, take me as an example of an object. I have the following properties: I was born in Russia, my name is Victor, my weight is around 65kg. Can these properties really express some high-level commitments? They can not. But when I return home and begin to play the role of a husband, I become responsible for all the marital affairs. So objects play a role. The fact that roles are not at the forefront of traditional object-oriented programming is wrong.
Sources
Actually the article itself:
If you are interested in this idea, I advise you to look at the following resources:
If you prefer reading books, I can recommend these three books to you:
- Clean Ruby by Jim Gay. This book addresses the need for a practical DCI presentation for rubists. She is still in development, but already looks promising.
- Lean Architecture: for Agile Software Development by James O. Coplien and Gertrud Bjørnvig. This is not only the first book on the market for coherent architecture and agile development, but it also explains the difference between these two powerful approaches and shows how they can be combined. This is also the first book containing a new software architecture from Trygve Reenskaug, called DCI: Data, Context, Interaction.
- Object Design: Roles, Responsibilities, and Collaborations by Rebecca Wirfs-Brock and Alan McKean. Since the book was released in 2002, it does not contain information on DCI. But it contains good material on designing objects, using roles, and modeling colleagues. These topics are very close to the core ideas of DCI.