This blog is part of a series explaining the SOLID design principles by looking at code from real projects or frameworks. SOLID is an acronym for the following design principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
People often explain the SOLID principles by using simple examples, but that sometimes makes it hard to spot them in your own code for a real project. That’s why I searched for some examples of these issues in Open Source projects and use them in this series.
The Liskov Substitution Principle
This Design Principle, defined by Barbara Liskov in 1988, states that subtypes must be substitutable for their base types. To be more precise:
“What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.”
Your initial reaction when first seeing this principle might be “But this is something that strongly-types OO languages like Java or C# already enforce right?” You can’t inherit from a base class or interface and not implement all the functionality that is required. You’ll get a compilation error. So therefore, you can always substitute an object for an object of a subtype. This is the heart of polymorphism.
But that is not what this principle is all about. Instead, the principle is about the behavior of the subtype instead of the structure. Strongly-typed OO languages force a consistent structure onto you, but not necessarily a consistent behavior. And this can lead to runtime errors instead of compilation errors, making violating this principle way more dangerous.
An example violation in the wild
Let’s look at some code from the NLog framework, a .NET framework that deals with logging and is similar to frameworks like Log4j in Java. I selected this project not because it has low quality code but because it is a medium sized project and has multiple contributors. Chances are that you’ll find issues like this in any reasonably sized project, either introduced consciously or unconsciously. That is one of the things that make Open Source great: we can be exposed to others’ work learn from it.
In this project, you’ll find these classes (simplified to focus on the violation):
With the code looking like this:
Can you spot the problem? The abstract class TargetWithLayout exposes a property called Layout with a get and set. But the implementation of the Layout property in NLogViewerTarget ignores the value when set.
If I use it as the latter code snipplet shows, I expect my provided CsvLayout to be used. But if the parameter target is of subtype NLogViewerTarget, my provided layout is ignored! Instead, it will always return an object of the type Log4JXmlElementLayout. This means that the behavior of the NLogViewerTarget is not consistent with that of the higher-level classes and could lead to subtle runtime bugs that are difficult to debug.
How to spot these kind of violations
You want to search for subtypes that don’t behave in the same way as their base classes or interface. This is of course quite difficult, except in cases where the subtype is doing less than the base class or interface describes, like the example used. Therefore, you could begin by finding this kind of violations.
Using multiple layers of inheritance also increases the risk of running into this problem. You increase the chance of having to create exceptions on certain behavior in your subtypes.
How to solve these violations
Think whether you created the right abstraction and structure. If you are using multiple layers of inheritance, try using composition instead of inheritance. In the example this means that Layout is not part of the base class. Instead, it is its own abstraction that Target implementations use using composition if needed (using design patterns like Strategy). You could also create a separate interface for it (something like IConfigurableLayout) and implement it by relevant Target subtypes.
In the example, I don’t know the reasoning behind and the effect of this choice, so it would take some more investigation to suggest a good solution.
I hope this real-life example explained and clarified the Liskov Substitution Principle a bit more. It can help you to solve and prevent violations of it in your own projects. Next time, I’m covering the Single Responsibility Principle. Until then, happy coding!
This blog was written by Harm Pauw.