DCI Architecture Is Visionary
Data, Context, and Interaction — the way to move your object-orientation to the next level. Unfortunately, there's no good way to implement it in Java yet.
Join the DZone community and get the full member experience.
Join For FreeWelcome in the sixth installment of my architecture series. So far we have covered 5 similar architectural styles, from the Layered Architecture to the Clean Architecture. In this article, we’ll look at something substantially different – the DCI architecture by James Coplien and Trygve Reenskaug.
What Is DCI?
As I said before, the DCI architecture is different than the five styles that we previously discussed. The main difference between DCI and the rest is that it tells you (close to) nothing about layers and code organization, in the sense of what package should your classes go into. So what does it tell us?
DCI stands for Data, Context, and Interaction. This name should resonate with the way most people understand object-orientation. We have some context i.e. a user’s incoming request, we retrieve some data, i.e. the objects, and we make those objects interact to satisfy the user’s request. So far, everything should feel common. Let’s move to the uncommon part.
Data
The Data part of DCI consists of domain objects à la entities in DDD or Clean Architecture. The major difference between DCI‘s data object and a typical entity is that the data object is relatively stupid. It’s not anemic. It can still contain important domain methods that would preserve it’s invariants etc., but it shouldn’t contain a single method that is specific to an application’s use case and not the domain itself. If this feels blurry, think about the concept of printers that some people promote:
class Book {
private String title, description;
// stuff
void print(Printer printer) {
printer.print("title", title);
printer.print("description", description);
}
}
They claim that this approach to serializing objects is better because it promotes better encapsulation and hence better object-orientation. In DCI's data, such an approach could not take place since printing is part of a use case and not a part of the domain object.
Interactions
So where does the use-case-specific code go? The answer is: in the object roles. The concept of roles is pretty unique to DCI – I haven’t seen it in any other major architectural style. The roles are supposed to dynamically extend the data object’s behavior with the use case-specific functionality. Since such functionality might want to operate on object’s state (like the printer above), an object role should preferably have access to the object’s internals e.g. via accessors or simple methods. In pseudo-code, our example with an extracted role could look like this:
class Book {
private String title, description;
// stuff
}
role PrintedBook {
void print(Printer printer) {
printer.print("title", title);
printer.print("description", description);
}
}
// usage:
Book imJustData = books.findById(id);
PrintedBook youCanPrintMe = imJustData extendedBy PrintedBook
I see two major benefits of this approach. First, the domain object is not cluttered by peripheral stuff that it would not contain if not for some use case. Second, an object role can be played by objects of different classes, as long as they contain data and methods necessary to fulfill the role. In our example, we could print a magazine the same way we print a book (we should adjust the role‘s name then).
Context
The place where data objects are retrieved and roles assigned is called the context. This would be a rough equivalent of an application service or Clean Architecture’s use case interactor. Why only rough? Because ideally, a context should provide the roles with references to all collaborators in a use case, call one of the roles and do nothing else.
Think about a bank transfer. We have two accounts – source account and destination account. Obviously, the account is a data object, while source account and destination account are roles. When the source account decreases its own balance, it wants to let the destination account know that it should increase its balance. Once this is done, a success message is shown in the GUI. Does that mean that the SourceAccount should carry around a destination account and a GUI reference? And if there were more collaborators, should each of them contain a reference to each one it collaborates with. That could easily get out of hand. Hence, DCI assumes that object roles should know what collaborators they have based on the context in which they execute.
All Together
Let’s walk through things the way control flows in the application. A user presses a button that sends a use-case-related request. An application receives that request, instantiates an appropriate context, and passes the request to it. The context retrieves all data objects necessary to fulfill the use case‘s goal and assigns appropriate roles to them. Then, it sends a message to an object role that begins a series of interactions between the objects. If there is a need to communicate something to the user, it’s also done by the roles. Once it’s complete, the user’s goal should be achieved.
The Essence of DCI
I see two special things about DCI: separating the stable from the ever-changing and having real networks of collaborating objects.
Separating the Stable
The idea of extracting the use case-specific logic to separate constructs called object roles might seem odd to you at first. So far I said that it prevents clutter and allows for better code reuse. That’s all true, but at a class level. At the perspective of the whole architecture, it has a far more profound meaning. We are separating things that change at different rates and for different reasons (Single Responsibility Principle, anyone?!).
Think about it. If you get your domain model right, or rather right enough, it should not change unless the domain itself changes – it’s STABLE. On the other hand, new use cases or parts of them that operate in this domain can flow in and out all the time. Businesspeople can change their mind. Add this feature, delete that, change something. The use case stuff is EVER-CHANGING and it changes mostly because people change their mind. Have you ever heard developers complaining that the architecture should provide a stable foundation for future development and it doesn’t do so? Well, it’s hard to achieve that if you keep mixing things up.
Networks of Collaborating Objects
It might seem surprising to you that I pointed it out. You know, isn’t it what the whole object-orientation is supposed to be about? And yet we keep writing code, in which communication looks like this:
There’s no collaboration. It’s just the service, calling methods one by one. In DCI, the communication would look like in the picture presented in the All Together section.
Implementing DCI
If you have thought all this time that such magic is impossible in Java, you were partly right. There is a project known as Apache Polygene, formerly known as Apache Zest and Qi4J. It allows you to mix classes into each other in the DCI-way, but the code produced when working with it is at least complex. For this reason, we’ll look at a Marvin example instead:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Marvin.Examples
{
public class MoneyTransfer
{
public MoneyTransfer(Account source,Account destination,decimal amount){
Source = source;
Destination = destination;
Amount = amount;
}
role Source{
void Withdraw(decimal amount){
Source.DecreaseBalance(amount);
}
void Transfer(decimal amount){
Console.WriteLine("Source balance is: " + Source.Balance);
Console.WriteLine("Destination balance is: " + Destination.Balance);
Destination.Deposit(amount);
Source.Withdraw(amount);
Console.WriteLine("Source balance is now: " + Source.Balance);
Console.WriteLine("Destination balance is now: " + Destination.Balance);
}
}
role Destination
{
void Deposit(decimal amount){
Destination.IncreaseBalance(amount);
}
}
role Amount{}
public void Trans(){
Source.Transfer(Amount);
}
}
}
As you can see, in Marvin, the roles are defined inside the context and actual objects are assigned to them the same way you’d assign fields. To initiate the use case , we call the Trans()
method, which calls the first role: Source
. The source account is responsible for decreasing its own balance and telling Destination
to increase its balance. For the example to work, the Account
obviously needs to implement the IncreaseBalance
and DecreaseBalance
methods:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Marvin.Examples
{
public class Account{
public Account(ICollection<LedgerEntry> ledgers){
Ledgers = ledgers;
}
role Ledgers{
void AddEntry(string message,decimal amount){
Ledgers.Add(new LedgerEntry(message, amount));
}
decimal GetBalance(){
return ((ICollection<LedgerEntry>)Ledgers).Sum(e => e.Amount);
}
}
public decimal Balance{
get {
return Ledgers.GetBalance();
}
}
public void IncreaseBalance(decimal amount)
{
Ledgers.AddEntry("depositing",amount);
}
public void DecreaseBalance(decimal amount)
{
Ledgers.AddEntry("withdrawing",0-amount);
}
}
}
Of course, this is a toy example, so the whole DCI-thing looks more complex than the domain itself. Unfortunately, there aren’t any non-toy examples online — or I haven’t found them. If you want more like this one, including even some Java code, you can check them out on the DCI page.
Benefits of DCI
Since I haven’t seen or heard about a DCI application running in production, we will look at its theoretical benefits.
- Screaming++ – it doesn’t highlight just the use cases, it also highlights the roles that objects play in these.
- Even higher cohesion and lower coupling – there are no methods that take care of single use case peripheral concerns in your domain objects.
- Lean – by putting the stable at its core, it helps to eliminate unnecessary rework in the process of adding features to the application.
- Plays well with DDD – the domain model has an important place in the architecture, and it’s pure and clutter-free.
Drawbacks of DCI
- Learning curve – the concept is pretty complex and there aren’t too many good learning materials for it.
- Lack of good tools – it’s not really supported by any major programming language. You need to work around with existing mechanisms or niche tools like Polygene.
- Heaviness – it’s not an architecture for simple applications like CRUDs.
When to Use DCI?
Initially, I wanted to put here some shallow advice that the team needs to know the architecture and the tools etc. But I changed my mind. If you’re working for a company with over 5 employees in the Java field, don’t go there. Even if your team feels good enough to pull it off, there’s a high chance that you will produce something incomprehensible for your successors. And all the rework time saved by DCI, you’ll spend struggling with niche tools and lack of community support. I imagine that the situation could look a little different in other languages, but I can’t say for sure.
Summary
DCI changes the way in which we think about object-orientation on the architecture level. By separating roles out of objects and making them communicate in contexts, we create real networks of cooperating objects. The data part of our application, where the static picture of our domain lies, remains stable and uncluttered in the process of adding new features. Unfortunately, this cool vision suffers from a lack of good tooling and learning resources, especially in Java. But even if you shouldn’t go DCI in your projects right now, it’s an architecture worth knowing and remembering.
If you want to learn more about architecture and DCI, check out its Wikipedia page or the excellent book Lean Architecture for Agile Software Development.
Published at DZone with permission of Grzegorz Ziemoński, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments