Data Context Interaction: The Evolution of the Object Oriented Paradigm
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.
Ruby 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.
Code Example
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 Account def decrease_balance(amount); end def increase_balance(amount); end def balance; end def update_log(message, amount); end def self.find(id); endendAs 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 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’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 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 endendFirst, 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 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 endend
What We Got
Locality
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.
Focus
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.
Resources
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.





def transfer_out amount
raise "Insufficient funds" if balance < amount
decrease_balance amount
update_log "Transferred out", amount
end
def transfer amount
in_context do
source_account.transfer_out amount
destination_account.transfer_in amount
end
end
I'm from C# background. Hope this is not a silly question.
This is not a silly question. It really depends on how you think about the problem.
To make this example more concrete, imagine instead of accounts we have Alice and Bob. And Alice wants to give Bob fifty dollars.
* She can hand $50 to Bob. That's the way it's implemented right now.
* She can use some sort of middleman who will take the money from her and give it to Bob.
Both approaches are valid.
Keep in mind though, if you move too much logic into the context itself, you'll get the centralized control style. Your context will become the center of making all decisions. The way it's done right now is closer to the delegated control style. For this particular example, it doesn't really matter, but I find the delegated control style works much better for complex interactions.
Cheers,
Victor
def transfer amount
in_context do
source_account.transfer_out amount
destination_account.transfer_in amount
end
end
Instead of the source account role need to know about the destination account? Seen as that seems to be the responsibility of "TransferringMoney"
Thanks for your comment. Please, see me response to Frank Wang's question.
Cheers,
Victor
Great article, thank you very much.
Just one question, can I see the implementation of the Context module, I don't see the purpose of the in_context (except for it's semantic purpose)
in_context do
source_account.transfer_out amount
end
Thanks,
Thanks for your comment.
module ContextAccessor
def context
Thread.current[:context]
end
end
module Context
include ContextAccessor
def context=(ctx)
Thread.current[:context] = ctx
end
def in_context
old_context = self.context
self.context = self
res = yield
self.context = old_context
res
end
end
As you can see, in_context sets the context variable (it's a thread local global variable) that all role players can access.
In DCI, you can use one context inside another one. To support it we need to maintain a stack of instantiated contexts. That's what 'old_context = self.context' does.
Cheers,
Victor
"In DCI, you can use one context inside another one. To support it we need to maintain a stack of instantiated contexts."
I still don't understand why we need to maintain a stack of instantiated contexts in order to use on inside another... Could you elaborate, please?
Thanks!
class ContextA
....
def execute
in_context do #Here we set Thread.current[:context] to this instance of ContextA
# at this point, Thread.current[:context] is ContextA
B.new.execute # Here we set Thread.current[:context] to this instance of ContextB
# if we don't maintain the stack of contexts, Thread.current[:context] will be ContextB. That's wrong.
# if we do maintain the stack of contexts, we can restore the previous context from the stack (ContextA).
end
end
end
#
class ContextB
...
def execute
in_context do #Here we set Thread.current[:context] to this instance of ContextB
...
end
end
end
Thread.current[:context] is a global variable that all role players use to access the current context.
First, we instantiate and run ContextA. This new instance of ContextA becomes the current context. Thus, the global variable must point to it. So all the roles of ContextA will be able to access it.
Then, we instantiate ContextB inside ContextA, and this instance of ContextB becomes the current context. Therefore, we must change the global variable to point to it.
Finally, ContextB returns, and the first instance of ContextA becomes the current context again. Therefore, we must change the global variable to point to it.
If it's still confusing, I recommend you take a look at this paper:
http://folk.uio.no/trygver/2012/DCIExecutionModel-2.1.pdf
Cheers,
Victor
As a way to get solid work working for people quickly, this all has a huge appeal for me.
Thanks so much for this addition to my learning. It's very helpful. I WILL be digging into this further - because of your article.
Cheers,
Victor
I've took a TDD approach in implementing your example, you can find the result here https://github.com/balauru/dci-example, is this close to what you will write in an actual application?
Regards,
Thanks for your comment. Yup, it's close. Of course, a real world context is usually more complicated than that, but the structure is similar.
Cheers,
Victor
That's quite the Object-Oriented straw man that you've beaten up.
There's nothing about OO that prevents it from expressing "collaboration between objects". You are confusing the Ruby community's perseveration on the Single Responsibility Principle with OO.
SRP (Single Responsibility Principle) attempts to reduce the number of reasons that an object would change. The more fan out dependencies on an object (that is, the more objects that the current object directly messages) the more reasons that it has to change.
Reducing dependencies between objects, also known as "loose coupling", simplifies testing and facilitates changing the code.
However, due to lack of a canonical definition of OO, adding dependencies for the sake of defining a process can still be considered OO. For that matter, DCI smacks of the Mediator Pattern (GoF: http://en.wikipedia.org/wiki/Mediator_pattern).
If you want to read more, see http://evan.tiggerpalace.com/articles/2012/11/21/use-rails-until-it-hurts/