πŸ–‹οΈ
This article is one part of my series on Robert (Bob) C. Martin's SOLID principles.

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