Design: Single Responsibility Principle

Probably the best principle to follow in software engineering, and in life in general.

John is your unicorn employee. He is as good at developing software as he is at selling it. So he’s responsible for both. One may find this organization simpler than having multiple people to manage, but changes can have a detrimental impact to John’s activities:

So maybe having John handle multiple responsibilities is not the simplest option after all.

In a way, John is a piece of the software that runs your company. And, indeed, software development shares exactly the same concerns: in the seek of agility (tolerance to change), we need a rule of thumb to design things that have a high acceptation for change. One way to find such a rule is to identify what makes change difficult.

Basically change becomes difficult when it triggers some kind of chain reaction: you’re being asked to update some business rule or technical behavior and you realizes that means changing a lot of things 😱. Not only it increases the cost of the change, but the widened scope of your work also increases the risk of breaking things, a.k.a. regression.

To avoid this, any design should make sure that any of those change targets, or “responsibility” should be located at a single place.

Let’s take our unicorn employee example again, and design it like the mess we described:

class Employee {   projects: Set<Project>
contracts: Map<Contract, Client>
constructor(vcs) {
this.vcs = vcs
}

commit(proj, code) {
this.vcs.commit(proj, code)
}

sign(contract, client) {
this.contracts.put(contract, client)
}
}

Looks like an employee is doing a bunch of different things, right? Actually a newcomer may have a hard time determining what is the purpose of this class… as it has many. It may also be confusing to read the commit or sign API which can be interpreted both development or sales operation.

Another oddity appears if we want to perform a couple of sales task for instance:

var vcs = new Git()
var john = new Employee(vcs)
var client1 = new InsurCorp()
var contract1 = new Contract()
john.sign(contract1, client1)

Why on earth would an employee need a VCS to sign a contract? If I want to test this software call, I would have to instantiate a VCS for… nothing. Maybe someone not familiar with development acronyms could even assume that a VCS is a sales thing.

Now let’s add a method to allow an to take some rest and let another employee take over:

class Employee {  projects: Set<Project>
contracts: Map<Contract, Client>
constructor(vcs) {
this.vcs = vcs
}

commit(proj, code) {
this.vcs.commit(proj, code)
}

sign(contract, client) {
this.contracts.put(contract, client)
}
goVacation(backup: Employee) {
backup.projects.add(this.projects)
backup.contracts.putAll(this.contracts)
}

}

Hey, wait! Now that I allow more than one employee I realize that it means that all employees should be unicorns able to handle both sales and development. It also looks that I cannot hand over an employee contracts without handing over its projects as well. Some coupling between development and sales arises here, along with the need for a better design.

Let’s refactor this as two classes with distinct responsibilities:

class Developer {  projects: Set<Project>  constructor(vcs) {
this.vcs = vcs
}

commit(proj, code) {
this.vcs.commit(proj, code)
}

goVacation(backup: Developer) {
backup.projects.add(this.projects)
}
}

Obviously this class is simpler to understand that the previous one. No wonder what commit is related to. Also, I can make sure that his projects will be handed over to another Developer that will be able to handle them.

The other class looks even more simple:

class Salesman {  contracts: Map<Contract, Client>

sign(contract, client) {
this.contracts.put(contract, client)
}
goVacation(backup: Saleman) {
backup.contracts.putAll(this.contracts)
}
}

No more need for a VCS to instantiate a Salesman, and this make more sense indeed.

As the result of the refactoring, both Developer and Salesman classes are now simpler in themselves (so, more maintainable) and more cohesive (their code is using all their state, not a part of it).

This refactoring follows the Single Responsibility Principle, coined by Robert C. Martin, a.ka. “Uncle Bob” and author of the Clean Code best seller. It states that:

A class should have only one reason to change.

As we’ll see, “class” could be replaced by many things actually, as this principle applies in many areas.

Basically that means that a class (or a component, or a function, or a package, it depends on the abstraction level you’re talking from) should take care of a one single concern*, no more.

This principle promotes simplicity as, the less concerns you’ll have:

Examples

The story of John was more a metaphor, a personification of a software piece. More concretely, here are several examples of applying the SRP.

Widgets

UI Components are good examples of SRP because they are usually “dumb”, i.e. they don’t know what they are used for. A button’s code will never include the implementation of the action it triggers for instance. Instead, it provides an asynchronous API that allows to specify what to execute when the button will be pressed.

The same principle should apply to every reusable component of your own. If you build a more complex component such as a ContactList, it should focus on displaying a contact list and that only. It should not care about what happens when a contact is selected, added or deleted. Instead it should allow callers of this ContactList to specify some routines to call back when those events occur.

What is the benefit of this?

Layering

When you split a design (an architecture typically) in layers, it is a way to split responsibilities to lessen coupling between those layers. Typically you don’t want a business rule to span across those layers, as changing it would require a consistent set of changes in different places.

Data structure

The danger here is about hacking/patching a structure to allow conveying data about different things instead of clearly defining different structures.

There could be multiple examples of this, but one of the most common is about hacking a model because of the way it is stored. Suppose you have some Customer data you want to save in a database:

interface Customer {
id: UUID
data: CustomerData
}

Every field is mandatory because it doesn’t make sense to store a Customer if you haven’t any data about it, and even more if you haven’t any key to find it.

But the problem is: you choose the IDs to be database generated. That is, at insertion time, the database will decide of the id value. So you can’t require your insertion code to have it before.

interface Customer {
id?: UUID // Database generated
data: CustomerData
}

For sure it can work, but what you got now is a totally corrupted notion of what a Customer is: something without a mandatory id. This is plainly false, and other developers could take it for granted. Even if not, they will now have to check that id is defined everywhere in the code (or worse, tell the compiler to not care about it). You have just ruined your design.

Why this? Because you wanted that data structure to play two roles at the same time (customer definition and customer persistence). You should have rather added a separate data structure to convey the second information.

Functional programming

Think SRP only applies to Object-Oriented design?

A function for every action

is a pretty good translation of it for the functional paradigm, to help you avoid mixing multiple responsibilities in a single code unit.

Extending it

As we mentioned before, as all principles the SRP should be applied at the relevant level of abstraction. Let‘s have a look at other levels than objects:

Conclusion

If one only, the SRP should be the principle that every developer should follow, as it promotes:

*SRP should not be confused with Separation of Concerns (SoC). While the latter sounds better and even more intuitively close to what this article aim to express, it was not meant to it. When he formulated it first in 1974, Dijkstra was talking about the different steps of software development, and recommended to isolate the concerns of each of those steps (“what is expected”, “how to do it”, “is it valuable”, etc.) one from each other.

Software engineer for three decades, I would like to share my memory. https://javarome.com