In an orchestra, every musician has a distinct role. If a symphony requires some notes to be played on flutes, the flutists take care of that. If there are some notes to play on cellos, the flutists don’t run over and start playing them - they only take care of their own instrument while the cellists take care of theirs!
Because each performer only has a single responsibility, it makes the orchestra much easier to write music for. And it makes it much easier for the conductor to fix the problem if something goes wrong.
Classical orchestras are great at enforcing the single responsibility principle - that is:
A class or function should do only one thing - and do it well.
Another way of saying this is:
A class or function should only have one reason to be modified.
Why Use the Single Responsibility Principle?
You want everyone using your code to understand what is happening—keeping class responsibilities to a minimum aids in this.
First, it makes it easier to find the code you’re interested in—one idea, one place that implements it. In our card game, you know that anything associated with a playing card will be found in the PlayingCard class.
Second, unit tests are much easier to write. While we haven’t written any for our application, the tests for PlayingCard would only need to validate the few things a PlayingCard is responsible for (rank, suit, and whether it is face-up).
And finally, it’s easy to come up with a name for the class or function (since it’s clear what it’s supposed to do).
To summarize:
For each responsibility, ensure it is placed into the correct class.
If a class is doing too many different things, create new classes to separate out the responsibilities.
How Do We Apply the Principle to Our Code?
It’s is an easy principle to violate. It happens when you have to add new functionality to the system. Every bit of functionality has to go somewhere, right? It seems sensible to put the new stuff into an existing class. But now the class has more than one thing to do.
Let’s look at our card game. One of the first classes we implemented is PlayingCard. It knows its rank and suit, whether it’s face-up, and whether it’s better than any other card.
But, there’s a common situation in which we’d need to keep changing this class. Can you see what it is? 🤔
Yes—every time we want to use these cards for a different game with different rules (such as with the lower cards being worth more), we need to modify or replace the PlayingCard class! PlayingCard shouldn’t be responsible for implementing game logic, as then whenever the game logic changes, it must change in tandem.
In real life, you don't need different decks for every card game you play. The same rule should apply to our application.
So if not the PlayingCard class, which of the following could implement the winner evaluation without breaking the single responsibility principle?
GameController
View
Player
Try it out for yourself using this file: card-game-p3-ch2-d1.py. Then check out the solution in the video below:
The Player class shouldn’t have to change every time we play a new game, so the winner evaluation doesn’t belong there. I shouldn’t have to replace my friends when I decide to play a new game!
The View shouldn’t have the unrelated responsibilities of presenting the game and deciding who wins. I shouldn’t need to buy a new screen for each different game I want to watch.
So that leaves the GameController! It’s responsible for running the game, irrespective of who is playing or how it is being displayed. The only reason it has to be modified is if the rules of the game change.
Let’s Recap!
Single responsibility means any class or function should only do one thing, i.e., it should only have one reason to be modified.
It’s an easy principle to violate, as you add new features to the system. When adding in a new feature, think about:
What future changes might impact the class?
What could give the class more that one reason to be changed?
Remember, if a class mimics a real-life concept, it should only implement the one responsibility that corresponds to that real-life concept.
Let's look at adding more capabilities to our system, but not modifying what we currently have written by applying the open/closed principle.