The Open/Closed Principle

The Open/Closed Principle (OCP) suggests two complementary objectives. On the one hand, a class should be open to extension. On the other hand, it should also be closed to modification. We want to design our classes to support extension via wrapping or subclasses. As a general habit, we'd like to avoid modifying classes. When new features are needed, a sound approach is to extend classes to add the features.

When we want to introduce changes or new features, the ideal path is via extension of existing classes. This leaves all of the legacy features in place, leaving the original tests in place to confirm no previous feature was broken by adding a new feature.

When keeping a class open to extension, there are two kinds of design changes or adaptations that arise:

  • A subclass needs to be added where the method signatures match the parent class. The subclass may have additional methods, but it will contain all of the parent class features and can be used in place of the parent class. This design also follows the LSP.
  • A wrapper class needs to be added to provide additional features that are not compatible with another class hierarchy. A wrapper class steps outside direct Liskov Substitution because the new features of the wrapper will not be directly compatible with the other classes.

In either case, the original class definitions remain unmodified as the design evolves. The new features are either extensions that satisfy the LSP, or wrappers that create a new class from an old class definition. This kind of design is a consequence of keeping the classes open to extension.

Our example classes, DominoBoneYard2 and DominoBoneYard3, shown previously, both suffer from a profound failure to follow the OCP. In both of these classes, the number of tiles in a hand is fixed at seven. This literal value makes the class difficult to extend. We were forced to create the FancyDealer4 class to work around this design flaw.

A better design of the DominoBoneYard2 class would lead to easier extension to all of the classes in this hierarchy. A small change that works very nicely in Python is to make the constant value into a class-level attribute. This change is shown in the following code sample:

class DominoBoneYard2b:

hand_size: int = 7

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[Hand3]:
for p in range(players):
hand = Hand3(self._dominoes[:self.hand_size])
self._dominoes = self._dominoes[self.hand_size:]
yield hand

The DominoBoneYard2b class introduces a class-level variable to make the size of each hand into a parameter. This makes the class more open to extension: a subclass can make changes without the need to modify any further programming. This isn't always the best kind of rework, but it has the advantage of being a very small change. The Python language facilitates these kinds of changes. The self.hand_size reference can either be a property of the instance, or a property of the class as a whole.

There are other places we can open this class to extension. We'll look at some of them as part of the Dependency Inversion Principle. 

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

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