SOLID: The Single-Responsibility Principle (SRP)
The Single Responsibility Principle is harder to grasp than its name implies, with a history of misleading definitions. We're going to be pedantic about the definition right out of the gate, to ensure we come away with the right understanding.
Definition
The original and most quoted definition of SRP is this:
A class should have only one reason to change.
Excuse me, I'm here for "The Single-Responsibility Principle." This is it? Oh, I expected something else...
So is it about change or responsibilities? I'm not the first to be confused by this, so much so that Bob later clarified β or, more accurately, an attempt was made to clarify (assuming he's not just messing with us all).
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
On the surface, this seems like a useful definition. It does explain something better than the first, and gives us some instruction.
But what the hell does it explain? Certainly not the principle, as I eventually learned. I used to think the SRP was about units of code having just one responsibility, and so still do many others; who can blame us? But we were wrong.
Looking for a useful and canonical definition from the horse's mouth, I was eventually led to Clean Architecture, a book written by Bob in 2017. In it, Bob iterates over three new attempts to define his principle β admitting that he hadn't quite got it down before β and lands on this:
A module should be responsible to one, and only one, actor.
This is what Bob was trying to convey all along, and it should be the only definition we remember of those thus far mentioned.
It took some twelve years to reach this definition, which means that plenty of engineers had already solidified their understanding of SRP to instead mean something closer to Separation of Concerns, which is arguably the more useful principle anyway. But it's not what we're here to discuss.
So, "a module should be responsible to one, and only one, actor." In his book, Bob goes on to explain that an "actor" could be considered a group, a role, or a stakeholder that wants the system to change in some way. Yes, we are talking about a person or group of persons.
As for what a "module" is, Bob roughly defines it as a source file or some language-equivalent. Personally, I believe we can apply this and other principles to different levels of granularity β a function, a module, a library β so long as the thing is cohesive and may be treated as one thing.
In my words, the Single-Responsibility Principle says that a module should only change because of different requirements from a single person or group of people sharing the same role.
The principle's name is commonly taken to mean that "code should have a single responsibility," but in fact should be taken to mean that "code should be responsible to a single person/group/etc."
There is still room for interpretation, but I think it's time to move onto an example.
Example
We run a simple web forum. Users can create topics, and write posts in those topics. Admins moderate content β deleting what's inappropriate, locking incendiary topics, etc.
Each day, everyone with an account receives a digest email. This digest includes a list of the day's popular topics. If the user is an admin, it also includes a list of posts that were reported as inappropriate.
Focusing on the email content, here is a naΓ―ve implementation:
class DailyDigest
def initialize(user:)
@user = user
end
def body
if user.admin?
admin_body
else
user_body
end
end
private
attr_reader :user
def admin_body
template { admin_content }
end
def user_body
template { user_content }
end
def admin_content
<<~CONTENT
# Requiring moderation
#{posts_requiring_moderation}
#{user_content}
CONTENT
end
def user_content
<<~CONTENT
# Popular topics
#{popular_topics}
CONTENT
end
def template
<<~TEMPLATE
Hi #{@user.name}!
This is your daily digest.
#{yield}
Best regards,
The Daily Digest Team
TEMPLATE
end
end
A naΓ―ve implementation which violates SRP.
We use a template for details common to all emails, then include content specific to the role. Users get only the popular topics, and admins additionally get posts that require moderation. It is important to note that the admin content includes the user content as well.
This code violates the Single-Responsibility Principle because it is responsible to β and therefore changes at the whims of β two actors: normal users and admins. We can resolve this by splitting content generation for admins and users into separate modules.
class DailyDigest
class Body
def body
<<~BODY
Hi #{user.name}!
This is your daily digest.
#{content}
Best regards,
The Daily Digest Team
BODY
end
def initialize(user:)
@user = user
end
private
attr_reader :user
end
class UserBody < Body
def content
<<~CONTENT
# Popular topics
#{popular_topics}
CONTENT
end
end
class AdminBody < Body
def content
<<~CONTENT
# Popular topics
#{popular_topics}
# Requiring moderation
#{posts_that_need_moderation}
CONTENT
end
end
def initialize(user:)
@user = user
end
def body
if user.admin?
AdminContent.new(user:).body
else
UserContent.new(user:).body
end
end
private
attr_reader :user
end
A refined implementation which resolves the SRP issue.
The astute reader will notice that we've introduced some duplication in this example. This was intentional, and should make sense shortly.
What Does This Principle Prevent?
We know what the principle is, and how to implement it, but we might still be unaware as to why it exists.
SRP is designed to mitigate code changes that have unintended consequences.
Suppose that our users want the digest to include the number of likes their posts received for the day. Coming back to the initial implementation, a developer would see user_content
and make the change there:
def user_content
<<~CONTENT
# Popular topics
#{popular_topics}
# Today's likes
#{like_count}
CONTENT
end
But recall that admin user_content
is in fact included in admin_content
. The above change will therefore alter both the user and admin digests β admins too will receive a like count in their emails, which they might not appreciate. By separating the code based on the actors that it answers to, cross-contamination like this doesn't happen.
The duplication we introduced in our refactor is in displaying the popular topics. We could accept this minor duplication or abstract it away to some more generic content container (or simply the template
method), so long as we're clear that it's generic content that is included for all users.
Takeaway
The Single-Responsibility Principle is easily and commonly confused with other principles. We should ignore early definitions and remember only this one:
A module should be responsible to one, and only one, actor.
Personally, I don't think this principle is important enough to belong on the pedestal we put SOLID. It certainly has its place and it does serve us to think about the actors who might dictate changes to our code, but in practice β at least in my experience β it doesn't often cause issues that can't be addressed as and when those issues become clear.
One might argue that separating code as we did in the example could be premature abstraction and just lead to more complexity. I would instead advocate making the existing code clearer β for example, using generic_content
rather than user_content
to demonstrate that it is used for more than just users β to begin with, and abstracting only as and when needed.
Further Reading
- The Single Responsibility Principle β Robert Martin's clarification post from 2014.
- Coupling and Cohesion β The core concepts that this principle is really trying to get at.
- Separation of Concerns β A related principle, not part of SOLID.
- I've vastly misunderstood the Single Responsibility Principle β There are dozens of us!
- Single Responsibility Principle β The best article I've found on SRP; Mienxiu gets it right.
- Clean Architecture β Bob's book which includes his best definition of SRP yet.