A Specification Pattern implementation20 March 2015
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.
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.
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.
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
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.
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.