Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
scroll top