1.6 Design principles
Object-oriented design isn’t easy. No design is particularly easy. There are a number of guiding principles that can help make decisions. One of the more famous sets of principles is called SOLID. This is a handy acronym for five ideas that can help transform a design from a tangle of loose threads into a tightly knit (and warm) garment.
We’ll use these principles throughout the book. This is only a superficial introduction. The five principles are these:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
These principles apply widely in an object-oriented design. We need to note that the Liskov substitution principle is focused on inheritance and the ”is-a” relationship, something we’ve avoided in the previous examples.
The SOLID ordering is handy for remembering the principles, but it isn’t the most useful way to understand them. We’ll talk about them in a more practical sequence.
1.6.1 Interface Segregation Principle
We are talking about this principle first because it is essential for understanding the boundaries around a class definition. When wondering what to encapsulate, it helps to keep the interface as small as possible. When an object is too complicated, the interface can grow to reflect that complication, and the collaborating classes are forced to depend on methods and attributes that they don’t actually need.
The goal, then, is to keep the interface small. This will minimize the intellectual burden of understanding the class. It will also ensure that other classes can evolve and change without disastrous problems stemming from unwanted (or unexpected) dependencies.
1.6.2 Open/Closed Principle
One of the key ingredients in a well-done design is class definitions that are open to extension but closed to modification. We want to be able to add features to a class using techniques such as inheritance and composition. We’ll look at these design techniques closely in Chapter 3. We don’t want to have to “tweak” the implementation code.
One aspect of this principle is relying on compositions of multiple objects to create complex behavior. This fits with the Interface Segregation Principle by separating features into distinct classes. We can extend one of those classes without the risk of breaking all the other classes in the application or library.
The other aspect is to create classes that inherit features from a base class. Via inheritance, the subclass is usable wherever the base class is expected, but it does something more specialized — more useful or appropriate — than the base class did. There’s more to this inheritance idea, captured in the Liskov Substitution Principle.
1.6.3 Liskov Substitution Principle
This principle — named after Barbara Liskov, inventor of the one of the first object-oriented programming languages, CLU — offers advice to constrain how inheritance is used. We’ll set the details aside for now to look at the overall goals of well-done design.
If we have a base class, such as Container, we want all of the subclasses, Barrel, Basket, and anything else we might need to invent, to have the same interface as the base class. They’re all containers, each with unique implementation details. By having the same interface, any of the subclasses can be used in place of the base class.
When we’re using tools such as mypy or pyright to check our type annotations, these tools will warn us of Liskov Substitution problems. The errors will pinpoint the places where a subclass interface doesn’t match the promise made by the base class. We’ll look at this in detail in Chapter 7.
1.6.4 Dependency Inversion Principle
The name for this principle is a little confusing: inverted with respect to what? If we don’t know what the “right side up” is, how can we judge whether the dependency is “upside down”?
The easy, obvious dependency is to have one class explicitly name a class of objects with which it collaborates. This is easy and fun for tutorial examples and introductory programming classes. In the long run, however, when one class directly depends on another class, we have problems with making changes.
Imagine the Python code for the Apple class that directly names the Barrel class. This turns into an Open/Closed Principle problem. When we need to start shipping apples in large packing crates, the new PackingCrate class is an extension to the Barrel class. It was, in turn, an extension on some abstract base class, Container. We really don’t want to edit a lot of code to add the new PackingCrate.
The idea of dependency “inversion” tells us that the Apple class should only name the base Container class. That way, any kind of container in the family tree can be associated with an Apple instance. The concrete relationship between the Apple class and the Barrel class should be something configured at runtime; it shouldn’t be defined in the software at its foundation.
The idea of using the base class echoes the Liskov Substitution Principle. It helps implement the Open/Closed Principle. As we’ll see later, this principle is a kind of implementation detail and helps make sure that the other principles are followed.
1.6.5 Single Responsibility Principle
This principle, given first, seems like it’s really a summary of the others. A class will have a single responsibility.
To get to this ideal, we’ll need to start by segregating the interfaces. Once we’ve decomposed a class into pieces to simplify the interfaces, we need to make sure to keep the design open to extension but closed to modification.
After these two initial steps, we need to review the details to make sure that Liskov Substitution will work. And, of course, we need to avoid “hard-wired” dependencies that require code modifications. To do this, we need to be flexible, inverting the dependencies so that the code depends only on base classes.
Once we’ve thought through our design, using these design principles, we’ll find that our classes have a single, easy-to-summarize responsibility. We can then work on how the objects collaborate to create the desired software features.
This is — of course — only a sketch of the principles. Each has consequences and considerations that, well, fill this book.