The SOLID design principles

One goal for the SOLID design principles is to limit the effects of change or extension on a design. Making a change to established software is a bit like casting a pebble into the sea: there will be an initial splash, followed by ripples of change spreading outward. When trying to fix or extend badly designed software, the initial splash radius covers everything; the ripples are large and lead to numerous problems. In well-designed software, the splash radius is tiny.

As a concrete example, consider a class to represent dominoes. Each tile has 2 numbers, from 0 to 6, leading to 28 distinct tiles. The class design looks more or less like a two-tuple. The overall collection of 28 tiles can be generated with a nested pair of for statements, or a generator expression with two for clauses.

In some games, however, the tiles could have an upper limit of 9, 12, or even 15. Having different upper limits leads to a change to the class that represents the overall collection of dominoes. In Python, this change may be as tiny as adding a default parameter, limit=6, to an object constructor. In poorly-designed software, the number 6 appears in more than one place in the class definition, and the change has a large splash radius.

While playing, some dominoes with double numbers, especially the double-six, can have special roles. In some games, double-numbers tiles are called spinners and have a dramatic impact on the state of play. In other games, the double-number tiles are merely used to determine who plays first, and have little overall impact. These changes in role can lead to changes in the design for a class definition. A SOLID design will isolate changes to one portion of the application, limiting the ripples from disturbing unrelated portions of the software.

In many games, the sum of the two numbers is the value of the tile; the winner's points are based on the sum of the values of the unplayed tiles in the other player's hands. This means that tiles with higher point values should often be played first, and tiles with lower point values represent less risk. This suggests a variety of policies for sorting tiles into order, leading to design variations.

All of these variations in the rules suggest we need to be flexible in our class design. If we focus on the rules for one specific game, then our class definition cannot easily be reused or modified for other games. To maximize the value of a class definition means providing a class general enough to solve a number of closely related problems. We'll start with a class definition containing a number of design flaws. The poor design is as follows:

import random
from typing import Tuple, List, Iterator

class DominoBoneYard:

def __init__(self, limit: int = 6) -> None:
self._dominoes = [
(x, y) for x in range(limit + 1)
for y in range(x + 1)
]
random.shuffle(self._dominoes)

def double(self, domino: Tuple[int, int]) -> bool:
x, y = domino
return x == y

def score(self, domino: Tuple[int, int]) -> int:
return domino[0] + domino[1]

def hand_iter(self, players: int = 4) -> Iterator[List[Tuple[int, int]]]:
for p in range(players):
yield self._dominoes[p * 7:p * 7 + 7]

def can_play_first(self, hand: List[Tuple[int, int]]) -> bool:
for d in hand:
if self.double(d) and d[0] == 6:
return True
return False

def score_hand(self, hand: List[Tuple[int, int]]) -> int:
return sum(d[0]+d[1] for d in hand)

def rank_hand(self, hand: List[Tuple[int, int]]) -> None:
hand.sort(key=self.score, reverse=True)

def doubles_indices(self, hand: List[Tuple[int, int]]) -> List[int]:
return [i for i in range(len(hand)) if self.double(hand[i])]

While this class can be used for some common games, it has many problems. If we want to extend or modify this definition for other games, almost any change seems to create a large splash with extensive ripples.

We'll call out a particularly difficult method, the can_play_first() method. In a double-6 game with 4 players, it's common for all 28 dominoes to be dealt. One of the 4 hands will have the double-6; that player goes first. In a 2-player variation, however, only 14 dominoes will be dealt, and there's a 50% chance that neither player has the double-6. To cover that common case, the rule is often stated as highest double plays first. This class doesn't easily find any double other than double-6.

This class should have been decomposed into a number of smaller classes. We'll look at each of the SOLID design principles in the following sections to see how they guide us toward better design. We want to build object models to better support solving a variety of closely related problems.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset