In our pizza restaurant, Pythonia, the waiters and waitresses are very highly skilled at their job. They obey the single responsibility principle by doing only one sort of thing (serving customers).
However, they only speak English. Let’s imagine we wanted to train them to communicate in Italian for the convenience of all the Italian-speaking customers.
This shouldn’t modify their existing skillset - they will still carry the pizzas in the same way and greet English-speaking customers in the same way. But after their Italian training, they have extended their skillset.
In this way, you could say that the waiters and waitresses are open for extension but closed for modification.
Could the same idea be applied to classes in Python?
Great guess! This brings us to the open/closed principle, which states that:
A class should be open to extension but closed to modification.
Why Use the Open/Closed Principle?
If you aren’t modifying the existing code, you know you aren’t breaking any of it. All the new functionality is contained in the newly added function(s) or class(es).
The hardest part about this principle is recognizing when it can be applied before you begin coding. Here are a couple of guidelines to help you recognize when open/closed may be applicable.
When you have algorithms that perform a calculation (cost, tax, game score, etc.): the algorithm will likely change over time.
When you have data coming or going from the system: the endpoint (file, database, another system) is likely to change. So is the actual format of the data.
How Do We Apply the Open/Closed Principle to Our Code?
One place for modification is in the winner evaluation. In the last chapter, we created an evaluate_game
function in the GameController class. What if we changed the game so that the lowest card wins? Then we’d have to modify GameController - maybe by adding a Boolean parameter to say "find the high card winner" vs. "find the low card winner":
class GameController:
def __init__(self, deck, view, high_card_wins=True):
# Model
self.players = []
self.deck = deck
# View
self.view = view
# Controller
self.high_card_wins = high_card_wins
def evaluate_game(self):
best_rank = None
best_rank_suit = None
best_candidate = None
for player in self.players:
this_rank = RANKS[player.hand.card_by_index(0).rank]
this_suit = SUITS[player.hand.card_by_index(0).suit]
if (best_rank is None
or (self.high_card_wins
and ((this_rank > best_rank)
or (this_rank == best_rank
and this_suit > best_rank_suit)))
or (not self.high_card_wins
and ((this_rank < best_rank)
or (this_rank == best_rank
and this_suit < best_rank_suit)))
):
best_candidate = player.name
best_rank = this_rank
best_rank_suit = this_suit
return best_candidate
Ugh, that’s complicated enough! But what happens if we wanted to add even more rules?! 😨 This class becomes difficult to understand, test, and maintain.
And furthermore, now everyone else who is using our GameController class has to implement this Boolean parameter. Not a good idea.
So, what should we do?
Really, the winner evaluation method should be defined outside of the GameController class, in a separate function or class.
We can make a separate GameEvaluator class to do the evaluation and pass an instance of it into GameController. Then evaluate_game
calls the relevant method from this GameEvaluator object.
Then when we add alternative rules, such as the low card winning, we can create a LowCardGameEvaluator
and use that instead.
Let’s try this together!
So how well are we following SOLID principles so far?
GameController still has a single responsibility.
GameEvaluator is a new class, also with a very clear single responsibility.
GameController remains open to an extension of the winner evaluation rules but closed to modification. Alternative rules for evaluating the winner can be added without affecting existing implementations.
But wait - it’s possible to play a game with more complicated rules, where the winner somehow depends on something else like the cards still in the deck. We would still have to modify the GameController class!
Yes, you’re right. It’s unrealistic to write code that will never need to be modified, and sometimes breaking changes are indeed required, which cause other code to be modified to accommodate the changes.
But the closer you are following the open/closed principle, the less frequent and less troublesome these changes will be.
Let’s Recap!
The open/closed principle says that classes should be open to extension but closed to modification.
In existing systems, it might take some rework to get the code in a position to take advantage of open/closed.
In the next chapter, we will look at avoiding misuse of inheritance with Liskov substitution.