15

Inspired by this answer:

Liskov Substitution Principle requires that

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.
  • History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Since subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this.

I was hoping if someone would post a class hierarchy that violates these 4 points and how to solve them accordingly.
I'm looking for an elaborate explanation for educational purposes on how to identify each of the 4 points in the hierarchy and the best way to fix it.

Note:
I was hoping to post a code sample for people to work on, but the question itself is about how to identify the faulty hierarchies :)

Songo
  • 6,548
  • 4
  • 48
  • 89
  • There's some other examples of LSP violations in the answers to [this SO question](https://stackoverflow.com/q/20861107/314291) – StuartLC Jan 19 '18 at 06:53

1 Answers1

18

It's a lot more simple than that quote makes it sound, accurate as it is.

When you look at an inheritance hierarchy, imagine a method which receives an object of the base class. Now ask yourself, are there any assumptions that someone editing this method might make which would be invalid for that class.

For example originally seen on Uncle Bob's site (broken link removed):

public class Square : Rectangle
{
    public Square(double width) : base(width, width)
    {
    }

    public override double Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Width;
        }
    }

    public override double Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Height;
        }
    }
}

Seems fair enough, right? I've created a specialist kind of Rectangle called Square, which maintains that Width must equal Height at all times. A square is a rectangle, so it fits with OO principles, doesn't it?

But wait, what if someone now writes this method:

public void Enlarge(Rectangle rect, double factor)
{
    rect.Width *= factor;
    rect.Height *= factor;
}

Not cool. But there's no reason that the author of this method should have known there could be a potential problem.

Every time you derive one class from another, think about the base class and what people might assume about it (such as "it has a Width and a Height and they would both be independent"). Then think "do those assumptions remain valid in my subclass?" If not, rethink your design.

David Arno
  • 38,972
  • 9
  • 88
  • 121
pdr
  • 53,387
  • 14
  • 137
  • 224
  • Very good and subtle example. +1. What you could do, is make Enlarge a method of the Rectangle class and override it in the Square class. – marco-fiset Oct 17 '12 at 12:12
  • @marco-fiset: I would rather see Square and Rectangle decoupled, Square with only one dimension, but each implementing IResizable. It's true that if there was a Draw method, they would be similar, but then I'd rather have them both encapsulate a RectangleDrawer class, which includes the common code. – pdr Oct 17 '12 at 12:31
  • 1
    I don't think that this is a good example. The problem is that a square has no width or height. It just has a length of it's sides. The problem wouldn't be there if width and height were only readable, but they are writable in this case. When introducing modifiable state it is always a lot more difficult to maintain LSP. – SpaceTrucker Oct 17 '12 at 13:41
  • @pdr Thanks for the example, but concerning the 4 conditions I mentioned in my post which part of the `Square` class violates them? – Songo Oct 17 '12 at 19:02
  • 1
    @Songo: It's the History Constraint. Better explained here: http://www.blackwasp.co.uk/LSP.aspx "By their nature, subclasses include all of the methods and properties of their superclasses. They may also add further members. The history constraint says that new or **modified members should not modify the state of an object in a manner that would not be permitted by the base class**. For example, if the base class represents an object with a fixed size, the subclass should not permit this size to be modified." – pdr Oct 17 '12 at 20:55
  • @Songo: I think. Or maybe it's postconditions. The point is that it's all variations on a simple theme and trying to figure out which rule you're breaking is futile. Just think about the assumptions a method makes when dealing with a superclass and see if they hold true in your subclass. – pdr Oct 17 '12 at 20:58
  • @pdr Does that mean that a child class can't have his own public methods that aren't declared in the parent class? – Songo Oct 17 '12 at 22:03
  • @Songo: No, it means that new methods (or overrides) cannot operate on the state of the object in a way that would be a surprise to the author of a method that deals only with the parent class. – pdr Oct 17 '12 at 22:12
  • +1. This is [almost exactly the same](http://programmers.stackexchange.com/questions/170222/what-can-go-wrong-if-liskov-substitution-principle-is-not-followed-while-coding/170384#170384) answer as the one I made to another LSP question yesterday. – Kaz Dragon Oct 19 '12 at 07:33
  • @Songo: By my understanding of a "history" constraint, that would be violated if `Square` had been the base class with `Height` and `Width` (perhaps both derived from an abstract `ReadableShape`) that were tied together, and `Rectangle` was a derived class which allowed the creation of an instance where their values differed. Otherwise, I think `Square` simply violates the contract which says that setting `Height` should not affect width nor vice versa. Note that `ImmutableSquare` could inherit from `ImmutableRectangle` even if the latter contained a `WithHeight` method that would return... – supercat Nov 17 '14 at 22:47
  • ...a shape with the specified height, and `WithWidth` that would return a shape with the specified width, if invoking those methods upon an `ImmutableSquare` could return an instance of `ImmutableRectangle` [it would also be possible that calling `WidthHeight` on an `ImmutableRectangle` whose width matched the passed-in value could return an instance of `ImmutableSquare`]. – supercat Nov 17 '14 at 22:48
  • If I double the width, I assume the area is doubled. The idea to make square a subclass of rectangle is just a bad idea from the start. – gnasher729 Oct 15 '21 at 06:57