Haxe is a nice multiplatform language which enables a full DCI implementation. If you don't know what DCI is, keep reading, you're in for a treat!
DCI stands for Data, Context, Interaction. The key aspects of the DCI architecture are:
- Separating what the system is (data) from what it does (function). Data and function have different rates of change so they should be separated, not as it currently is, put in classes together.
- Create a direct mapping from the user's mental model to code. The computer should think as the user, not the other way around.
- Make system behavior a first class entity.
- Great code readability with no surprises at runtime.
Install via haxelib: haxelib install haxedci
Then put -lib haxedci
into your hxml.
An ATM money transfer will be our simple DCI example and tutorial. (Thanks to Marc Grue for the original money transfer tutorial)
Let's start with a simple data class called Account
, containing a few methods:
class Account {
public var name(default, null) : String;
public var balance(default, null) : Int;
public function new(name, balance) {
this.name = name;
this.balance = balance;
}
public function increaseBalance(amount: Int) {
balance += amount;
}
public function decreaseBalance(amount: Int) {
balance -= amount;
}
}
This is what we in DCI sometimes call a "dumb" data class. It only "knows" about its own data and trivial ways to manipulate it. The concept of a transfer between two accounts is outside its responsibilities and we delegate this to a Context - the MoneyTransfer
Context class. In this way we can keep the Account class very slim and avoid that it gradually takes on more and more responsibilities for each use case it participates in.
From a users point of view we might think of a money transfer as
- "Move money from one account to another"
and after some more thought specify it further:
- "Withdraw amount from a source account and deposit the amount in a destination account"
That could be our "Mental Model" of a money transfer. Interacting concepts like our "Source" and "Destination" accounts of our mental model we call "Roles" in DCI, and we can define them and what they do to accomplish the money transfer in a DCI Context.
Our source code should map as closely to our mental model as possible so that we can confidently and easily overview and reason about how the objects will interact at runtime. We want no surprises at runtime. With DCI we have all runtime interactions right there! No need to look through endless convoluted abstractions, tiers, polymorphism etc to answer the reasonable question where is it actually happening?!
To use haxedci, you need to be able to create Contexts. Lets build the MoneyTransfer
class step-by-step from scratch:
Start by defining a class and let it implement dci.Context
.
class MoneyTransfer implements dci.Context {
}
Remember the mental model of a money transfer? "Withdraw amount from a source account and deposit the amount in a destination account". The three italicized nouns are the Roles that we will use in the Context. Lets put them there. They are defined using the @role
metadata:
class MoneyTransfer implements dci.Context {
@role var source : {}
@role var destination : {}
@role var amount : {}
}
Using this syntax, we have now defined three Roles. Having the type {}
means that these Roles can be played by any object, but we want to be more specific.
In DCI, the type of a Role is called its RoleObjectContract, or just contract.
The common thinking is to use the already defined classes. The source and destination Roles could be an Account
.
We're not interested in the whole Account
however, that is the old, class-oriented thinking. We want to focus on what happens in the Context right now for a specific Role, so all we need for an object to play the source Role is a way of decreasing the balance. The Account
class has a decreaseBalance
method, which can be useful:
@role var source : {
function decreaseBalance(a : Int) : Void;
}
We're using standard Haxe class notation to define the contract. Let's do the same for the destination Role, but it needs to increase the balance instead:
@role var destination : {
function increaseBalance(a : Int) : Void;
}
The amount role is special. We're using an Int
in this example, which means it can't play a Role. No basic types (Int
, Bool
, Float
, String
) can play Roles, so we can make it an ordinary var, or pass it as a parameter. Let's use a var in this case.
var amount : Int;
(In a more realistic example, amount would probably have some Currency
class behind it, enabling it to play a Role.)
Our MoneyTransfer
Context now looks like this:
class MoneyTransfer implements dci.Context {
@role var source : {
function decreaseBalance(a : Int) : Void;
}
@role var destination : {
function increaseBalance(a : Int) : Void;
}
var amount : Int;
}
So what are the advantages of this structural typing? Why not just put the class there and be done with it?
The most obvious advantage is that we're making the Role more generic. Any object fulfilling the type of the RoleObjectContract can now be a money source, not just Account
.
Another interesting advantage is that when specifying a more compressed contract, we only observe what the Roles can do in the current Context. This is called "Full OO", a powerful concept that you can read more about here, but basically, by doing that we don't need to understand Account
, or essentially anything outside the current Context.
This also affects locality, the ability to understand code by looking at only a small portion of it. So plan your public class API, consider what it does, how it's named and why. Then refine your contracts. DCI is as much about clear and readable code as matching a mental model and separating data from function.
Now we have the Roles and their contracts for accessing the underlying objects. That's a good start, so lets add the core of a DCI Context: functionality. It is implemented through RoleMethods.
Getting back to the mental model again, we know that we want to "Withdraw amount from a source account and deposit the amount in a destination account". So lets model that in a RoleMethod for the source
Role:
@role var source : {
function decreaseBalance(a : Int) : Void;
public function withdraw() {
decreaseBalance(amount);
destination.deposit();
}
}
The withdraw RoleMethod, created as a function with a body, as opposed to contracts which has no body, is a very close mapping of the mental model to code, which is the goal of DCI.
Note how we're using the contract method only for the actual data operation, the rest is functionality, collaboration between Roles through RoleMethods. This collaboration requires a RoleMethod on destination called deposit
, according to the mental model. Let's define it:
@role var destination : {
function increaseBalance(a : Int) : Void;
public function deposit() {
increaseBalance(amount);
}
}
RoleMethods must be declared public
to allow access outside the Role. Contract fields should only be accessed from the Role's own RoleMethods however. This enables the ability to trace the flow of cooperation between Roles, instead of any Role being able to call another Role's underlying object at all times. It's a helpful separation between the local reasoning of how Roles interact locally with their object, and how Roles interact with each other. A goal with DCI is readability, and this helps reading and understanding the use-case-level logic of a Context.
There could be cases when a calling a contract field from another Role is wanted, so contract fields can also be declared public
, but accessing them will emit a compiler warning, and its presence should be viewed as a compromise measure that explicitly erodes the readability of the code. It is a way for the programmer to say: “Trust me” in spite of the fact that readers of the code can’t verify what goes on behind the curtain.
The exception is if you're using Haxe 4 and the contract field is final
. Then the field is immutable and can be accessed without warning.
A RoleMethod is a method with access only to its RolePlayer (through the Role-object contract) and the current Context. You can access the current RolePlayer through the self
identifier. this
is not allowed in RoleMethods, as it can create confusion what it really references, the RolePlayer or the Context. Use self
and the other Role names when referencing them directly.
Let's add a constructor to the class (showing off the self
identifier as well, and public RoleMethods):
class MoneyTransfer implements dci.Context {
public function new(source, destination, amount) {
this.source = source;
this.destination = destination;
this.amount = amount;
}
@role var source : {
function decreaseBalance(a : Int) : Void;
public function withdraw() {
self.decreaseBalance(amount);
destination.deposit();
}
}
@role var destination : {
function increaseBalance(a : Int) : Void;
function deposit() {
self.increaseBalance(amount) : Void;
}
}
var amount : Int;
}
There's nothing special about the constructor, just assign the Roles as normal instance variables. This is called Role-binding, and there are two important things to remember about it:
- All Roles must be bound in the same function.
- A Role should not be left unbound (it can be bound to
null
however).
Rebinding individual Roles during executing complicates things, and is hardly supported by any mental model. So put the binding in one place only, you can factorize it out of the constructor to a separate method if you want. The Roles can be rebound before another Interaction in the same Context occurs, which can be useful during recursion for example, but it must always happen in the same function.
We just mentioned interactions, which is the last part of the DCI acronym. An Interaction is a flow of messages through the Roles in a Context, like the one we have defined now, based on the mental model. To start an Interaction we need an entrypoint for the Context, a public method in other words. This is called a System Operation, and all it should do is to call a RoleMethod, so the Roles start interacting with each other.
If you're basing the Context on a use case, there is usually only one System Operation in a Context. Let's call it transfer
. Try not to use a generic name like "execute", instead give your API meaning by letting every method name carry meaningful information.
MoneyTransfer.hx
class MoneyTransfer implements dci.Context {
public function new(source, destination, amount) {
this.source = source;
this.destination = destination;
this.amount = amount;
}
// System Operation
public function transfer() {
source.withdraw();
}
@role var source : {
function decreaseBalance(a : Int) : Void;
public function withdraw() {
decreaseBalance(amount);
destination.deposit();
}
}
@role var destination : {
function increaseBalance(a : Int) : Void;
public function deposit() {
increaseBalance(amount);
}
}
var amount : Int;
}
With this System Operation as our entrypoint, the MoneyTransfer
Context is ready for use! Let's create two accounts and the Context, and finally make the transfer.
Account.hx
class Account {
public var name(default, null) : String;
public var balance(default, null) : Int;
public function new(name, balance) {
this.name = name;
this.balance = balance;
}
public function increaseBalance(amount: Int) {
balance += amount;
}
public function decreaseBalance(amount: Int) {
balance -= amount;
}
}
Main.hx
class Main {
static function main() {
var savings = new Account("Savings", 1000);
var home = new Account("Home", 0);
trace("Before transfer:");
trace(savings.name + ": $" + savings.balance); // 1000
trace(home.name + ": $" + home.balance); // 0
// Creating and executing the Context:
new MoneyTransfer(savings, home, 500).transfer();
trace("After transfer:");
trace(savings.name + ": $" + savings.balance); // 500
trace(home.name + ": $" + home.balance); // 500
}
}
With the above three files, you can now build and test the example with haxe -lib haxedci -x Main
.
DCI pays great respect to Alan Kay and Smalltalk, which has a feature popularized in the fluent interface. See message passing and east-oriented code for more information.
If you're designing your objects to return "this", enabling a fluent interface, there is a special feature when such an object is playing a Role in a Context:
@role var destination : {
function increaseBalance(a : Int) : dci.Self;
}
Ok, we have learned new concepts and a different way of structuring our program. But why should we do all this?
The advantage we get from using Roles and RoleMethods in a Context, is that we know exactly where our functionality is. It's not spread out in multiple classes anymore. When we talk about a "money transfer", we know exactly where in the code it is handled now. Another good thing is that we keep the code simple. No facades, design patterns or other abstractions, just the methods we need.
The Roles and their RoleMethods gives us a view of the Interaction between objects instead of their inner structure. This enables us to reason about system functionality, not just class functionality. In other words, DCI embodies true object-orientation where runtime Interactions between a network of objects in a particular Context is understood and coded as first class citizens.
We are using the terminology and mental model of the user. We can reason with non-programmers using their terminology, see the responsibility of each Role in the RoleMethods, and follow the mental model as specified within the Context.
DCI is a new paradigm, which forces the mind in different directions than the common OO-thinking. What we call object-orientation today is really class-orientation, since functionality is spread throughout classes, instead of contained in Roles which interact at runtime. When you use DCI to separate Data (RoleObjectContracts) from Function (RoleMethods), you get a beautiful system architecture as a result. No polymorphism, no intergalactic GOTOs (aka virtual methods), everything is kept where it should be, in Context!
Functionality can change frequently, as requirements changes. The Data however will probably remain stable much longer. An Account
will stay the same, no matter how fancy web functionality is available. So take care when designing your Data classes. A well-defined Data structure can support a lot of functionality, by playing Roles in Contexts.
When designing functionality using RoleMethods in a Context, be careful not to end up with one big method doing all the work. That is an imperative approach which limits the power of DCI, since we're aiming for communication between Roles, not a procedural algorithm that tells the Roles what to do. Make the methods small, and let the mental model of the Context become the guideline. A Use case is a formalization of a mental model that is supposed to map to a Context in DCI.
A difference between [the imperative] kind of procedure orientation and object orientation is that in the former, we ask: "What happens?" In the latter, we ask: "Who does what?" Even in a simple example, a reader looses the "who" and thereby important locality context that is essential for building a mental model of the algorithm. (From the DCI FAQ)
Of course the answer is No, DCI isn't suitable for every problem. DCI is an approach to design that builds on a psychological model of the left-brain/right-brain dichotomy. It is just one model, though a very useful one when working close to users and their needs, especially where the discussions end up in a formalized use case.
Some cases don’t lend themselves very well to use cases but are better modeled by state machines, formal logic and rules, or database tables and transaction semantics. Or just simple, atomic MVC. Chances are though, if you're working with users, domain experts, stakeholders, etc, you'll notice them thinking in Roles, and if you let them do that instead of forcing a class-oriented mental model upon them, they will be happier, and DCI will be a great help!
- The haxedci-example repository has a larger example and demo that really showcases the power of DCI and MVC together. Check it out!
- SnakeDCI is a recreation of the classic Snake game, to show you how DCI works in combination with a game framework.
Because of the syntax, there are some problems with autocompletion for Roles. When inside a Role, RoleMethods below the current one may not show up.
'A Glimpse of Trygve: From Class-oriented Programming to Real OO' - Jim Coplien [ ACCU 2016 ]
DCI – How to get ahead in system architecture
Website - fulloo.info
FAQ - DCI FAQ
Support - stackoverflow, tagging the question with dci
Discussions - Object-composition
Wikipedia - DCI entry