6

I'm aware that there are 4 ways to violate Liskov Substitution from here.

But I'm not sure what these violations would like in practice. Can you show examples of code that breaks the principle for each of the 4 ways?

  • 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.
1
  • I think you pasted the wrong link
    – Alexander
    Commented Feb 2, 2022 at 1:34

1 Answer 1

13

Preconditions cannot be strengthened in a subtype.

The preconditions are assumptions we are making on the state/parameters at the time a method is invoked. Here's a simple example using a setter.

class Super {
    private int myInt;

    // Precondition: newValue can be anything.
    public void setMyInt(int newValue) {
        myInt = newValue;
    }
}

class Sub extends Super {

    // Stronger precondition: newValue must be > 0.
    @Override
    public void setMyInt(int newValue) {
        if (newValue <= 0) throw new RuntimeException("Must be positive");
        super.setMyInt(newValue);
    }
}

This breaks Liskov substitution because we can't guarantee anymore that superInstance.setMyValue(-3); won't throw an exception (i.e. if superInstance is of actual type Sub).

Edit per Christophe's comment: this is only a violation of LSP if the contract of super.setMyValue(someNum) is broken in a derived class. The contract is what it sounds like: an agreement between the caller and the implementer about how a method will behave. In our example, if the contract allows for setMyValue(int newValue) to throw exceptions, then there's no LSP violation even if, coincidentally, the base method does not throw, while the overridden method does.

Some languages allow us to specify the possible exceptions that can be thrown in the method declaration. For others, you may just have to read the docs to find out whether a method is allowed to throw exceptions, and which kind.

Postconditions cannot be weakened in a subtype.

The postconditions are assumptions we are making on the state/return value after a method is invoked. Here's another example using a getter.

class Super {
    private String myString;

    // Postcondition: myString is always nonull.
    public String getMyString() {
        return myString == null ? "" : myString;
    }
}

class Sub extends Super {

    private String myBetterString;

    // Weaker postcondition: myString can be null.
    @Override
    public String getMyString() {
        return myBetterString;
    }
}

Invariants of the supertype must be preserved in a subtype.

Invariants are certain assumptions that we are allowed to make about the state.

class Super {
    // Invariant: numInstances is an increasing sequence. 
    protected static int numInstances;
    
    public static int getNumInstances() {
        return numInstances;
    }

    public Super() {
        numInstances++;
    }
}

class Sub extends Super {
    // Invariant violated: creating a new Sub decrements the sequence.
    public Sub() {
        numInstances--;
    }
}

This breaks Liskov substitution because we can't guarantee anymore that Super.getNumInstances(); is always increasing (or even always positive) once we start creating Sub instances. Yeah, I know we could fix this by making numInstances private instead. A real invariant might be more subtle.

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 hadn't heard of the history constraint before. As I come to understand it, the basic idea is that derived methods shouldn't change the state of the superclass in a way that it couldn't have been changed with just the superclass' public methods. This page gives a good example of a history constraint violation:

A violation of this constraint can be exemplified by defining a mutable point as a subtype of an immutable point. This is a violation of the history constraint, because in the history of the immutable point, the state is always the same after creation, so it cannot include the history of a mutable point in general.

2
  • 3
    Nice answer. Excellent job about the often-forgotten history rule. On your very first example however, it is important to remind that LSP is about contracts and not about code. If the super class’ contract would allow for exceptions in case of unsuitable value (i.e precondition is that input can have any value, but postcondition might be exception in some cases) your code’d still comply with LSP, since the postcond would then be strengthened (exception for negative values) while the precondition (any value) would be unchanged.
    – Christophe
    Commented Feb 2, 2022 at 8:42
  • 1
    @Christophe great point, I did fail to specify that it's about the contract. I'll edit to make this more clear. Commented Feb 2, 2022 at 17:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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