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. RubyString
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. [gist id=”3333664″] 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: [gist id=”3333670″] 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 thetransfer
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:
[gist id=”3333671″]
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:
[gist id=”3333675″]
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.
Frequently Asked Questions (FAQs) about DCI (Data, Context, Interaction)
What is the main difference between DCI and traditional Object-Oriented Programming (OOP)?
Traditional OOP focuses on the classification of objects based on their properties and behaviors, which are defined in classes. However, DCI extends this concept by introducing the idea of roles that objects can play in different contexts. In DCI, objects are not just defined by their classes, but also by the roles they can play in different interactions. This allows for more flexibility and adaptability in software design, as objects can change roles depending on the context.
How does DCI improve code readability and maintainability?
DCI improves code readability and maintainability by separating the concerns of what the system is (data) from how it behaves (context and interaction). This separation of concerns makes it easier to understand and modify the system’s behavior without affecting its underlying data structures. Moreover, by focusing on the interactions between objects rather than their individual behaviors, DCI promotes a more holistic understanding of the system’s behavior.
Can DCI be used with any programming language?
While DCI was initially developed for use with object-oriented programming languages, it can be adapted to other programming paradigms as well. The key is to understand the principles of DCI – data, context, and interaction – and apply them in a way that makes sense for the particular language and paradigm you are using.
What are the challenges in implementing DCI?
One of the main challenges in implementing DCI is that it requires a shift in mindset from traditional object-oriented programming. Developers need to think in terms of roles and interactions, rather than just classes and objects. Additionally, not all programming languages support the concept of roles natively, which can make the implementation of DCI more complex.
How does DCI handle data persistence?
In DCI, data persistence is typically handled by the data objects themselves. These objects are responsible for storing their own state and retrieving it when necessary. This approach allows for a clear separation between the system’s behavior (defined by the context and interactions) and its state (defined by the data objects).
Can DCI be used in conjunction with other design patterns?
Yes, DCI can be used in conjunction with other design patterns. In fact, DCI can be seen as a higher-level pattern that provides a framework for organizing and structuring other design patterns. By focusing on the interactions between objects, DCI can help to clarify the roles and responsibilities of different patterns within the system.
How does DCI handle error handling and exception management?
Error handling and exception management in DCI are typically handled by the context. The context is responsible for coordinating the interactions between objects, and as such, it is well-placed to handle any errors or exceptions that may occur during these interactions.
How does DCI support agile development practices?
DCI supports agile development practices by promoting a flexible and adaptable approach to software design. By separating the concerns of data, context, and interaction, DCI allows for changes to be made to the system’s behavior without affecting its underlying data structures. This makes it easier to respond to changing requirements and to iterate on the design quickly and effectively.
How does DCI handle concurrency and multi-threading?
Concurrency and multi-threading in DCI are typically handled by the context. The context is responsible for coordinating the interactions between objects, and as such, it can manage the execution of these interactions across multiple threads.
What are some practical examples of applications that use DCI?
DCI has been used in a variety of applications, ranging from web applications to embedded systems. Some examples include the Squeak/Smalltalk programming environment, the Qi4j framework for Java, and the Marvin conversational AI platform. These applications demonstrate the flexibility and adaptability of DCI in different domains and contexts.
Victor Savkin is a developer interested in Domain Driven Design, Enterprise Architecture, and Domain Specific Languages. He works on large enterprise applications written in Rails. Being a language nerd he spends a lot of his time playing with Smalltalk, Groovy, Scala, Clojure, Ruby, and Ioke.