Question

I am learning a lot about this principle (also thanks to two answers I received here) and would like to elaborate on another point that somebody mentioned.

1) Is the following a violation of LSP?

class Base
{
   public virtual void UpdateUI()
   { 
     //documented: immediately redraws UI completely
   }  
}

class Component: Base
{
  public override UpdateUI
  {
    if (Time.Seconds % 10 ==0)  //updates only every ten seconds
    {
      //drawing logic
    }
  }
}

In my understanding, the code description in the base class represents the contract, expected behavior, that is violated in the subtype.

2) Why breaking of behavior does not matter for weakening precondition?

class Human
{
  public virtual void DoSomething(int age)
  {
     //precondition - age < 100
  {

}

class Cyborg : Human
{
  public virtual void DoSomething(int age)
  {
     //precondition - age < 200
  {
}

The Cyborg class weakened the precondition, which is allowed. For valid arguments, substitution works well. Whenever I have a Human object, a Cyborg object can be used. But what if I have a test like this: Human(110) - must fail, argument needs to be < 100

When I substitute with Cyborg, the test will pass. I.e. the behaviour changed. Why is that allowed?

Was it helpful?

Solution

To the very interesting answer of CandleOrange, I'd like to add that:

  • Preconditons cannot be strengthened means what can be done with the supertype can be done with the subtype
  • Postconditions cannot be weakened and invarients must be preserved means what is guaranteed by the supertype is guaranteed by the subtype

I disagree however with his conclusions for case 1.

Case 2: it's ok !

The following code works for the supertype. If your code is LSP compliant, everything that works for the supertype works for the subtype. So you're fine:

 // suppose x is a Human or a Cyborg
 x.DoSomething (50);  // if it works for a Human, it should work for a cyborg
     

Attention: If the preconditions of the supertype are not met, you can't say anything about whether it should work or not for the subtype. This is not said by LSP but is just a consequence of the implication relationship (i.e. from p implies q, you can deduce that not q implies not p, but you can't deduce anything from not p):

// suppose x is a Human or a Cyborg
x.DoSomething (150);   // Id doesn't even work for a Human,
                       // so we don't care if it works for a Cyborg

Case 2: could you be confused ?

Now the things would be completely different, if you would expect some exception or a failure to happen for humans beyond a certain age. But then you're no longer thinking of preconditions but about postconditions. And the subtype is not allowed to weaken it:

class Human
{
   public virtual void DoSomething(int age)
   {
      // post condition:  exception thrown if age >= 100
   {
  }

class Cyborg : Human
{
  public virtual void DoSomething(int age)
  {
      // post condition:  exception thrown if age >= 200  (!!! INVALID IN LSP)
  {
}

Case 1: it's not ok, or is it ?

Case 1 is interesting, because from the point of view of the code, everything seems LSP compliant, and many humans are tolerant to a slower refresh, so it doesn't seem to matter very much.

But from the contract point of view it is not ok at all: the immediate redraw is a postcondition and the subtype is not allowed to weaken it! So this is not LSP compliant.

To illustrate the problem, imagine that Base and Component are part of a surgical robot, and that Base would be a robotic arm, and Component an arm equipped with a scalpel. If the guarantee given to the surgeon is that the UI with the camera picture gets a real-time update of every move, Component could result in the death of a patient. So you'd better consider postconditions seriously !

OTHER TIPS

I'll answer your questions in reverse order. From 2)

When I substitute with Cyborg, the test will pass. I.e. the behaviour changed. Why is that allowed?

Because the substitution principle does not demand that behaviour be unchanged.

Liskov substitution has four behavioral conditions

  • 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). Because 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. It was the novel element introduced by Liskov and Wing. 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. Fields added to the subtype may however be safely modified because they are not observable through the supertype methods. Thus, one can derive a circle with fixed center but mutable radius from immutable point without violating LSP.

Wikipedia: Liskov substitution principle

The goal of those four behavioral conditions is that the client code which uses these objects shouldn't need to change because of the substitution. The substituted object must be just as, if not more, tolerant of what the client code expects, not less. That doesn't mean that the substituted object can't do whatever it pleases. It just can't force client code to deal with new problems. The client code needs to be able to not know or care about the substitution.

Which is why I'd also say that 1) is not a violation. There is nothing new here that client code would have to care about.

Liskov applies to plain inheritance, not to polymorphism. Liskov states that properties and methods that exist in a base class should be available still in any descendants.

With polymorphism (your virtual or abstract method), behavior will and should divert. That is the point of it. Logically though there is no issue: the method fulfills the same purpose in a different context. Any test should verify if the screen was redrawn appropriately, not whether the actions performed are exactly the same in both cases.

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