4

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?

  • What's being shown for case 1 only updates not every 10 seconds (as the comment suggests), but only when called at a time for which seconds are a multiple of 10. That means that many times when called it will do nothing, not even schedule an update. However, generally speaking this is getting into the area of non-functional requirements, i.e. performance expectations/guarantees, which may or may not be part of LSP depending on how the guarantees are defined. – Erik Eidt Jan 30 '18 at 0:48
3

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 the sheer consequence of the implication relationship (i.e. from p implies q, you can deduce that non q implies p, but you can't deduce anything from non 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 fo 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 !

  • I too think case 1 is not lsp compliant, because the cintract states that the method immediately updates ui. Snd for the subtype, it is not the case, i would say the precondition is stronger (this modulo condition). Or what is the precondition here? Agree that postcondition is updated ui – John V Jan 20 '18 at 13:00
  • For case 2: I know that, but is it ok? When I replace failing Human.DoSomething(150) with Cyborg..., it will work and failing tests change to passing. As Liskov says wherever is the base class, subtype can be used without changing behavior or introducing bugs. – John V Jan 20 '18 at 13:07
  • Also I am not sure that age>200 is a postcondition, which is something that holds true after the method finishes. But here int is passed as an argument – John V Jan 20 '18 at 13:20
  • @user970696 in fact, the precondition is what is assumed to be true when the function is called. So you should not call DoSomething(150) in view of the precondition; not even in a test case. If you call it in a test case with such a value, the test is not relevant. If you expect the test to fail, this means that in reality you have no precondition (i.e. the fonction can be invoked with any value) and you expect some post-condition (i.e. that unexpected values trigger some errors). Problem:Doing defensive programming checking for preconditions and testing it modifies the contract. – Christophe Jan 20 '18 at 13:23
  • @user970696 indeed,age>200 is not a postcondition. The postcondition is that if age>200 there is an error triggered. – Christophe Jan 20 '18 at 13:24
3

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.

  • 1
    Isn't the instant update a post-condition of updateUI() ? – Christophe Jan 20 '18 at 12:44
  • 1
    Well Liskov does say that behavior must remain the same, not changing or introducing bugs when supertpype is switched for subtype. The paper explicitly mentioned that behavior is what matters. Also, the contract is broken case 1 - while base class always provides update, subtype does not, which confuse its consumers – John V Jan 20 '18 at 12:57
  • @user970696 behavior is what matters. But if behavior can't change at all there can be no substitution at all. – candied_orange Jan 20 '18 at 19:12
  • @CandiedOrange No, the thing is you can change the implementation as you wish, but must follow the contract. If the base class method guarantees something (eg. sets some value, etc.), the derivatives must result in the same thing. If not, you are violating LSP. Good example of why LSP is violated in my Case 1 presented Christophe with his surgical robot. If I mentioned that redrawn screen is a postcondition of that method, then it would be obvious that LSP is violated. – John V Jan 21 '18 at 8:18
  • @user970696 does system.out.print("") violate its contract? Semantically we were promised it would print but it doesn't. Is this a contract violation? No, we were promised it would print whatever was passed. Not that it would actually be anything. Similarly case 1 does not violate because what it is choosing to do is sometimes update the UI with nothing new at all. Assuming that post condition doesn't weaken the system in anyway then that doesn't weaken the contract. The substituted class has the responsibility to decide what the UI is updated with. – candied_orange Jan 22 '18 at 23:24
-3

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.

  • Liskov explicitly states several principles (must not strengthen preconditions, must not weaken postconditions...), as well as the need for the subtypes to follow the contract of the base class. The behavior must be consistent, otherwise you have the famous rectangle - square problem. The base class must be always replacable with its derivative with no change in behavior, this is the rephrased and often quoted part. – John V Jan 20 '18 at 14:57
  • The point is - if a method returns e.g. always true or sets an attribute in the base class, in the subtypes it must be the same (result, not implementation). This is the contract.. If not, LSP is violated. See here: softwareengineering.stackexchange.com/questions/170138/… – John V Jan 20 '18 at 15:04

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.