Object-free DCI

Some of you may already know me, as I’ve been around the Rails community for some time, including being on the RubyNZ Committee for two terms and having organised the 2015 RailsCamp NZ. You might say that I know a few things about Ruby, and maybe even about object oriented design.

Over the last couple of years the Ruby community has been learning from its Smalltalk roots about DCI with the help of great books like Clean Ruby by Jim Gay. Whether you buy into all the principles of DCI or prefer “DCI lite” (or Use Cases as Shevaun Coker calls them) there’s been plenty of effort put into trying to teach Rails developers how to avoid “fat models”.

This is not a Rails blog post

So by now you’re thinking “oh great, another blog about how to do DCI in Ruby and make my Rails code base so clean I can eat my dinner off it.” Ha! Tricked you! This is a blog post about Elixir. You read that right. I’m talking about DCI and Functional Programming in the same post.

Quick definition

So DCI stands for “Data, Context, Interaction” but that doesn’t really explain what it is or how to use it. I would rather avoid a complete explanation of DCI as there a lot of really great resources for DCI available online, including those linked in the preamble. Here’s the sort version.

Data

In DCI you always have at least one “data object”. Also called “role players”, these are the objects we work on during the transaction. For example an account object.

Context

The context is an object that handles collecting the role players, decorating them with the behaviour needed to play their roles and then triggering that behaviour. For example a transfer of funds between accounts.

Interaction

Interaction is the behaviour that gets added to the role players (ie, the “role”). For example making the accounts be “transferable”.

Ruby Example

There’s a great example in the README of Mr Darcy – a RubyGem I wrote that handles DCI using asynchronous promises, so I’m going to steal it wholesale:

class BankTransfer < MrDarcy::Context
role :money_source do
def has_available_funds? amount
available_balance >= amount
end

def subtract_funds amount
self.available_balance = available_balance - amount
end
end

role :money_destination do
def receive_funds amount
self.available_balance = available_balance + amount
end
end

action :transfer do |amount|
if money_source.has_available_funds? amount
money_source.subtract_funds amount
money_destination.receive_funds amount
else
raise "insufficient funds"
end
amount
end
end

Here you can see that the object BankTransfer is the context, it has two roles which specify extra behaviour that is given to the two role players as they come into the context and an action. Here’s how you’d use it:

Account = Struct.new(:available_balance)
marty = Account.new(10)
doc_brown = Account.new(15)

context = BankTransfer.new money_source: marty, money_destination: doc_brown
context.transfer(5).then do |amount|
puts "Successfully transferred #{amount} from #{money_source} to #{money_destination}"
end

context.transfer(50).fail do |exception|
puts "Failed to transfer funds: #{exception.message}"
end

Here we create two accounts with individual balances, place them into a context as the role players and then attempt to perform the interaction and either succeed or fail.

Elixir Example

For those of you not familiar with Elixir, it’s a functional programming language which runs on the Erlang VM (known as the BEAM) but with a bunch of great features sprinkled on top. The first thing you’ll notice is its Rubyish syntax, which leads many people to think that Elixir is “the CoffeeScript of Erlang”, but that’s not true. Read Devin Torres’ great blog post Elixir: It’s Not About Syntax for more information.

Data

Functional programming is all about data, and Elixir is no different, with core data types such as integer, float, tuple, list and map we can easily model our data. Elixir also has something special, the concept of a “struct”. Structs can be thought of as “maps with a name”, but there’s some other magic we’ll get into a bit later, including polymorphism.

So using the example from above, our Data would be two account structs:

defmodule Account do
defstruct available_balance: nil
end

marty = %Account{available_balance: 10}
doc_brown = %Account{available_balance: 15}

Context

To implement a context we might make a module something like this:

defmodule BankTransfer do
def transfer amount, %{money_source: source, money_destination: dest} do
if MoneySource.has_available_funds? source, amount do
source = MoneySource.subtract_funds source, amount
dest = MoneyDestination.receive_funds dest, amount
{:ok, %{money_source: source, money_destination: dest}}
else
{:error, "insufficient funds"}
end
end
end

So let’s explain what happened here; we created a module called BankTransfer and defined a function called transfer which takes an amount and then a map and pattern matches it’s money_source and money_destination properties and assigns them to local variables. We may want to add additional guard clauses for this function also (such as when is_number(amount) and amount > 0).

Interaction

The next thing we did was make these accounts play the roles of MoneySource and MoneyDestination. How do we do this? This is where Elixir’s protocols come in; they allow us to implement polymorphism for a function based on the type of its data. First we define the protocols:

defprotocol MoneySource do
def has_available_funds? money_source, amount
def subtract_funds money_source, amount
end

defprotocol MoneyDestination do
def receive_funds money_destination, amount
end

When you define a protocol you define the signatures of the functions, without specifying the implementations. Next we’ll define the implementations of these functions for our Account module:

defimpl MoneySource, for: Account do
def has_available_funds?(%Account{available_balance: bal}, amount) when bal >= amount do
true
end

def has_available_funds?(%Account{}, _amount) do
false
end

def subtract_funds %Account{available_balance: bal}=account, amount do
%{account | available_balance: bal - amount}
end
end

defimpl MoneyDestination, for: Account do
def receive_funds %Account{available_balance: bal}=account, amount do
%{account | available_balance: bal + amount}
end
end

Some cute things in the above code:

  1. We’re able to implement MoneySource.has_available_funds? completely within guard clauses. If the first function matches it will return true, otherwise it will fall through to the next implementation, which matches in all circumstances.
  2. We’ve used Elixir’s short-hand syntax for updating maps to update the account, but we could just as easily have used the Map.put/3 function.

And finally:

So we’ve taken our two accounts (which could easily enough have been Ecto Models) and implemented the behaviour we need, and the context in which we with them to behave this way. To use it we would do something like:

marty = %Account{available_balance: 10}
doc_brown = %Account{available_balance: 15}

case BankTransfer.transfer 5, %{money_source: marty, money_destination: doc_brown} do
{:ok, result} ->
IO.puts "Successfully transferred 5 from #{inspect result.money_source} to #{inspect result.money_destination}"
{:error, msg} ->
IO.outs "Failed to transfer funds: #{msg}"
end

So that’s how you implement DCI in Elixir. The only “objecty” feature needed is polymorphism, which Elixir provides by way of protocols. In many ways I’d suggest this a more “pure” expression of DCI than doing it with objects. Just saying.