Concerning Rails: Simple Single-Purpose Services
The Single Responsibility Principle is a concept in the SOLID principles which states that all classes, modules, and methods should do one thing and one thing only.
In Practice
Respecting The Single Responsibility Principle is to make code easier to read and maintain. A class that started doing one thing can often become many things after a few bug fixes and feature changes making it harder to continue supporting and fixing said class. Sometimes I make a change that can start with a less-than-optimal approach and I need to go through a few steps to realize something can be simpler.
When I started programming, I thought complexity was the goal and that the more complex something was, the better it worked and the more efficient it was. One thing I can say is that past Jake was extremely wrong. At Popmenu, we strive for clean, readable, and maintainable code — and here’s a recent example where I took a complex service and made it simpler.
The Problem
This isn’t exactly my recent experience. Names and places have been changed to protect the privacy and identity of services and intellectual property.
Let’s start with an example pseudo service called TaxBuilder which returns a single positive integer.
TaxBuilder should do one thing, build taxes. However, we all know that taxes are complicated as hell. So, let’s try to keep this stupid simple.
# original code
taxes_due = TaxBuilder.call # returns a positive integer
My first thought was I could return an array of these numbers.
# things you would like to get out of the TaxBuilder
taxes_paid_now # always is taxes_due
deferred_taxes # if remaining_taxes is positive
recouped_taxes # if remaining_taxes is negative
I know what you’re thinking, that’s only two numbers.
I wanted remaining_taxes
to be either positive or negative.
A positive amount would be deferred_taxes.
A negative amount would be recouped_taxes.
We only use these recouped_taxes
when there are taxes to bill.
The caller of TaxBuilder
would then call .sum
on the result:
taxes_paid_now = TaxBuilder.call.sum
Other uses of this service would be to record when we defer or recoup these taxes.
Something like:
# Original approach: returns an array with the sum of taxes
taxes_paid_now, remaining_taxes = TaxBuilder.call
if remaining_taxes > 0
# positive amount is amount we could not bill
deferred_taxes = remaining_taxes
elsif remaining_taxes < 0
# negative amount is amount available to bill
recouped_taxes = remaining_taxes
end
This felt right because we only bill one number, taxes_paid_now
. Since it’s one number and one service that returns one array, we’re still separating concerns, right? Right?
Not exactly.
We have three numbers that go into the sum of one number.
While this worked, it can be hard for developers reading this later to understand why we used .sum
on TaxBuilder.call
. They had to dig into the code to figure out what remaining_taxes
represented, making it confusing.
So, let’s make it simpler.
The Solution
Let’s separate this into three services for each number that goes into the sum of taxes_paid_now
.
TaxBuilderBase
TaxAmountPaidService < TaxBuilderBase
TaxAmountDeferredService < TaxBuilderBase
TaxAmountRecoupedService < TaxBuilderBase
Now all three services will use the same math through OOP and return only a positive integer.
taxes_paid_now = TaxAmountPaidService.call
deferred_taxes = TaxAmountDeferredService.call
recouped_taxes = TaxAmountRecoupedService.call
The above code does not need comments because it’s self-explanatory.
I came up with the original solution and thought ‘Wow this is fun’ because I thought it was clever. Clever is not necessarily simple but in this case, it was fun.
Working on complex or hard-to-read code when something is broken for the biggest client and the code reads like Don Quixote is challenging. I like fixing things fast and moving on to write more fun code. Hopefully, you feel the same way and this helps remind you to keep it simple. Happy coding.