In a recent reddit post and more generally on his website, Gregory Brown raised some valid issues with DCI and in particular queried the value of DCI contexts to represent use-cases.

In this article I will do my best to present some reasons as to why Contexts can be useful.

What is a Context in DCI?

When programming in DCI style a context declares an interaction between a set of objects. It does this by assigning objects roles, these objects then interact as their roles.

Usually a Context is a class which is initialised with role-playing objects, then called with the parameters of the interaction.

Not a new idea

The idea of using Contexts to govern interactions between objects is not a new one. The concept that we can organise system behaviour into encapsulated procedures is a very common one in programming.

For instance Transaction Scripts are one way of organising an interaction between different components of a system.

Coding without Contexts

When not using a context to organise object interactions we are relying on our classes and objects to work together to implement our use-cases. This isn't necessarily bad if our classes are simple and easily understandable.

For instance in the following code we may be writing code for an online store. We need an invoice to be generated when a product is bought.

Product = Struct.new(:name, :price, :description)
Invoice = Struct.new(:address,  :details, :total) do
  def print
    p "#{details}, sent to: #{address}, totalling #{total}"
  end
end

class User
  attr_reader :name, :address, :purchases

  def initialize(name, address)
    @name     = name
    @address  = address
    @purchases = []
  end

  def buy_product(product)
    purchases << product
  end
end
class Accounts
  attr_reader :invoices

  def initialize
    @invoices = []
  end

  def generate_invoice(address, product)
    description = "#{product.name} - #{product.description} @ #{product.price} ea." 
    total = product.price
    invoice =  Invoice.new(address, description, total )
    @invoices << invoice
    invoice
  end
end
pants = Product.new("red pants", 10.0, "A Pair of nice red pants") 
user = User.new("Prince Charles", "Buckingham Palace")
accounts  = Accounts.new

user.buy_product(pants)
invoice = accounts.generate_invoice(user.address, pants)
p invoice.to_s


A user, is sent the #buy_product method to store that product in his list of purchased items. The purchased item is then passed to the #generate_invoice method of the accounts department, this then triggers an invoice to be created which gets stored in the accounts departments @invoices array. Finally the invoices is printed to screen with Invoice#print

The program flow is spread throughout the methods on the classes.

Is that bad?

No, not really.

Programmers have been coding like this forever and it mostly works well. However, I suspect it's fairly common for good practice to emerge:

  • Commonly called chains of methods are grouped together.
  • Methods will only be passed parameters that they can handle.
  • Shared methods may be extracted to a mix-in.
  • Internally called methods are hidden as private.

Look at my calling of the above classes, I call User#buy_product, then Accounts#generate_invoice and finally Invoice#print. I dare say, in a full blown application, a similar order of method calls will be repeated in some form and quite possibly listed in its own method or class. Sounds kinda like a context to me.

In-fact when you think about it we are almost always writing code in a context. When writing a method I'm thinking under what circumstances might this method will be called, what parameters might it receive, who might send them. We are always referring to some context, even if its an implicit one. Even if we don't actually identify contexts as such we use them all the time, for instance ActionController methods can be considered a form of a context.

In my opinion, contexts are synonymous with Commands, or Queries, or Tests, or even View Presenters. They are any explicit codified instruction to carry out object interactions in a clear and understandable way.

So why DCI Contexts?

I think DCI contexts can enable many of the best practices that we try to do when writing code:

  • In a DCI Context, by definition, order of execution is defined explicitly.
  • Methods are limited to roles within a context.
  • Shared methods are put into roles.
  • We know what parameters will be passed between objects because we control the entire interaction.

In addition to encouraging good practice, DCI Contexts have some other advantages:

  • As use-case behaviour is encapsulated in a context object, we can easily swap out behaviour in our system.
  • Use-cases become a testable unit. The number of pathways that need to be tested in code is reduced because a use-case can be tested in an isolated way.
  • We can make changes to one use-case without affecting or breaking any of the others in our system.

Some Code Examples

To be honest, I'm not sure how best to write this section. Example code can often be far too simplistic and not show the benefits of using a particular technique, or it becomes too domain specific and the approach is lost in the details. I'll present a few examples of simple use-cases, and then an extract from some existing code.

Lets consider the scenario above, and rewrite it in DCI style. I want to be able to show how we can naturally move from domain objects, to easily implementable use-cases. As with all my DCI stuff I am using my AliasDCI library, it is a proof of concept only, in order to get the principles of DCI working in ruby as much as possible.

An Ad-hoc Example

We start with the domain objects, these are User, Product, Accounts, & Invoice classes without any behaviour.

class User
  attr_reader :name, :address, :purchases

  def initialize(name, address)
    @name     = name
    @address  = address
    @purchases = []
  end
end

Product = Struct.new(:name, :price, :description)

Invoice = Struct.new(:address,  :details, :total)

class Accounts
  attr_reader :invoices

  def initialize
    @invoices = []
  end
end

[User, Product, Invoice, Accounts].each{|k| k.send :include, AliasDCI::DataObject}

These objects are about as simple as you can get. Very easy to understand as they have no behaviour or interacting methods. This is what the "System Is" the contexts will provide what the "System Does".

Now we can begin to write contexts in a straightforward way to implement the following requirements

Story: Purchase process

  • In order to keep track of purchases
  • As an accounts department
  • I want to manage invoices when a product is bought

Scenario 1: Invoices should be stored in the accounts department

General requirement:

  • When a product is purchased by a customer
  • An invoice is created and stored in accounts

As a test:

  1. Given a customer has selected red pants costing £10
  2. When he buys red pants
  3. Then I should have an invoice totalling £10

Writing the context directly as per the use-case:

class PurchasingProcess
  include AliasDCI::Context

  def initialize(user, product, accounts)
    assign_named_roles(:customer            => user, 
                       :selected_product    => product,
                       :accounts_department => accounts)
  end

  def call
    in_context do
      customer.buy_product
      accounts_department.generate_invoice
    end
  end

  role :customer do
    def buy_product
      purchases << selected_product
    end
  end

  role :selected_product do  
    def invoice_desc
      "#{name} - #{description} @ #{price} ea."
    end
  end

  role :accounts_department do
    def generate_invoice
      invoice =  Invoice.new(customer.address, selected_product.invoice_desc, total )
      invoices << invoice
    end
  end

end

The above context is independent of my domain objects, it is self contained and it cannot affect any other use-case or code in my application. The PurchasingProcess instance simply becomes a 'blackbox' that mutates our domain objects. This can be tested independently from the rest of the system. There are no side-effects as our domain objects do not have behaviour.

Because we are strictly implementing a specific interaction it doesn't hugely matter that these methods are coupled. This is because this logic is executing only within this context. All the code is in front of us, and we don't need to worry too much about making methods generic so they can be used in different circumstances.

There isn't any risk of other object's calling the accounts department's #generate_invoice method or the customer's #buy_product method because they don't exist anywhere else. The methods can be written purely to deliver this use-case.

In theory I could delete the above code, and nothing else would break in my application.

The other advantage of DCI contexts is that they can be versioned and/or forked. Maybe during the holiday sales season there needs to be a different purchasing process. With a DCI context it is as simple as instantiating HolidaySalesPurchasProcess#new instead of the regular PurchaseProcess. This lets us modularise our business processes.

Scenario 2: Product is on Sale

  • When an on-sale product is bought
  • The invoice amount is reduced by 10%.

As a test:

  1. Given a customer has selected a samsung phone costing £300
  2. And samsung products are on sale
  3. When he buys the phone
  4. Then I should have an invoice for £270

To deliver this we add a #on_sale? method to the selected_product role, to return true if that item is on sale. We also modify the invoice generation method in the accounts_department role.


# in selected_product role:
def on_sale?
  ['samsung', 'nokia'].any? {|s| name.include?(s)}
end

# in accounts_department role:
def generate_invoice
  total = selected_product.price
  total *= 0.9 if selected_product.on_sale?
  ...
end

Every new use-case or requirements generally adds or modifies methods in our system. If we were not using DCI Contexts then these methods would usually be added to class files. The class files grow, and methods get shared between use-cases. If not careful unintended coupling can then occur because modifying the code that implements one use case can affect another.

Uncessary methods can also get added to classes, for instance why does the User class really need an #buy_product method? That method only really makes sense in a purchasing context.

Story: Customers are charged for engineer visits. (Yes, a very mean business!)

General requirement:

  • When an engineer is sent to a customer to fix a broken product
  • An invoice is created and saved in the accounts department

As a test:

  1. Given a customer has already purchased a DVD Player
  2. And I currently have an accounts department
  3. When a £100 a day engineer is sent to fix his DVD Player
  4. Then I should email the user to confirm attendance
  5. And I should have an invoice totalling £100

Here we write a context in a straightforward way, we do not need to worry with clashing with previous code we've written, as its in a separate context.

class EngineerCallOut
  include AliasDCI::Context

  def initialize(customer, product, accounts)
    assign_named_roles(:customer => customer, 
                       :purchased_item => product, 
                       :accounts_department => accounts)
  end

  def call( callout_rate )
    in_context do
      customer.confirm_attendance
      accounts_department.generate_invoice(callout_rate)
    end  
  end


  role :customer do 
    def confirm_attendance
      p "Emailing user:#{name} - An engineer has been booked to fix your #{purchased_item.name}"
    end
  end

  role :purchased_item

  role :accounts_department do
    def generate_invoice(callout_rate)
      description =  "Call out charge for engineer to fix #{purchased_item.name}" 
      invoice = Invoice.new(customer.address, description, callout_rate)
      invoices << invoice
    end  
  end 

end

As you see in this instance, our accounts department has a different generate_invoice method. This is perfectly fine, we do not need to worry about existing behaviour on the Accounts class because there is none.

Summary

In the above examples we added two processes to our system, a purchasing process and an engineer callout process. The processes are completely independent from each other, and behaviour in one does not affect the other, even when similar entities like invoices are being generated.

Furthermore these processes are largely decoupled from our domain objects, only relying on exposure of a few key properties. Instead of sending an engineer to a user, we can actually send an engineer to any object that has a name and an address:

starbucks = Company.new(:name => "Starbucks", :address=>"1 Times Square", :company_id => "666", :business_category=> "Coffee and Food")
EngineerCallOut.new(starbucks, dvd_player, accounts).call(200)

A Context In Use

Below is an example of a context I am currently working on for a maintenance firm. It gets Jobs and Faults and needs to register and monitor them, eventually attending jobs and closing out faults.

With this command handler, I have segregated all of this logic from the rest of my system. I can isolate and test it, and I know as long as I call it with job_attributes and fault_attributes My entities will be created as expected. It is also independent from the web-stack, so this plus the domain objects can be used in rails, or Sinatra, or just plain ruby files.

I will leave this article at that, I hope I've presented some ideas as to why DCI contexts may be useful when developing applications with multiple use-cases.

class CreateJobWithFaultCommandHandler
  include AliasDCI::Context

  attr_reader :job_attributes
  attr_reader :fault_attributes

  #
  #  The interface to the command, accepts job_attribute and fault_attributes hash
  #
  def self.execute(job_attributes, fault_attributes)
    command_handler   = self.new(job_attributes, fault_attributes)
    command_handler.call
  end

  #
  #  Initializes a blank new job, new fault as roles.
  #
  def initialize(job_attributes, fault_attributes)
    @job_attributes   = job_attributes
    @fault_attributes = fault_attributes
    assign_named_roles(:new_job => Job.new , :new_fault => Job::Fault.new ,:command => self)
  end

  #
  #  The logic of the command, checks for pre existing fault, assign job attributes
  #  and assign's the fault to the new job, then saves the job and the fault.
  def call
    in_context do
      unless pre_existing_fault?
        new_job.assign_attributes
        new_fault.assign_attributes
        new_job.assign_fault
        new_job.persist
      else 
        fail OpManError.new("Cannot Create Job as Fault Number already exists.")
      end
    end
  end

  # The new_job role, responsible for assigning a fault, and persisting
  # the job & fault
  role :new_job do
    def assign_attributes
      self.status        = :open
      self.location      = command.job_attributes[:location]
      self.description   = command.fault_attributes[:description]
      self.site          = command.job_attributes[:site]
      self.contract      = command.job_attributes[:contract]
      self.client        = command.job_attributes[:client]
    end                             

    def assign_fault
      self.fault = new_fault
      new_fault.job = self
    end

    def persist
      Job.transaction do
        self.save
        new_fault.save
      end
    end
  end

  #
  # The new fault role stores a description and a fault number
  #
  role :new_fault do
     def assign_attributes
       self.status        = :open
       self.description = command.fault_attributes[:description]
       self.fault_no    = command.fault_attributes[:fault_no]
     end
  end

  #
  # The command context, checks for pre_existing fault
  #
  role :command do
    def pre_existing_fault?
      fault_no = fault_attributes[:fault_no]
      Job::Fault.first(:fault_no => fault_no) ? true : false
    end

  end
end


blog comments powered by Disqus

Published

2012-12-21

Categories


Tags