Main menu

The Single Responsibility Principle

The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

It seems to generate a lot of confusion. Just a few days ago, Jon Reid had to clarify a misconception about that very principle:

SRP is misunderstood. Despite the name, it's not about some Platonic "single responsibility". No, it's "one reason to change." https://t.co/2iuCZH7oLd

There, he refers to the definition of Robert C. Martin, who expresses the principle as, "A class should have only one reason to change." In this post, I want to write down my own thoughts about the "Single Repsonsibilty Principle". I hope I can clarify a few things, and do not add too much new confusion ;)

A Definition that "Works for Me"

So, some of the confusion comes from "single responsibility" vs. "single reason to change". The definition that talks about "responsibilities" is hard to follow in practice: What exactly is a responsibility? What if I can divide the responsibility in multiple sub-responsibilities? How can I make sure that all the code is part of the same responsibility?

On the other hand, "one reason to change" sounds more like a heuristic to me than a real definition. Yes, when a class has many different responsibilities, it also has many reasons to change. But if that was the definition, we should rename the principle to "SRTCP" (Single Reason To Change Principle).

So, I was searching for a definition that works for me and that gives me some guidance in my day-to-day work. After discussing with several very good developers, I nowadays really like the following definition:

You can describe everything a design element (method, class, module, ...) does - at a reasonable level of abstraction - as a single, coherent thing.

In other words, if you use "CRC cards" to desing your classes, the "Responsibilities" column should contain a single bullet point.

If the level of abstraction is too high, you can describe everything as a single thing ("Is a part of the XYZ system"). If the level of abstraction is too low, everything has many responsibilities ("Execute assembler instruction x. Execute assembler instruction y. ..."). So what is a "reasonable" level of abstraction?

We'll come to that soon, after an example...

Hangman

When I teach TDD workshops, I ask the attendees to implement Hangman (the word guessing game) - even multiple times. I now want to discuss with you a few possible designs for implementing this game (none of them is the best possible design, of course).

Let's start simple. The whole problem is so easy, you can implement everything in a single class:

This design has a few disadvantages. Most glaringly, in a TDD class: It is really hard to test. Especially if you do not make any compromises like making methods public that should actually be private.

We can try to simply split the class along the four responsibilities that we have already identified:

Now you can easily test three of the four classes, and with some work, you can probably even test the UI. And you can test every class in complete isolation from the other classes, which is great for achieving stable tests...

Is 4 classes for such a simple problem over-engineering? Quit possibly. But I am trying to make a point here...

To add clarity to your design, make sure that all design elements in a level are roughly on the same level of abstraction (yes, there is gut-feeling involved in deciding that).

So, all the public methods in a class should be roughly on the same level of abstraction, and the class itself would be on a higher level. They delegate to private methods of that class to do the real work, which are on a lower level.

Sometimes you can find interesting responsibilities by looking at the tests of a class or method. And when you split it, you might need new design elements (a new package or a new class) to keep everything at roughly the same level of abstraction.

Tests and Responsibilities

So, I wrote some tests for the "Rules" class - This time using a different design, where I do not split out the game state to its own class. Here is the output of Jest, a JavaScript Test Runner:

Hangman - Implements the flow of a single Hangman game, given a secret word.
√ returns a hint that contains only underscores at the start of the game
√ shows a hint with the correct length for the secret word "test" at the start of the game
√ shows a hint with the correct length for the secret word "a" at the start of the game
√ shows a hint with the correct length for the secret word "few" at the start of the game
√ shows a hint with the correct length for the secret word "cases" at the start of the game
√ updates hint to "c____" after guessing "c" when word is "cases"
√ updates hint to "c_s_s" after guessing "c,s" when word is "cases"
√ updates hint to "c_ses" after guessing "c,s,e" when word is "cases"
√ does not update the hint when making a wrong guess
√ decrements the number of remaining tries after a wrong guess
√ does not decrement the number of wrong guesses after a right guess
√ indicates game is over ("Lost") when there was only one guess remaining and the user guessed wrong
√ indicates game is over ("Won") when the user guessed all letters of the secret word
√ does not accept any input after the game is over

Oh, some of these tests seem to belong together. Let's group them, and look at the test output again:

Hangman - Implements the flow of a single Hangman game, given a secret word.
Generates Hints from the secret word and the input
√ returns a hint that contains only underscores at the start of the game
√ shows a hint with the correct length for the secret word "test" at the start of the game
√ shows a hint with the correct length for the secret word "a" at the start of the game
√ shows a hint with the correct length for the secret word "few" at the start of the game
√ shows a hint with the correct length for the secret word "cases" at the start of the game
√ updates hint to "c____" after guessing "c" when word is "cases"
√ updates hint to "c_s_s" after guessing "c,s" when word is "cases"
√ updates hint to "c_ses" after guessing "c,s,e" when word is "cases"
√ does not update the hint when making a wrong guess
Keeps track of remaining guesses, so UI can draw the gallows pole
√ decrements the number of remaining tries after a wrong guess
√ does not decrement the number of wrong guesses after a right guess
Keeps track of whether the game is running or over (Won / Lost)
√ indicates game is over ("Lost") when there was only one guess remaining and the user guessed wrong
√ indicates game is over ("Won") when the user guessed all letters of the secret word
√ does not accept any input after the game is over

It seems like this class has three different responsibilities (at least at some level of abstraction). So, if I wanted, I could split this "Rules" class even further, into one class for each of the groups, and one to coordinate them. Then I would probably need a package to group these two "Rules" classes, and the responsibility of that package could now be "Implements the state changes of a single game, based on the rules".

Does it always make sense to split a class like that? That depends on a lot of things, but from the perspective of the Single Responsibility Principle, we could do it...

Conclusion

The Single Responsibility Principle gives you an indicator when to change your design. Split your methods / classes / modules when they have more than one responsibility. Restructure code when your classes / methods / modules do not fully encapsulate that responsibility.

When your design elements have many different responsibilities, they have many reasons to change. And they are also hard to test. When your design elements do not reasonably encapsulate their responsibility, changes will cascade through your code. And again, it will be harder to test.

But do not start to split all your classes alon their reponsibilities right away! The SRP should not be the only driving force of your designs - There are other forces, and sometimes they give you conflicting advice. Take, for example, the SOLID principles - 5 design principles, where the SRP is only one of them.

My name is David Tanzer and I have been working as an independent software consultant since 2006. I help my clients to develop software right and to develop the right software by providing training, coaching and consultanting for teams and individuals.