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?

Christophe
  • 74,672
  • 10
  • 115
  • 187
John V
  • 4,898
  • 10
  • 47
  • 73
  • 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 00:48
  • I find it bizarre that none of the questions on softwareengineering.stackexchange.com concerning programming by contract actually use a programming language that supports [pre- and postconditions, such as Ada](https://blog.adacore.com/the-case-for-contracts). There are no pre- or postconditions in the code above, so technically anything is valid. – TamaMcGlinn Aug 26 '20 at 12:24
  • @TamaMcGlinn The thing is, these pre and post-conditions might be implicit and expected in terms of behaviour, which Liskov originally described in her paper. – John V Aug 27 '20 at 10:00
  • But your test, using Human(110), implies a postcondition that some exception is thrown. That postcondition was never in the definition of Human; the client using Human has figured out something about the internals of the function, and has decided this is a postcondition. Without explicit pre- and postconditions in the specification of Human, we can't say the client is wrong about this; there is no way of telling what they are intended to be, and hence, no satisfactory answer to your questions. – TamaMcGlinn Aug 27 '20 at 13:02

3 Answers3

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.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 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 08: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
  • @CandiedOrange Good point, but as Christophe explained, this does violate LSP. The update is likely internally setting something (like UpdateTime) and in the derived class, this does not happen because the method is not called at all. In your case with print, I agree, that would be no violation, because you are still calling the method. – John V Jan 23 '18 at 08:14
  • @user970696 So calling the method, not it's behavior, is what matters? `UpdateUI()` would follow LSP if it copied every setting in the UI and overwrote the UI with that copy? – candied_orange Jan 23 '18 at 08:52
  • @CandiedOrange The behaviour is specified by the contract. LSP states that postcondition cannot be weakened (the child must do at least as much as its parent), so if the base class expects UI to be updated (likely there is a value somewhere set), base classes have to follow. Otherwise, you cannot substitute the base class for its derivative without inducing changes in behavior (like the surgical robot example), which is violation of the main LSP principle. – John V Jan 23 '18 at 09:18
  • @user970696 the contract is to take responsibility for updating the UI. You're saying the UI must be updated from exactly the same source. I fail to see how the source of the update became part of the contract. – candied_orange Jan 23 '18 at 09:25
  • @CandiedOrange No, I am not saying that. The problem in my Case 1 is, that the derived class overrides the method and now is not providing the update immediately, as the contract in the base class states. Try to switch those two types in an app and you will see. And Liskov says, this cannot happen. – John V Jan 23 '18 at 09:27
  • @user970696 yes you are. Because if the derived class is free to update from any source then a source that produces no change to the UI is valid. It is producing no change at all, immediately. – candied_orange Jan 23 '18 at 09:30
  • @CandiedOrange The derived class just have to make sure that the immediate update to the UI is done. As long as it sets the same postconditions, it would be valid, yes. Now I see what you mean. But in the example I just made the call conditional, i.e. it would behave differently than in the base class. – John V Jan 23 '18 at 09:37
  • Let us [continue this discussion in chat](http://chat.stackexchange.com/rooms/72094/discussion-between-candiedorange-and-user970696). – candied_orange Jan 23 '18 at 09:48
  • @CandiedOrange Unaccessible for me (workplace firewall). Anyway I understand what you meant and would agree that if you a similar method doing nothing but setting the postconditions (basically mocking) the Update, it would be fine. But in my example, just assume the call is made in the base class and sometimes made in the derivative. – John V Jan 23 '18 at 09:53
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 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 !

Glorfindel
  • 3,137
  • 6
  • 25
  • 33
Christophe
  • 74,672
  • 10
  • 115
  • 187
  • 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
  • Well but that is still checking the precondition and throwing an exception if the argument is not in the range, it does not concern the status which is left after the method finishes. See here (mid-page): https://www.codeproject.com/Articles/613304/SOLID-Principles-The-Liskov-Principle-What-Why-and – John V Jan 20 '18 at 15:00
  • This is an interesting article. In the middle of the code, in the calculator the author throws an exception if preconditions are not met. This is perfectly valid, because the LSP is about what happens assuming that the preconditions are true. If they are not, you can do whatever you want, because you're not in the scope of the contract. The problem I'm referring to, is when your test suite verifies if the exception is thrown when the preconditions are not met, because the test case then verifies something that is not in the contract. The contract only applies if preconditions are met. – Christophe Jan 20 '18 at 15:17
  • Right. So it is alright that the subtype, replacing supertype, accepts values that the supertype did not (in our Human example, age >=100 and < 200). – John V Jan 20 '18 at 15:31
  • Yes, perfectly exact ! – Christophe Jan 20 '18 at 15:33
  • You should change the postcondition from "error thrown if age >= xxx" to "error thrown if age is so large that DoSomething cannot do what it is supposed to do". So Cyborg and Human will have the same postcondition. – gnasher729 Jan 20 '18 at 15:37
-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.

Martin Maat
  • 18,218
  • 3
  • 30
  • 57
  • 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: https://softwareengineering.stackexchange.com/questions/170138/is-this-a-violation-of-the-liskov-substitution-principle/170142 – John V Jan 20 '18 at 15:04