DCI Example with NRoles
The DCI (Data, Context, Interactions) architecture is a style that proposes an interesting separation of concerns between the domain model of a system (what the system is) and its behavior (what the system does), normally associated with its use cases. The rationale is that, since these parts incur change at different rates, isolating them from each other results in a system that's easier to understand, evolve and maintain.
In order to enact a specific use case, a specialized context instantiates the necessary data objects assigned to the needed roles to run the required "algorithm".
A very common example is that of a bank transfer operation. This example has already been described in C# with the C# quasi-mixins pattern. With NRoles the code for a role lives in a single abstraction, which results in a more compelling implementation. I'll change the example a little bit, to introduce a stateful role; something that's harder to achieve with pure C#.
The stateful role represents a log of events, from a generic type T
:
public class RJournal<T> : Role {
private List<T> _entries = new List<T>();
public IEnumerable<T> Entries { get { return _entries; } }
public void Log(T entry) {
_entries.Add(entry);
}
}
The next roles are the debitor and the creditor in a transfer operation, a source and a target:
public abstract class RTransferSource : Does<RJournal<string>>, Role {
public abstract void Withdraw(int amount);
public void TransferTo(RTransferTarget target, int amount) {
Withdraw(amount);
target.Deposit(amount);
LogWithdraw(amount);
target.LogDeposit(amount);
}
internal void LogWithdraw(int amount) {
this.As<RJournal<string>>().Log("Withdrew: " + amount);
}
}
public abstract class RTransferTarget : Does<RJournal<string>>, Role {
public abstract void Deposit(int amount);
public void TransferFrom(RTransferSource source, int amount) {
source.TransferTo(this, amount);
}
internal void LogDeposit(int amount) {
this.As<RJournal<string>>().Log("Deposited: " + amount);
}
}
They define abstract methods that have to be provided by composing classes. They also compose the Journal<string>
role. The Account
class composes all preceding roles, which means that it can be a debitor or a creditor in an operation. It provides the required abstract methods:
public class Account : Does<RJournal<string>>
Does<RTransferSource>, Does<RTransferTarget>
{
public string Id { get; private set; }
public int Balance { get; private set; }
public Account(string id, int balance) {
Id = id;
Balance = balance;
}
public void Withdraw(int amount) {
Balance -= amount;
}
public void Deposit(int amount) {
Balance += amount;
}
}
This diagram describes the above constructs:
Since roles are about composition, and not inheritance, all roles included in a class are flattened. In particular, RJournal<T>
ends up only once in Account
.
The TransferContext
class executes the use case to transfer money from a source to a target:
public class TransferContext {
public RTransferSource Source { get; private set; }
public RTransferTarget Target { get; private set; }
public int Amount { get; private set; }
public TransferContext(RTransferSource source, RTransferTarget target, int amount) {
Source = source;
Target = target;
Amount = amount;
}
public void Execute() {
Source.TransferTo(Target, Amount);
}
}
The following sample code invokes the scenario:
var savingsStartBalance = 10000;
var checkingStartBalance = 500;
var savings = new Account("Savings", savingsStartBalance);
var checking = new Account("Checking", checkingStartBalance);
var context = new TransferContext(
checking.As<RTransferSource>(),
savings.As<RTransferTarget>(),
100);
context.Execute();
PrintAccount(savingsStartBalance, savings);
Console.WriteLine();
PrintAccount(checkingStartBalance, checking);
Where PrintAccount
is simply:
private static void PrintAccount(int startBalance, Account account) {
Console.WriteLine("{0}:", account.Id);
Console.WriteLine("Start: " + startBalance);
account.As<RJournal<string>>().Entries.ToList().ForEach(Console.WriteLine);
Console.WriteLine("Balance: " + account.Balance);
}
Running it yields this output:
Savings: Start: 9000 Deposited: 100 Balance: 9100 Checking: Start: 500 Withdrew: 100 Balance: 400
To know more and download NRoles, take a look here.
Nicely written blog post. However when it comes to DCI there are a few misconceptions about what DCI is. DCI is concerned with objects not classes and the mixing of what the system does and what it is happens between objects (context on one hand and RolePlayers on the other). Your account derive from a "role" however a role is not a type. It's an identifier internal to a context. Any object playing said role will for the duration of the execution of said context have a number of role methods available. The compile time object of the object reflects what the system IS and not what the system does (as is the case in your example). DCI is also very much concern with keeping the algorithm as a part of the context so that you can read the entire algorithm in one place. You have the transferto algorith split between different classes (RTransferTarget and RTransferSource)
ReplyDeleteYes, it would be better if the roles could be attached to the objects themselves, like it's possible in Scala. To do it in C#, we could use dynamic proxies and create the compositions dynamically. There're frameworks for that, like Castle Dynamic Proxy and Linfu. It's just that the nature of NRoles is that it's a compile-time (well, post-compile time) roles weaving mechanism, and you have to attach the roles to the classes themselves, like with the C++ traits-pattern also described in some DCI literature.
DeleteDo you have any plan to support that features? I mean to attach a role to a data.
DeleteI have thought about the design of the feature (baked into the compiler): look here. An excerpt, also applicable to the original comment by @rfs:
Delete"This gives an interesting and powerful way to organize the code, where classes only play certain roles depending on the current execution context. The alternative would be to pollute a class static interface with many roles that wouldn't be applicable to many of its usages."
.. which is exactly what happens with NRoles today :-(
Anyway, I'd like to work on this, but I haven't had any time for such a big feature lately.....
It's very interesting if NRoles can be used something like this
DeleteAccount account1 = new Account{ AccountNo=1, Balance = 100m};
account1.With(); //Compiler checking if not match the role contract, not runtime error
account1.With();
Account account2 = new Account {Account=2, Balance = 50m}
account2.With();
account1.TransferTo(account2,50m);
instead of
public class Account :Does, Does
{
}
Hi Jordao
DeleteHere is my example doing DCI+CQRS (EventSourcing) with NRoles
http://pastebin.com/BFR6ZxVR
Very nice example @ryzam, thanks for sharing! Event sourcing like that is a really nice fit for an accounting system.
DeleteI'm having a problem if i want to debug inside the role method, do u have any idea?
DeleteHi ryzam, are you using the 1.8 version that's available in the download page? If so, can you please try compiling the latest code from source?
DeleteI'm still having a problem to debug inside the rolemethod (TransferTo) , even with the new source code.
Delete//Role
public abstract class SourceAccountRole : Does, Does, Role
{
public abstract Guid Id { get; set; }
public abstract decimal Balance { get; set; }
//RoleMethod
public virtual void TransferTo(ISession s, SinkAccountRole sinkAccount, decimal amount)
{
WithDraw(amount);
var e = this.As().GetEvents();
if (e.Raise())
{
//2
sinkAccount.Deposit(amount);
}
}
}
any suggestion
Excellent library
ReplyDelete