SOLID: Single Responsibility Principle

  • 20/03/2016
  • 9 minuten leestijd

SOLID: Single Responsibility Principle

The second of the seven part series on the SOLID principles, this blog describes the first of the principles, the Single Responsibility Principle (SRP).

The SRP is described by the original author as follows:

A class should have only one reason to change [1]

which isn't very self-explanatory. For the time being I'll translate it to two sentences which use the word "responsibility", and come back to why these sentences mean the same later on. So, without further ado:

A class should have only a single responsibility, and should be the only class holding that responsibility

If you've never heard of the responsibility keyword in programming languages, don't be too discouraged, neither have I. Just like in most SOLID principles, this definition is more about behaviour than about anything else. In fact, please remember the term behaviour, as it will show up quite often in my discussions of the SOLID principles, or anything else related to OOP for that matter.

Responsibility

So what is meant by the word "responsibility"? To determine that, lets look at a code example of an often used type; a User, including some generic functionality:

<?php

class User
{
    /** @var  string */
    protected $email;
    /** @var  string */
    protected $encryptedPassword;
    /** @var  string */
    protected $name;
    /** @var  string */
    protected $street;
    /** @var  string */
    protected $city;

    /**
     * Sends an email to the user with $emailText as content
     *
     * @param string $emailText
     */
    public function sendEmail(string $emailText)
    {
        // ...
    }

    /**
     * Returns true iff the $nonEncryptedPassword is equal to the
     * $encryptedPassword after encryption has been applied to it
     *
     * @param string $nonEncryptedPassword
     * @return bool
     */
    public function authenticate(string $nonEncryptedPassword)
    {
        // ...
    }
}

So, lets put into words what this class does:

  • The User class keeps track of user-data.
  • The User class allows for sending emails.
  • The User class performs password authentication.

These sentences are all behaviours of the User class. Lets, for the moment, also assume these are the responsibilities of the User class (even though they need not be, I'll come back to that later).

The problem with multiple responsibilities

So what is wrong with this class? Well, imagine we want to replace this User with a new implementation that uses a different authentication scheme than a locally stored password? You'd say we'd have to implement a new User with fields relevant to the other authentication and overwrite the authenticate method, and that's all. And you would be right, except that the application would not be able to sent emails to the new user, and we have to re-implement that behaviour too, even though nothing about it has changed!

Thus, if a class has multiple responsibilities, we set ourselves up for a lot of work when we need to change the way a particular responsibility is being handled, and, being lazy programmers, we do not like a lot of work. Not to mention all of the bugs we'll introduce while re-implementing the mail method.

Of course, in almost all OO languages we can use the extends keyword to make a new class that inherits all of the behaviours of its parent, after which we change the one aspect we want to change, which would also solve the particular issue above. But there are a lot of issues with using the extends keyword in this way, which I consider out of scope (I love that term) for this particular post.

The "right" way

Solving the issues described above is as easy as it is obvious: just create a different class for each of the responsibilities.

<?php

class User
{
    /** @var  string */
    protected $email;
    /** @var  string */
    protected $encryptedPassword;
    /** @var  string */
    protected $name;
    /** @var  string */
    protected $street;
    /** @var  string */
    protected $city;
}

class Mailer
{
    /**
     * Sends an email to the $user with $emailText as content
     *
     * @param User $user
     * @param string $emailText
     */
    public function sendEmail(User $user, string $emailText)
    {
        // ...
    }
}

class Authenticator
{
    /**
     * Returns true iff the $nonEncryptedPassword is equal to the
     * $user's password
     *
     * @param User $user
     * @param string $nonEncryptedPassword
     * @return bool
     */
    public function authenticate(User $user, string $nonEncryptedPassword)
    {
        // ...
    }
}

If we need to change the manner in which users are authenticated in this example, we do not need to change the way mails are sent, thus preventing a lot of work and bugs.

However, applying the SRP is not always as clear cut as in this example, where each method is a responsibility, and moving those methods to a separate class is all we need to do. For example, by introducing a Mailer class, which appears to be responsible for sending mails, we also have to inspect other classes for this kind of responsibility and move it to the Mailer. This is in fact a good thing, as we'll know all mails are sent through this class, and this restricts the need for applying a single change to the way mails are handled in more than one place. For example, changing the SMTP host of the Mailer becomes a lot easier.

A less obvious example

Another example of less clear-cutness is when we once again inspect the (new) User object and wish to add a Company object

<?php

class User
{
    /** @var  string */
    protected $email;
    /** @var  string */
    protected $encryptedPassword;
    /** @var  string */
    protected $name;
    /** @var  string */
    protected $street;
    /** @var  string */
    protected $city;
}

class Company
{
    /** @var  string */
    protected $name;
    /** @var  string */
    protected $street;
    /** @var  string */
    protected $city;
    /** @var  string */
    protected $ceo;
}

So how are we now breaking the SRP? Well, let us once again take an example of a change request as starting point: we are asked to add a country field to the addresses. In the solution above, this one change request means having to change two classes, and all that entails (changes in code, unittests, etc...). This suggests we have a shared responsibility.

And we do: both classes are responsible for holding address data. Once again, this is easily solved by introducing a class that holds that responsibility and introducing a reference in the originating classes to an instance of that new class:

<?php

class User
{
    /** @var  string */
    protected $email;
    /** @var  string */
    protected $encryptedPassword;
    /** @var  Address */
    protected $address;
}

class Company
{
    /** @var  string */
    protected $ceo;
    /** @var  Address */
    protected $address;
}

class Address
{
    /** @var  string */
    protected $name;
    /** @var  string */
    protected $street;
    /** @var  string */
    protected $city;
    /** @var  string */
    protected $country;
}

We have now effectively separated authentication data (User) from address data (Address), and thus allowed ourselves to reuse the address data somewhere else (Company). As an additional bonus, if we ever need to add another type of address data to other objects (eg: a webshop Order, or a secondary address for the company), we do not need to reinvent the wheel yet again: we just have to add a address field to that object, and that's all.

Pittfalls

Obviously, when applying any of the SOLID principles too strictly, we'll end up making things worse instead of better. So I'll try to add some issues too look out for applying them in each of the discussions, so you'll know when SOLID becomes too dense.

Fragmentation

Earlier, I already hinted at this pitfall, which is based on the way we determine the responsibilities of a class. In the original example I claimed three different ones, but I could also have stated the responsibilities of the User class as follows:

  • Anything to do with a User

Which can be said to be a single responsibility. I would personally never use the word "anything" in a responsibility, but it does illustrate that the determination is not as clear cut as you'd think. The responsibilities could also have been stated to be

  • The User class keeps track of a user's email
  • The User class keeps track of a user's encryptedPassword
  • Etc...

All of which are proper behaviours of the User class. If I'd claim these to be the responsibilities of the User class, I'd have to make a separate class for each property of User, thus fragmenting the class into as many classes as there are variables. If we do that, we are basically once again programming in a procedural style, and, in my opinion, no longer in OO.

Pre-destination

Another pitfall of the SRP is when we try to determine beforehand what exactly the responsibility of a class is. Usually, when I create a User object, I already disconnect address data from the authentication object. I do this because in almost every project I was involved in that had Users, this issue came up sooner or later.

But what I was in fact doing by separating them in advance, is introducing complexity that the application did not (yet) need: there was no reason the User should not be responsible for address data of the user, until I introduced a Company object; determination of a responsibility is therefore always dependent on the ecosystem of the class.

Making development more complex due to wishes that may or may not occur later on, is of course not how we want development to work. It is definitely not agile —where we should only solve these issues when they arise— but even in a waterfall system it is unwanted, unless the design shows its need for it.

Therefore, trying to determine future changes in responsibilities before they arise is a common pitfall, one you've probably already fallen into once or twice without realizing even now! All in all the advice is to stick to the needs of the current design, and worry about extensions to the system later.

Conclusion

When we apply the SRP correctly, we reduce the number of steps we need to take to implement or change a feature. This is because the feature either clearly falls into one of the already existing responsibilities, or is a new responsibility altogether. In both cases, we need to change or add only a single class, and it should immediately be obvious which class that is.

This principle therefore reduces complexity of the code and promotes its maintainability by making it easier to understand.

However, applying the SRP is not always as obvious as it sounds, particularly because changes in the application also change the ecosystem of the application, which might implicitly change the responsibilities. This can not be prevented beforehand, so we need to constantly remain aware of how the changes in the application are impacted by the SRP.

Addendum: Original wording of the principle

Not that it really matters, but the original declaration of the principle differs from the one I used to explain its importance. Nonetheless, I'd argue that the two are essentially the same, which hopefully is clear now that you've read the full explanation of the principle. If it is not, just consider that if we've properly given each class a single, and only a single, responsibility, changes in how that responsibility is handled become the only reason to ever change the class itself.

So when the original text claims there should only be a one reason to change a class, it refers to behaviour, and not to anything else. There could be a lot of bugs in the code that are all "reasons" to change the class, but all of those bugs should relate to a single responsibility, and fixing that responsibility is the only reason to change the class.

Similarly, the responsibility of a class can change in a lot of ways, eg: when we are refactoring a User object to remove the address data from it. There can be many changes in the class for this reason, but all must come down to the one responsibility it holds: whether that changes, is implemented incorrectly or even incompletely: all changes are due to only one underlying reason.

References

  • Robert C. (2003). Agile Software Development, Principles, Patterns, and Practices. Prentice Hall. p. 95. ISBN 978-0135974445.