Back To Basics. SOLID Part One, The Single Responsibility Pattern!
I’ve struggled to remember the SOLID principles after reading about them. I’m currently about halfway through reading Clean Architecture and the author regularly refers back to SOLID using their acronyms and it’s making it more difficult to read so I need to learn them. The best way to learn is to write. So, here we go.
What is SOLID?
- SRP - Single Responsibility Principle
- OCP - Open Closed Principle
- LSP - Liskov Substitution Principle
- ISP - Interface Separation Principle
- DIP - Dependency Inversion Principle
I’ll go over why I think these principles are solid advice by going through them one by one in a number of posts.
First of all I would like to say that SOLID is not a collection of rules that are never to be broken. They’re principles which can be used to guide your architecture and design. They are certainly not a tool which you use to bash your team mates with at code review. No one will like you if you leave comments on a PR saying things like “this doesn’t obey Liskov”. Understand them and use your understanding to help inform your opinions.
Single Responsibility Principle
This means that a module should be responsible to only one actor. Here an actor means a stakeholder or user but could mean something else. By module, we mean a cohesive set of functions and data structures. This could be a single source file. The problem this principle is trying to solve and the solution is well explained through an example. I will be writing my examples in Go pseudocode, but it should be easily understandable by anyone who has used any OO language before.
Say we have a class called Employee with a signature like so:
type Employee struct {}
func (e Employee) CalculatePay() int { ... }
func (e Employee) ReportHours() int { ... }
func (e Employee) Save() error { ... }
Now imagine that the CalculatePay()
method is used by the CFO’s team and the ReportHours()
function is used by the COO’s team. Assume they both used a shared method in Employee
called regularHours()
. Now imagine the CFO’s team asks for a change from the development team to how pay is calculated. The developer sees that the change can be made by just manipulating the regularHours()
method but doesn’t realise this method is used in both ReportHours()
and CalculatePay()
. Now the developer has changed how ReportHours()
method in a way that the stakeholders do not want and therefore created a bug. This came about because the Employee
‘module’ is responsible to separate actors with different motives.
The solution to this is relatively straightforward, you can do something like the following:
type payer interface {
CalculatePay(data EmployeeData) int
}
type hourReporter interface {
ReportHours(data EmployeeData) int
}
type saver interface {
Save(data EmployeeData) error
}
type EmployeeData struct { ... }
type Employee struct {
payer payer
hourReporter hourReporter
saver saver
employeeData EmployeeData
}
func (e Employee) CalculatePay() int { return e.payer.CalculatePay(e.employeeData) }
func (e Employee) ReportHours() int { return e.hourReporter.ReportHours(e.employeeData) }
func (e Employee) Save() error { return e.saver.Save(e.employeeData) }
Here we have used something called a facade, the interface that the Employee
presents is a facade because it provides a simplified interface to a wider body of code. We also used a pattern called the delegate pattern. Here the Employee
delegates the requests to another class, which for CalculatePay()
will be some concrete implementation of the payer
interface. From now on I may refer to the Employee
as the delegator and to the payer
, hourReporter
and saver
as the delegates.
I’ve used a single type called EmployeeData
to hold all the employee data and pass it on to the methods of the delegates but that is only for convenience, they could have any number of arguments and the delegator could have any number of fields instead of a single data field. It could also be structured so that the delegates fetch the employee data themselves.
So how does this help? By structuring code in such a way we can separate the concrete implementations of the delegates into their own modules. Each of these modules will be responsible to only one actor. In our example, the CFO’s team will be responsible for the implementation of the payer
and so on for the other two. The regularHours()
function will be private members of the payer
and reportHours
implementations. This will prevent the hard to notice bug that the previous architecture produced.
Discussion
Just Add Tests!
There are a couple of interesting things to discuss here. The first is that a good suite of tests would have completely prevented this bug from going unnoticed. But that’s beside the point. The developer still would have broken the tests for ReportHours()
when the change was made to regularHours()
and then be forced to make some change. The developer has two choices here if they want to keep the same architecture as before, one is paramaterise regularHours()
or to create two versions of regularHours()
within the class. I would argue that this situation is confusing and should raise alarms. This still leaves the Employee
class vulnerable to the same bug. One of the new regularHours()
functions may be used for a new function CalculateOvertime()
for the COO, but which of the two regularHours()
functions is supposed to be used? The wrong one could be used quite easily.
What about DRY?
In case you don’t know, DRY stands for “don’t repeat yourself”. If we have two different functions to calculate the same thing then it will be very tempting to just merge them into one. Say the CFO’s team never asked us to make the change that required changing how hours were reported and the original way the Employee
class was built as a facade. Then in the implementations of hourReporter
and payer
we would have two identical implementations of regularHours()
. This is what we want, we don’t want one version of a function that is responsible to two stakeholders with different motives.
Conclusion
It’s a common misconception that the SRP means that a module should do one thing. I avoided mentioning this previously and just got straight to the point. What the SRP really means is that a module should only have one reason to change. A module only changes when requested by particular users or stakeholders, so the SRP means that a module should be responsible to only one user/stakeholder. This is what makes a module a cohesive set of functions and classes, a responsibility to one and only one actor.