Liskov Substitution Principle – Getting Solid with SOLID – Part 3

Software engineering principles and patterns help us craft good clean software. One such pattern is an acronym we know as SOLID. “L” represents the Liskov Substitution Principle (LSP) which was coined by Barbara Liskov in 1987. Today I focus on Liskov Substitution Principle and how we apply it in modern application development.

A lot has been said about SOLID design principles over the years. Sometimes easy, sometimes hard. Today’s post is third in a series where I attempt to boil SOLID design principles down to layman’s terms. You may find the first parts here: Single Responsibility Principle and Open/closed principle.

What is the Liskov Substitution Principle?

Barbara Liskov is an America computer scientist and professor. She was one of the first women to receive her doctorate’s in CS within the United States, a Turing Award winner, and a professor at MIT. In other words, she’s crazy smart.

Barbara initially introduced the Liskov Substitution Principle at a conference in 1987 during her Data abstraction and hierarchy keynote address.

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

Barbara Liskov – 1987 – Data abstraction and hierarchy

If you’re like me you probably had to read Barbara Liskov’s statement a few times to understand it. If not, well that’s cool too. Robert C. Martin makes it a bit more clear.

LSP: “Derived classes must be substitutable for their base class.”

Robert C. Martin – The Principles of OOD

That seems pretty clear right? I should be able to substitute a derived class in place of a base class and the program should still function the same. Most noteworthy our favorite uncle also said “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”

Can we please expound on that?

Is it just me or do the simple definitions sometimes add more confusion than they clear up? Just me? Ok. Well then, for my own benefit I’ll just boil it out as best I can.

If I have a class that derives from a base class, that class must minimally have the same public members as the base class. Hence those members must have the same signatures (input/output) and they must have the same behavior when calling them.

Beyond the requirement for signature parity, the subtype must also meet behavioral conditions (see Wikipedia article). Briefly to summarize: preconditions cannot be strengthened and postconditions cannot be weakened. Put in other words, validation (preconditions) can be less stringent on a subtype and assertions (postconditions, typically via unit tests) can be more stringent.

Another requirement for LSP is known as the “history rule”. Personally I have difficulty understanding this one based on the Wikipedia definition: “objects are regarded as being modifiable only through their methods (encapsulation)…” When put in context of the fragile base class problem, however, it makes more sense to me. The premise is that seemingly innocuous changes in a base class internals could have unintended ramifications on sub types. That’s bad.

How to violate the Liskov Substitution Principle

It’s pretty common for examples and tutorials to talk about what something is. It is less common for things to talk about what it isn’t. As a result I decided I’d use a few examples to show how easy it is to violate LSP.

Problems with inheritance

Consider your basic object oriented programming CS courses. They teach you about inheritance and composition. You know, “is a” and “has a”. Your first basic examples are about a Vehicle and the common fields and methods on a Vehicle. They go on to show some differences. Maybe you have a Truck sub class. Perhaps you have a SportsCar sub class. Inevitably somebody throws out the idea that an Aircraft is a vehicle, same with Boat and Bicycle.

While to an extent these all have something in common–in a lot of cases, it’s a stretch– you start to see they have a lot more differences than commonalities. This particular problem is known as the Circle-ellipse problem. Generally speaking, the problem is that we quickly see that methods and properties on the base class don’t quite jive with a more specific sub class.

In order to make your inheritance model work you may be tempted to have several unimplemented methods on your sub types. This clearly breaks LSP since anywhere that was previously using the base type could now throw an exception instead when replaced with the sub type.

Another example with inheritance

Stackoverflow — hallowed be it’s name — is a font of so much information. In the above scenario laid out, we can quickly see how inheritance can get out of hand and violate LSP. Here is a great example using an IVehicle interface and several different ways we might violate LSP implementing that interface.

I wanted to take special note to point out this “Glaring Violation – Runtime type switching” from the above Stackoverflow Q/A:

void MethodWhichViolatesLSP(IVehicle aVehicle)
{
   if (aVehicle is Car)
   {
      var car = aVehicle as Car;
      // Do something special for car - this method is not on the IVehicle interface
      car.ChangeGear();
    }
    // etc.
 }

If you’ll recall in our definition section I quoted Uncle Bob where he stated that “… classes must be able to use objects of derived classes without knowing it.” This code clearly knows about it. I don’t know about you but I have specifically violated LSP in this exact way several times throughout my career. Here, slap my wrist for me.

Strengthening the precondition

I personally find it easier to understand things by seeing what not to do. Stackoverflow once again has a great example. Here a user asks whether or not some code violates LSP. There are two answers that greatly exemplify why the code in question does indeed violate LSP.

The premise of this question centers around inheriting from their own class Task (not to be confused with System.Threading.Tasks.Task). In this particular use-case the violation is due to the code breaking the rule by strengthening the precondition. The accepted answer also provides an example how to fix it.

Recommendations

SOLID design is all about simplification. With that in mind, here are a few recommendations to help your code follow LSP:

Conclusion

While the original wording for LSP could make it seem a bit difficult to understand, in practicality it is straightforward. The basic premise is to build against abstractions and that implementations of your abstractions should not break or change signatures, return types, exceptions, etc., of those abstractions.

Additional Reading

If you really want to get into the meat of LSP, I invite you to read Robert C. Martin’s article (alternate link here). His article contains several examples of LSP violations. See also his blog article The Principles of OOD.

Here are a few other articles on LSP that I found useful:

Photo Credits

Photo by Gavin Allanwood on Unsplash