One of my son’s chores is emptying the dishwasher. Apparently this doesn’t include putting the glasses into the cupboards - only the plates. He’s very good at enforcing the single responsibility principle. That is:
A class should do one thing, and do it well. Another way of saying this is a class should only have one reason to change.
Why Use the Single Responsibility Principle?
You want to be able to understand and have others understand what is happening — keeping the responsibilities of a class 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, we know that anything associated with a playing card is going to 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 faceUp).
And finally, it’s easy to come up with a class name (since it’s clear what the class does).
To summarize our approach:
Examine the requirements to determine what the code needs to do.
Partition the code into MVC responsibilities.
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?
This 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 often seems sensible to put the new stuff into an existing class. But now the class has more than one thing to do well.
Let’s look at our card game. One of the first classes we implemented is PlayingCard. It’s pretty straightforward: a rank, a suit, and whether the card is face-up or down. This implementation could be used in many different card games. Just like in real life! You don’t need a new deck for every game you play.
Now we want to add the actual game evaluation to determine which player has the best card. One approach would be to add a “isBetterThan(PlayingCard other)” method to the PlayingCard class. And this sort of makes sense. PlayingCards know about each other, so adding this poor implementation would be easy:
// poor implementation
// game specific logic embedded in model
class PlayingCard {
Rank rank;
Suit suit;
boolean faceUp;
public bool isBetterThan(PlayingCard other) {
// perform rank and suit evaluation here
}
}
But, the class now has a new reason to change. Can you see what it is? 🤔
If the rules change, we have to change PlayingCard. And if the rules change again, we edit this class once more. As mentioned above, in real life, you don't need different decks of cards for every card game you play. The same rule should apply to our application. The class has more than a single responsibility.
The evaluation of who wins belongs somewhere else. Currently, this code is buried in the controller. It also violates single responsibility. Deciding who wins doesn't change the flow of interactions (the controller's job). Can you think of a way to implement the winner evaluation and still enforce single responsibility?
Let me introduce you to the class GameEvaluator. It has a method that determines which player is holding the best card and it's the only reason for change!
Let's do it together!
public class GameEvaluator {
public Player evaluateWinner(List<Player> players);
}
Speaking of change, in the next chapter, we will change the rules of the game, showing the benefit of the open/closed principle.
Let's Recap!
Single responsibility means a class should only do one thing, or to put another way, only have one reason to change.
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 change?
Remember, if a class mimics a real life concept, it should only implement the one responsibility that corresponds to the 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.