2

Out of the 5 SOLID principles, I find LSP to be the most confusing one.

The most popular description of this principle is simply

"A subclass must be replaceable with it's base class without breaking the program".

Solid Principles

So if I have a base class Bird, with the method eat(), and a subclass FlyingBird with the method fly()

And I have an array of flying birds FlyingBird[] birds, if I wanted to call the method eat in each one of them with a loop, I should be able to use instances of Bird rather than just FlyingBird, and the program should not break.

But then, what happens if I want to use the method fly? That method is no longer in Bird, it's exclusive to FlyingBird. According to the above description of LSP, it should be impossible for the program to break if I replace an instance of a subclass with its base class. However, this statement seems super contradictory with inheritance, because then why would you want to have a subclass if it apparently cannot have unique methods?

So I think this could have 2 outcomes:

  1. I'm missing something about the principle and I don't understand it.
  2. That definition of the principle is pretty bad, and people should stop using it.

Which one it is? I have also heard it being described as "Every subclass should still fullfill the contract of the base class", that definition sounds better as it doesn't implies that the subclass can't have new methods, but there's still people who swear by the "Should be to replace the class in code"

So what is going on here?

7
  • 4
    What is going on is you misquoted the principle. A base class must be replaceable by any of its subclasses without breaking the program. But this mix up is also why this form of it isn’t as popular as you make it out to be. – Telastyn Dec 27 '20 at 0:45
  • But the image posted says "subclasses must be replaceable for base types without breaking the program". This implies the one being replaced is the subclass, not the baseclass. This is really how I have seen it everywhere, not just in this image but in several websites and books. And the examples provided are always about those cases where the subclass is the one that gets replaced. – ThePorcius Dec 27 '20 at 1:05
  • 2
    The first part of the image is more clear. “Subclasses should behave nicely when used in place of their base class.” Wikipedia and pretty much all of the answers on this site concur... – Telastyn Dec 27 '20 at 1:39
  • Let's not forget that software is soft, and thus can be changed to remove previously baked in assumptions. Sometimes refactoring is better than trying to future-proof designs from the start. – Erik Eidt Dec 27 '20 at 1:50
  • 1
    The fact that your "most popular description" contains spelling mistakes should be a clue as to its quality … – Jörg W Mittag Dec 27 '20 at 7:55
13

A flawed example of LSP

One thing that I want to touch on but not dig deeply into: the common SOLID examples were not designed to be scrutinized as much as we've ended up scrutinizing them. They are simple examples, and weren't intended to be countered using complex interpretation of possible additional circumstance or requirements.

You can find fault in almost all of them, and I personally do believe that we should use better example, but that doesn't mean that they do still highlight the issue they're trying to highlight.

So I think this could have 2 outcomes:

  1. I'm missing something about the principle and I don't understand it.
  2. That definition of the principle is pretty bad, and people should stop using it.

It's actually both.

In short: you misunderstood an example (and some examples do unfortunately leave things open to misinterpretation), and it caused you to misunderstand the principle it's trying to showcase. The principle isn't flawed, but your understanding of it is (because the example you saw was flawed).


A flawed understanding of LSP

And I have an array of flying birds FlyingBird[] birds, if I wanted to call the method eat in each one of them with a loop, I should be able to use instances of Bird rather than just FlyingBird, and the program should not break.

Correct. Better shown in code:

FlyingBird[] flyingBirds = GetFlyingBirds();

foreach (Bird bird in flyingBirds)
{
    bird.Eat();
}

But then, what happens if I want to use the method fly?

You should not be trying to make Bird objects fly. It makes no sense, since the Bird class does not define a Fly() method. This is LSP in a nutshell.

However, this statement seems super contradictory with inheritance, because then why would you want to have a subclass if it apparently cannot have unique methods?

You're misunderstanding the example. When you discussed it, you started off from the subclass FlyingBird. However, LSP focuses on situation where you start from the superclass (Bird), and how this code must not break when handling FlyingBird objects.

Let's take a real example. Let's say we have another class Caretaker, and they take care of the birds. To maintain their health, birds should eat. Flying birds, however, should be both fed and allowed to fly around a bit. We'll ignore the flying birds until later

Taking care of birds is easy:

public class Caretaker
{
    public void TakeCareOf(Bird bird)
    {
        bird.Eat();
    }
}

Pretty straightforward, right? You would do something like this:

Caretaker caretaker = new CareTaker();
Bird[] birds = GetBirds();

foreach(var bird in birds)
{
    caretaker.TakeCareOf(bird);
}

Let's revisit what you said about LSP:

subclasses must be replaceable for base types without breaking the program

In other words, for our specific example here, we should be able to insert FlyingBird objects into the TakeCareOf method, and the program should keep working.

Well, can we?

Caretaker caretaker = new CareTaker();
FlyingBird[] flyingBirds = GetFlyingBirds();

foreach(var flyingBird in flyingBirds)
{
    caretaker.TakeCareOf(flyingBird);
}

The code won't throw an exception, but we actually have a problem here. The caretaker isn't taking proper care of the flying birds, because he's not letting them fly around.
If you prefer, we can imagine a scenario where an exception would arise, e.g. if flying birds refuse to eat unless they've flown (thus throwing an exception when told to Eat() before being told to Fly()).

The problem remains the same: we are currently not able to insert instances of a subclass (flying birds) into code that should be able to handle the superclass (birds). They are not replaceable, and therefore the LSP is violated.

This is the core of LSP. Any code that claims to handle birds, must be able to correctly handle all birds, including any subclasses of Bird.


However, some well-known LSP examples don't just focus on the core principle, but they are actually tailored to combat a bad way that people try to work around the principle.

You might think that there's a sneaky way to fix the above issue:

public class Caretaker
{
    public void TakeCareOf(Bird bird)
    {
        if(bird is FlyingBird flyingBird)
            flyingBird.Fly();

        bird.Eat();
    }
}

Note: This code example exactly mirrors the ElectricDuck example that you often see used in examples of LSP violations.

It does prevent the exception. The caretaker can now also take care of flying birds. But this is a very bad solution, because you've essentially done away with the reusability of your inheritance.

The main goal of inheritance is to provide reusability. But here, you've actually had to manually develop different code for a new subclass. That's not reusable. Whenever a new kind of bird is developed, your Caretaker class is going to have to be updated. That's very bad design. It's actually both a violation of LSP and OCP.


The answer is long enough as it is, but I wanted to leave you with a good way of solving this:

public class Bird
{
    public void Eat()
    {
        // eating logic
    }

    public virtual void MaintainHealth()
    {
        this.Eat();
    }
}

public class FlyingBird : Bird
{
    public void Fly()
    {
        // flying logic
    }

    public override void MaintainHealth()
    {
        this.Fly();
        this.Eat();
    }
}

public class Caretaker
{
    public void TakeCareOf(Bird bird)
    {
        bird.MaintainHealth();
    }
}

And then the usage:

Bird[] birds = GetAllKindsOfBirds();
Caretaker caretaker = new Caretaker();

foreach(var bird in birds)
{
    caretaker.TakeCareOf(bird);
}

Now, every bird defines its own health maintenance logic, instead of mashing it all together in a single method. And the caretaker can maintain the health of any bird, without needing to know the specific kind of bird he's handling.

I can keep creating new kinds of bird, and I will never have to update the Caretaker logic to account for any new bird type. This is what LSP promotes.

I could add completely new birds, and the above code would keep working.

public class Parrot : FlyingBird
{
    public override void MaintainHealth()
    {
        this.Say($"{this.Name} wants a cracker!");
        this.Give(new Cracker());
        
        base.MaintainHealth();
    }
}


public class DieselBird : Bird
{
    public override void MaintainHealth()
    {
        this.CheckOilLevel();
        this.FillUpFuelTank();
        this.Hull.Polish();
    }
}

public class DeadBird : Bird
{
    public override void MaintainHealth()
    {
        // Nothing needs to be done anymore
    }
}

I admit my examples are getting silly, but I hope you get the point I'm trying to get across.

8
  • This answer is showing how inheritance should be used correctly (hence +1 from me). However, I am under the impression it conflates the LSP with the OCP in some aspects. To my understanding, the LSP is a design principle which applies to subtypes exclusively, whilst the OCP applies to supertypes exclusively. Applying the OCP makes it possible to write code which follows the LSP by making it possible not to break contracts in derived classes, and that is what is correctly demonstrated in this examples. – Doc Brown Dec 27 '20 at 9:11
  • @DocBrown: Note that I only mentioned OCP in the part that addresses bad workarounds for LSP, rather than the part that addresses the core of LSP. However, the ElectricDuck example, which is the most common LSP example you find, actually highlights an OCP-violation (needing to write if baseObject is SubType in the basetype-related code to account for subtypes) which stems from trying to ignore LSP (by trying to hardcode and manually write a forced replaceability of the base type by the subtype). All in all, when dealing with inheritance, OCP and LSP are next door neighbors. – Flater Dec 27 '20 at 10:32
  • @DocBrown "the LSP is a design principle which applies to subtypes exclusively, whilst the OCP applies to supertypes exclusively" Maybe put more tersely: when you don't follow the LSP (and are dealing with inheritance), you are therefore not in the subtypes. So your LSP-avoiding code is in the supertype, where it in turn creates an OCP problem. If you followed LSP, then there would indeed not have been an OCP violation either because you wouldn't be trying to work around LSP. – Flater Dec 27 '20 at 10:33
  • @jmoreno: LSP violations do not require exceptions to be thrown for them to be LSP violations. The problem remains the same regardless of whether the application behaves in an undesirable manner (not letting flying birds fly when maintaining their health) or throws an exception (which is also a form of behaving in an undesirable manner). You're getting caught by the misleading examples I mentioned in the first part of the answer - just because most examples use an exception for easily identifiying an exemplified incorrect behavior, that does not make exceptions a necessity. – Flater Dec 28 '20 at 8:35
  • 1
    @jmoreno - behavior of the program, as used here, is not the particular sequence of events that happens on a specific run, it's the specification of what the program does. E.g., the behavior of "foobar".substring(0, 3) is that it should return "foo". The behavior is not the steps it takes to achieve that - that's an implementation detail. E.g., if there was a bug that caused it to sometimes return "bar", than you wouldn't say it's not a bug if it doesn't happen in a particular run. Same with LSP. The code that's using it relies on the behavior being within what's specified. – Filip Milovanović Dec 29 '20 at 16:47
4

The most popular description of this principle is simply

"A subclass must be replaceable with it's base class without breaking the program".

I have a couple of things to say about this. First, some general advice: correctness is not a popularity contest. Just because this description is “the most popular” does not make it right. It is, in fact, horribly wrong in at least three different ways.

Secondly, that description contains spelling mistakes. That alone should be a huge clue that it is not trustworthy.

I surely hope that you are wrong and this is in fact not “the most popular description”, because it is absolutely horrible, even ignoring the fact that apparently nobody ever noticed that “the most popular description” of the Liskov Substitution Principle isn’t even spelled properly. Thankfully, I haven’t seen this description before, so it doesn’t actually seem that popular.

So, with that out of the way, what is actually wrong with this description, other than the misspelling?

LSP is about types

The Liskov Substitution Principle is about types and subtyping, not about classes. Types and classes, subtyping and subclassing, subtyping and inheritance, are all different things. Do not confuse them.

There are plenty of programming languages that don’t have classes, for example C, Rust, Go, Pascal, Modula-2, and most importantly CLU (Barbara Liskov’s own programming language which she used to research what is now known as the Liskov Substitution Principle). There are plenty of programming languages where classes aren’t types, for example ECMAScript, Python, Ruby, Smalltalk, etc. There are plenty of programming languages where there are more types than just classes, for example Java, C++, C#, TypeScript. There are programming languages where subclassing and subtyping are distinct (e.g. Bard), and so on and so forth.

Frankly, how someone can think the LSP is about classes when Barbara Liskov’s own programming language doesn’t even have classes is beyond me. Liskov herself says the following in her OOPSLA87 keynote address Data Abstraction and Hierarchy:

We are using the words “subtype” and “supertype” here to emphasize that now we are talking about a semantic distinction. By contrast, “subclass” and “superclass” are simply linguistic concepts in programming languages that allow programs to be built in a particular way. They can be used to implement subtypes, but also, as mentioned above, in other ways.

LSP is about instances

The LSP is about replacing instances of a subtype for instances of a supertype.

It’s the wrong way around

The LSP says the exact opposite: I should be able to substitute instances of the subtype for instances of the supertype without changing the observable desirable properties of the program.

The LSP is descriptive, not prescriptive

Barbara Liskov formulated the property as “IFF S and T satisfy the following property, THEN S is a behavioral subtype of T”.

It was the OO community which later took this property and reversed it to say: “When S is a subtype of T, it must satisfy the following property”.

It makes sense to do this, because Barbara Liskov’s notion of behavioral subtyping has some nice properties, but in her own formulation, it is the property that induces the subtyping relationship, not the subtyping relationship that requires the property.

So, what is the LSP?

Well, Barbary Liskov never called it a “principle”, and she also didn’t name it after herself.

As mentioned in the preceding section, she called it a “subtyping property”, and there are two different variants of it, from Data Abstraction and Hierarchy (1987):

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.

And from A Behavioral Notion of Subtyping (1994):

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 <: T.

I don’t know what is “the most popular” definition, but these two are the correct definitions, which in my personal opinion is much more interesting than “the most popular”.

So, in simple terms: if I treat an instance of a subtype as if it were an instance of a supertype, I should not be able to tell the difference.

So if I have a base class Bird, with the method eat(), and a subclass FlyingBird with the method fly()

And I have an array of flying birds FlyingBird[] birds, if I wanted to call the method eat in each one of them with a loop, I should be able to use instances of Bird rather than just FlyingBird, and the program should not break.

This is again the wrong way around.

If you have a program that expects to work with Birds, I should be able to give it FlyingBirds without breaking the program. In your example, this will work, since FlyingBirds can also eat().

But then, what happens if I want to use the method fly? That method is no longer in Bird, it's exclusive to FlyingBird.

But you don’t want to use the method fly(). You don’t even know that method exists. The program expects to work with Birds, it would never attempt to use fly() because it does not know about that method. It only knows about eat().

According to the above description of LSP, it should be impossible for the program to break if I replace an instance of a subclass with its base class.

This is again the wrong way around. The LSP says that you should be able to replace an instance of the supertype with an instance of the subtype.

However, this statement seems super contradictory with inheritance, because then why would you want to have a subclass if it apparently cannot have unique methods?

It is perfectly sensible to have a subtype that does not have more operations than the supertype. It could, for example, be more efficient. For example, an integer set that uses a bit string representation is more efficient than a general set, but it supports the exact same set operations.

But of course a subtype can have additional operations, and you can use those operations through the API of the subtype. You just can’t use them through the API of the supertype, since they don't exist in that API.

So I think this could have 2 outcomes:

  1. I'm missing something about the principle and I don't understand it.
  2. That definition of the principle is pretty bad, and people should stop using it.

Which one it is?

All of the above.

I have also heard it being described as "Every subclass should still fullfill the contract of the base class", that definition sounds better as it doesn't implies that the subclass can't have new methods, but there's still people who swear by the "Should be to replace the class in code"

This is also correct. It is technically not the definition but rather the consequence of the definition.

So what is going on here?

What is going on here is that you have two correct definitions, the one you just quoted and the one in the picture you posted, and one wrong definition that says the exact opposite of the other two definitions, and it turns out that the wrong definition is, in fact, wrong.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.