by Carles

A Specification Pattern implementation


At times programming can be boring. That happens when a feature is no more than writing a controller action, a view, and some other framework-related tasks. But sometimes we find opportunities to develop our creativity, to exercise our craft. We had one of these opportunities recently, a business requirement that forced us to find a solution a little beyond and deeper thinking.

Almost everything in PeerTransfer is about moving money. Thus, the money transfers and the related processes reside in the very center of our system. Daily, one of the problems that our operators have to deal with is what to do when the money we receive from our customers is more or less than we expected. That’s what we call Overpayments and Underpayments.

Depending on the difference between the expected and received amounts, the actions we take vary, so we classify the situation in four different scenarios: Small Overpayments, Large Overpayments, Small Underpayments and Large Underpayments.

A first approach to the solution

So let’s say there is a process we call ReceiveTransfer. It receives a transfer object and it does whatever needs to be done to process that transfer. A first approach could be to add the new overpayments and underpayments logic there in a procedural way.

class ReceiveTransfer
  def do(transfer)
  	if is_small_overpayment(transfer) do
  	  do_whatever_with_small_overpayment(transfer)
 	elsif is_large_overpayment(transfer) do
 	  do_whatever_with_large_overpayment(transfer)
 	elsif is_small_underpayment(transfer) do
 	  do_whatever_with_small_underpayment(transfer)
 	elsif is_large_underpayment(transfer) do
 	  do_whatever_with_large_underpayment(transfer)
 	else
 	  do_whatever_with_correct_amount(transfer)
  	end

  	# What ReceiveTransfer already does
  end

  # Rest of the class
end

In the code above we’ve applied Extract Method to make the code more readable, so the class continues as follows.

class ReceiveTransfer

  def do(transfer)
    # implementation
  end

  private

  def is_small_overpayment(transfer)
    # implementation
  end

  def do_whatever_with_small_overpayment(transfer)
    # implementation
  end

  def is_large_overpayment(transfer)
    # implementation
  end

  def do_whatever_with_large_overpayment(transfer)
    # implementation
  end

  def is_small_underpayment(transfer)
    # implementation
  end

  def do_whatever_with_small_underpayment(transfer)
    # implementation
  end

  def is_large_underpayment(transfer)
    # implementation
  end

  def do_whatever_with_large_underpayment(transfer)
    # implementation
  end

  def do_whatever_with_correct_amount(transfer)
    # implementation
  end
end

The solution, althought working, isn’t perfect at all. If you imagine that piece of code without hiding the implementation of the private methods, you will undoubtedly think that there is too much work being done in that class.

Let’s rethink it in a more object-oriented way.

Inheritance based

Inheritance based
Inheritance based

The diagram above represents what was actually our first approach to the solution. A Large/Small class stands for each one of the situations, resulting in four children. Each one of them implements a detected? method that receives a transfer and returns true if the conditions for a specific situations apply.

class SmallUnderpayment < Underpayment
  include Threshold

  def detected?(transfer)
    super && amount_below_threshold(transfer)
  end
end

As you can see in the example, the classes also include some behaviour from a Threshold mixin. This behaviour carries on with the is small versus is large dichotomy. So the hierarchy handles two different taxonomies; the size and the sign of the deviation.

The client uses commands to match the PaymentConditions and execute the proper task.

class ReceiveTransfer
  def do(transfer)
    condition_that_matches(transfer).execute
  end

  def condition_that_matches(transfer)
    conditions = [
      SmallOverpayment.new,
      LargeOverpayment.new,
      SmallUnderpayment.new,
      LargeUnderpayment.new
    ]
    conditions.find do |condition|
      condition.detected?(transfer)
    end
  end

  # Rest of the class
end

That inheritance-based classification worked okay until we needed to add a bunch of new vectors to our decision engine in order to make more complex decisions. We had no other way to add these modifications than adding the logic to the four classes, or keep digging in the hierarchy by creating new subclasses. That would easily get off of our hands. As we needed more rules to be implemented we felt that we needed a more sustainable solution.

Composition based

If you haven’t read the Specifications paper (Evans, Fowler) this is a good time for doing it. Go, but come back later!

We replaced the inheritance-based implementation with a set of small classes implementing the PaymentSpecification interface.

Composition based
Composition based

A complex specification can be built by gathering the rules needed and putting them together with logical operators. These operators are, too, composite implementations of the PaymentSpecification interface. See Composite for more info.

Composition based detail
Composition based detail

Last notes about boundaries

If you compare the inheritance and composition based diagrams, you may notice that there is a slight change in the interface implementations. Besides the renaming from a mysterious detected? to a more meaningful satisfied_by?, the objects they expect are different. Are they? What’s a transfer and what’s a payment for our system. Well, it depends on the context. See Fowler’s article on Bounded Contexts for further details and references.

The problem was that we didn’t want to have our big Transfer ActiveRecord model walking through the entire application. Every feature might need to add more and more logic to that model, and it has became fat enough to start worrying about heart attacks. To mitigate the pain we are slowly introducing Bounded Contexts in our system.

Bounded context
Bounded context

When an instance of Transfer is received by the over/underpayments context, a Payment is created from it and it is discarded. The Payment is a Data Transfer Object whose purpose is to break the dependency between the subsystem and the framework.

Carles Climent Granell.
Developer at peerTransfer.