Scala has several nice language features, including the elegant use of val
for immutable variables and var
for mutable, but the feature that I miss the most on a day-to-day basis is "traits."
Traits allow you to implement one or more methods of an interface. The canonical use is to "mix-in" behavior while avoiding the "diamond-problem."
DCI has the idea that Object
s (domain-meaningful entities that correspond to user conceptions) adopt Role
s, which are context-specific. Roles interact to produce value. So, for instance, when you're transferring money at an ATM, you're dealing with two accounts that are the same type of Object (Account
), but which are in two different roles in the context of "Transfer Money": a TransferSource
and a TransferSink
. And an Account
in a TransferSource
role has different behavior than an Account
in a TransferSink
role (e.g., TransferSource
expects to withdraw(Money amount)
while TransferSink
expects to credit(Money amount)
).
In C#, the way to specify that a class has a certain set of behaviors is to specify those behaviors in an interface
and specify that the class implements them:
public class Account: TransferSource, TransferSink
And then, of course, you would implement the various methods of TransferSource
and TransferSink
within Account
.
But the very essence of DCI is the premise that classic OOP type-systems don't appropriately capture the relationships between Objects-in-Roles, even though "Objects-in-Roles working with each other" is the domain-users mental model ("I pick a source account, and a destination account, and specify an amount, and the amount is debited from the source and credited to the destination"). So DCI says that the TransferTo
method that corresponds to the use-case should be elevated to a first-class object.
But in C# you cannot partially implement an interface
. But you can create and implement an extension method on an interface!
public static class TransferContextTrait
{
public static void TransferTo(this TransferSource self, TransferSink sink, Decimal amount)
{
try
{
if(self.Funds < amount)
{
self.FailTransfer(new TransferFailedReason("Insufficient Funds"));
}
else
{
self.Withdraw(amount);
sink.Deposit(amount);
var details = new TransferDetails(self.Name, sink.Name, amount);
self.AccomplishTransfer(details);
}
}
catch(Exception x)
{
self.FailTransfer(new TransferFailedReason(x.ToString()));
}
}
}
Note an interesting restriction, though: You cannot trigger an event
from within an extension method! So in this case, although I would have preferred to propagate the results of the calculation by self.TransferAccomplished(this, details)
I have to use a proxy function in Account
:
public void AccomplishTransfer(TransferDetails details)
{
TransferAccomplished(this, new TArgs<TransferDetails>(details));
}
public event EventHandler<TArgs <TransferDetails>> TransferAccomplished = delegate {};
I'll be talking more about DCI and other cross-platform architectural techniques at MonkeySpace in Chicago next week. Hope to see you there!