The Interface Segregation Principle

One definition of the Interface Segregation Principle (ISP) is that clients should not be forced to depend on methods that they do not use. The idea is to provide the smallest number of methods in a class. This leads to focused definitions, often separating a design into multiple classes to isolate the impact of any changes.

This principle seems to have the most dramatic impact on a design because it decomposes the model into a number of classes, each with a focused interface. The other four principles seem to follow from this beginning by providing improvements after the initial decomposition.

The type hints embedded in the class definition shown earlier suggest there are at least three different interfaces involved in the class. We can see type hints for the following:

  • The overall collection of dominoes, List[Tuple[int, int]], used to deal hands. 
  • Each individual domino, defined by a type hint in the form of Tuple[int, int].
  • A hand of dominoes, also defined by the type hint in the form of List[Tuple[int, int]]. This is, perhaps, an ambiguity that is exposed by having similar types with different purposes.
  • The doubles_indices() query about specific dominoes within a hand, where the result is List[int]. This may not be different enough to merit yet another class definition.

If we decompose the initial class based on these separate interfaces, we'll have a number of classes that can then evolve independently. Some classes can be reused widely in a variety of games; other classes will have to have game-specific extensions created. The revised pair of classes is shown in the following code:

class Domino(NamedTuple):
v1: int
v2: int

def double(self) -> bool:
return self.v1 == self.v2

def score(self) -> int:
return self.v1 + self.v2


class Hand(list):

def __init__(self, *args: Domino) -> None:
super().__init__(cast(Tuple[Any], args))

def score(self) -> int:
return sum(d.score() for d in self)

def rank(self) -> None:
self.sort(key=lambda d: d.score(), reverse=True)

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

The Domino class preserves the essential Tuple[int, int] structure, but provides a sensible name for the class and names for the two values shown on the tile. A consequence of using a NamedTuple is a more useful repr() value when we print objects.

The __init__() method of the Hand class doesn't really do much useful work. The cast() function applied to a type, Type[Any], and the args object does nothing at runtime. The cast() is a hint to mypy that the values of args should be considered as having the Tuple[Any] type, instead of the more restrictive Domino type. Without this, we get a misleading error about the list.__init__() method expecting objects of the Any type.

The score of a Hand instance depends on scores of the various Domino objects in the Hand collection. Compare this with the score_hand() and score() functions shown previously. The poor design repeats important algorithmic details in two places. A small change to one of these places must also be made to another place, leading to a wider splash from a change.

The double_indices() function is rather complex because it works with index positions of dominoes rather than the domino objects themselves. Specifically, the use of for i in range(len(self)) means the value of the i variable will be an integer, and self[i] will be the Domino object with the index value equal to the value of the i variable. This function provides the indices of dominoes for which the double() method is True.

To continue this example, the overall collection of dominoes is shown in the following code:

class DominoBoneYard2:

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

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

This creates individual Domino instances when the initial set of dominoes is created. Then, it creates individual Hand instances when dealing dominoes to players.

Because the interfaces have been minimized, we can consider changing the way dominoes are dealt without breaking the essential definition of each tile or a hand of tiles. Specifically, the design as shown doesn't work well with the tiles not dealt to the players. In a 2-player game, for example, there will be 14 unused tiles. In some games, these are simply ignored. In other games, players are forced to choose from this pool. Adding this feature to the original class runs the risk of disturbing other interfaces, unrelated to the mechanics of dealing. Adding a feature to the DominoBoneyard2 class doesn't introduce the risk of breaking the behavior of Domino or Hand objects.

We can, for example, make the following code change:

class DominoBoneYard3(DominoBoneYard2):

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

This would preserve any undealt dominoes in the self._dominoes sequence. A draw() method could consume dominoes one at a time after the initial deal. This change does not involve changes to any other class definitions; this isolation reduces the risk of introducing astonishing or confusing problems in other classes.

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

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