3

I have read two opinions on the subject. Let's assume the following simple code:

class Enemy
{
    public virtual void CheckHealth()
    {
        if (Life <= 0)
            Dispose();
    }
}

class Boss : Enemy
{
    public override void CheckHealth()
    {
        if (Life <= 0 && PowerSourceDestroyed)
            Dispose();
    }
}
  1. From a first look, the LSP is obviously violated as the behavior of the child is not the same as its parent's (if the base class is replaced by the child, it might not work due to the strengthened precondition which is explicitly forbidden by LSP). However, many think that actually to decide whether LSP is violated or not, the contract must be known because the contract might state: "Once the life is <= 0, the system attempts to destroy the enemy". In that case, the LSP would not be violated as the behavior of the child complies.
  2. But I am thinking, do I really need the contract? In the end, LSP is formulated as follows: Let O(x) be a property provable about objects x of type T. Then O(y) should be true for objects y of type S where is a subtype of T. I mean, if something is supposed to be provable then I think only the actual representation matters, which in this case seems like the LSP is violated - the property of the parent is not valid for the child because the child imposed an additional condition.
Deduplicator
  • 8,591
  • 5
  • 31
  • 50
Ezoela Vacca
  • 881
  • 1
  • 6
  • 12
  • 1
    you need to add methods to those classes, then i think you will find the LSP is not violated – Ewan Jul 26 '18 at 08:19
  • @Ewan: what do you mean? If this code is in the methods (I will update immediately) then LSP is violated - the behavior guaranteed by the parent is not guaranteed anymore (destruction of the object upon the method is called). And the thing is, if the contract stated "upon the CheckHealth, the game attempts to destroy the enemy with life <=0", it might comply, but is this truly so? It certainly violates LSP if the contract says "upon the call of CheckHealth, the object is destroyed"when Life<=0). – Ezoela Vacca Jul 26 '18 at 08:34
  • also class Enemy : Boss should be Boss : Enemy ? – Ewan Jul 26 '18 at 09:00

5 Answers5

8

Update

Part of the issue here is that you misunderstand the life<0 as a precondition. RubberDuck explained it better than I could:

@EzoelaVacca I believe you may be misunderstanding the meaning of “precondition”. A precondition is not the internal check you’ve shown in your example. A precondition is what must be true prior to calling a method in order to avoid exceptions/undefined behavior. In other words, a precondition is the state of the system prior to calling a method. It’s that state of the system that can not be made stricter.

Update You dismiss the explanation without actually understanding it:

I do understand that, I included the check so that it is visible what precondition is expected. It does not change the fact that the precondition is stronger in the derived class.

Your continuation proves that you do not understand what RubberDuck meant. The check is not a precondition.

Your example does not have a different precondition between Boss and Enemy. The calling code should not have any expectation of the life value, nor whether it passes the <0 check before calling the method.

The entire point of the method is so that the method can be called without needing to pre-check. The calling code is able to call the method without needing to care about pre-checking if the enemy is dead.
Because if that wasn't the case, then your method should've been a Dispose() without a built-in check.


This answer is partly based on your comment to the other answer:

No, the provable property is that when you call the method CheckHealth with life<=0, the function Destroy() is called. This is not true for the child. It is clearly violating the requirement of not strengthened pre-conditions.

You're getting caught up on the actual check (life<=0), not the intention of the method.

First of all, the method is wrongly named. It does not "check health", it tells the enemy to "dispose yourself when you're dead".

Checking the health is indirectly part of it, as it is part of what defines "death" for an enemy. However, as your code shows, the definition for a boss death is different: they need to both have their health dropped to zero and their power source destroyed.

Both for the enemy and the boss, the "dispose yourself when you're dead" contract is the same. The contract merely stipulated a conditional dispose. It does not stipulate the exact condition (what is death?), nor what a dispose actually entails (e.g. a boss may have more resources to dispose of when it dies - which does not violate LSP in any way)

To make this clear, I could rewrite your example to:

class Enemy
{
     public sealed void DisposeIfDead()
     {
         if (this.IsDead)
           Dispose();
     }

     protected virtual bool IsDead => Life <= 0;
}

class Boss: Enemy
{   
     protected override bool IsDead => Life <= 0 && PowerSourceDestroyed;
}

This proves the point that the DisposeIfDead() does not behave differently. It simply relies on a different definition of "death". Omitting other differences, a Boss is different from an Enemy specifically because things about them are different, including death conditions.


The LSP focuses on derived classes who behave unexpected from the point of view of a third party.

Let's say I have a method that triggers enemy behaviors. Note that I changed your code example slightly to make the example clearer.

public void HandleEnemyBehavior(Enemy enemy)
{
    bool isDead = enemy.DisposeIfDead();

    if(!isDead)
    {
        enemy.MakeMove();
        enemy.AttackRandomTarget();
    }
}

Let's look back at what you said:

No, the provable property is that when you call the method CheckHealth with life<=0, the function Destroy() is called. This is not true for the child.

Notice how the third party (HandleEnemyBehavior()) does not care how death is defined. It only care that the method gets called, and in this case it cares about knowing if a death occurred.

HandleEnemyBehavior() does not have any expectation as to what defines "death" for an enemy. Therefore, Boss does not break the Enemy contract. As far as the HandleEnemyBehavior() method is concerned, every enemy that can be passed to it (Boss/Enemy) behaves the same way. It does not violate LSP.

I'll give an example of a third enemy type that does violate LSP:

public class Ghost : Enemy
{
    protected override bool IsDead => throw new Exception("Ghosts can't die!");
}

The problem is a bit simplistic (exceptions are clearly not a good idea) but it's a clear example.

This violates LSP, because HandleEnemyBehavior() is now unable to handle any enemy (Boss/Enemy/Ghost) in the exact same way. The only way to get the method to work is:

public void HandleEnemyBehavior(Enemy enemy)
{
    bool isDead = false;

    if( !(enemy is Ghost) )
         isDead = enemy.DisposeIfDead();

    if(!isDead)
    {
        enemy.MakeMove();
        enemy.AttackRandomTarget();
    }
}

And this is the issue that violates LSP. HandleEnemyBehavior() needs to handle ghosts differently from bosses/enemies. Implicitly, HandleEnemyBehavior() needs to be aware of the immortality of the ghost, which violates the idea of encapsulating the definition of death inside the Enemy class (and its subtypes).

Somewhat obviously, the correct implementation of Ghost would be protected override bool IsDead => false;, which does not violate LSP. The exact definitions of death aren't really the focus of the answer, it's more a matter of observing the external behavior of a class and its subtypes.


Your expectation of the violation of the LSP suggests that you're thinking of a scenario like this:

public void KillEnemy(Enemy enemy)
{
    enemy.Life = 0;
    enemy.DisposeIfDead();

    //The enemy is now dead.
}    

This would work for an Enemy but not for a Boss (if their power source is still active).

However, you've now violated SRP and encapsulation. The KillEnemy() method has an expectation of the definition of death. That defeats the purpose of encapsulating the definition of death inside the Enemy class.

The issue is in the intention of DisposeIfDead(). It implies that the caller (KillEnemy()) tasks the object (Enemy or a subtype) with deciding if it's dead or not.

SRP comes into play here. The method implies that the object defines death criteria. Therefore, if KillEnemy() were to also contain death criteria, you've written the same thing twice. Both the object and the KillEnemy() method each have their own definition of death, which may or may not be the same. That is a clear violation of SRP.


No, the provable property is that when you call the method CheckHealth with life<=0, the function Destroy() is called.

This is the internal logic of the enemy class. Using encapsulation, outside agents are unaware of the inside logic of the enemy class.
LSP is violated when the external handling of the class needs to differentiate between subtypes of the class.

LSP ensures that when a third party handles a type of Enemy, that it does not matter if the real object is an Enemy or any of its subtypes. The third party does not need to care about the subtype of the object.


In response to one of your comments:

An example from a book: Base class requires an INT to be < 100. The child requires it to be < 50. LSP is violated.

That is an overly simplistic example. The behavior encountered when you pass 75 to the child class very much influences whether LSP was violated.

If it throws an immediate validation exception, you are correct that it violates LSP.

However, if it merely behaves differently (e.g. returns a different value) but does not misbehave (exception, nonsensical information, data corruption), then LSP is not violated.

"Stricter" is a really bad metric to test for violation. Because while you can argue that the boss has "stricter criteria for death", I could argue that the boss has "simpler criteria for staying alive".
The only difference is in the name of the variables and methods, there is no technical distinction between the two.


Flater
  • 44,596
  • 8
  • 88
  • 122
  • not a bad answer, if a bit long winded. but i think you miss the important factor of Dispose() having external consequences. Maybe add a section? – Ewan Jul 26 '18 at 10:45
  • 1
    @Ewan: I'm not saying that there are no issues with external consequences to disposing objects, but these issues do not matter for the question at hand. The method name could've been `FooMyBar()` and the topic wouldn't have been impacted in any way. – Flater Jul 26 '18 at 10:48
  • hmm no, its very important that its Dispose and not FooMyBar. I suspected that you missed this – Ewan Jul 26 '18 at 10:49
  • actually i do mention it, maybe you missed it – Ewan Jul 26 '18 at 10:56
  • Actually I am not sure if you are correct because this way you could always argue that strengthening the condition is actually just changing the existing condition..and yet this is what LSP forbids. How does my example change from the textbook ones, such as: base class method accepts a parameter of type "int", child class "short" - LSP violated. But you could again argue that that we merely reduced the group of inputs. Main point is, LSP tells you not to make the condition stronger in subtypes, which I believe is my case. – Ezoela Vacca Jul 26 '18 at 13:06
  • Anyway, as one of the answers suggest, I went through the original paper again and it does mention that you cannot decide on LSP violation based on the module itself, you need the contract specification about its behavior. – Ezoela Vacca Jul 26 '18 at 13:10
  • @EzoelaVacca: There is a massive difference between changing the input parameter's type from `int` to `short` (which does violate LSP) and simply doing something different based on a given valid input parameter (which does not violate LSP). For example, a normal enemy gets stunned when they receive damage > 15, whereas a boss only gets stunned when receiving damage > 30. That is not a violation of LSP. A violation of LSP would be `Boss : Enemy` implementing `void Stun() { throw new Exception("Bosses cannot be stunned!");` as that does break the "enemy can be stunned" contract. – Flater Jul 26 '18 at 13:15
  • @Flater I do appreciate the discussion. The thing is, the client code expects that if life=<0, the enemy is destroyed. But that is not going to happen with the boss as the condition is more strict in that sense. This way, the child will not behave the way it parent would, if replaced. – Ezoela Vacca Jul 26 '18 at 13:18
  • @EzoelaVacca: `The thing is, the client code expects that if life=<0, the enemy is destroyed.` The issue here is the expectation. You can't at the same time give `Enemy` the responsibility to decide death and then have client code that independently decides whether death should have been applied or not. ("Do this if it needs to be done."_"It didn't need to be done."_ "Well it should have!") The sole exception to this being unit tests, which do inherently contain fact-checking logic. – Flater Jul 26 '18 at 13:20
  • @Flater I do not mean the client code to decide anything, but if the contract says "When life<=0, enemy is destroyed" then the client code rightfully could expect the same from the Boss, right? – Ezoela Vacca Jul 26 '18 at 13:22
  • But as you implied, the expectation is the thing and the paper does mention that. I believe if the contract specifically mentioned the condition (life value) then this would be breaking it. – Ezoela Vacca Jul 26 '18 at 13:23
  • @EzoelaVacca: The contract should not specify values. It specifies behavior. "When life<=0" should not be part of the contract (at least not directly). "Dispose the enemy when it is dead" is the contract. The definition of dead is something that can vary based on subtype, but it will always be a binary result (is dead/is alive). The binary nature of death can be part of the contract (and most likely will be). – Flater Jul 26 '18 at 13:23
  • @Flater I beg to differ, contracts in most C++ books dealing with LSP always include value, especially when dealing with input parameters (like I>0 && I<10). How else would you enforce parameter value requirement? – Ezoela Vacca Jul 26 '18 at 13:24
  • @EzoelaVacca Examples are often oversimplified and not pedantically accurate as to real world applicability. The 0-10 value range is a specific value validation, it's more than just a changed behavior. For example, let's say damage is capped 0-100. But a boss does not take damage<5 because of armor. Therefore, when you enter 0-4 damage, the boss' life does not decrease. This is not a violation of LSP. It would be a violation of LSP if you made it impossible to enter these values (e.g. exception). But if you simply accept the value and don't actually do anything with it, that's not a violation. – Flater Jul 26 '18 at 13:29
  • If you can, try to check C++ for Artists: The Art, Philosophy, and Science of Object-oriented Programming, page 490-494, available on Google books. The examples only deal with input parameter values. – Ezoela Vacca Jul 26 '18 at 13:29
  • @Flater Yes that is correct. If the Enemy supertype accepts value 0-100 and so does the boss, that is OK. The boss only must not make it more strict, e.g. accepting only 10-50. – Ezoela Vacca Jul 26 '18 at 13:31
  • 1
    @EzoelaVacca: I have repeatedly mentioned that the error is in _blocking_ behavior such as throwing exceptions. I'm not sure how the issue you're bringing to the table conflicts with that. Your posted example doesn't even touch on changing accepted input values, since the method does not even have any input values. This current discussion seems unrelated to the actual posted example. – Flater Jul 26 '18 at 13:38
  • @Flater Yes but it does break the behavior, which is the main point in LSP. If I have a third party code, e.g. EnemyDestroyer accepting objects of type Enemy and destroying them by making their life=0, it will not work with the boss object as its condition is more strict, requiring additional value to be 0. This is what I mean. It will be OK for the parent but not for the child. How is that different from having parent accept 100 and child only 50 (=strengthening that violates LSP)? – Ezoela Vacca Jul 26 '18 at 13:42
  • 2
    @EzoelaVacca: `Yes but it does break the behavior, which is the main point in LSP.` **External** behavior, yes. Internal behavior, no. And that's the point. What the object does (or doesn't do) inside its method is irrelevant, as long as the external caller does not need to personally and explicitly account for any change in behavior. That is the crux of the issue here. **You could create an enemy that is literally immortal and it still wouldn't violate LSP as long as it doesn't need to be handled differently by client code.** Your alleged problem situations all violate encapsulation and SRP. – Flater Jul 26 '18 at 13:48
  • Yes, it breaks the external behavior and that is why I believe my example does violate LSP. The strengthened precondition will cause that the boss object will not get destroyed when the enemy object will, which is what the caller reason. – Ezoela Vacca Jul 26 '18 at 13:51
  • 3
    @EzoelaVacca: This question is pointless if you're just going to keep repeating your flawed interpretation. I have adequately and repeatedly explained the issue in your understanding of the intention of LSP. You define the value of an object's property as its behavior, which it is not. You break encapsulation and SRP and then claim LSP violations based on expectations that should have never existed in your client code. The issue isn't in your implementation of LSP but in your expectation of what is and isn't behavior. You can't judge violations if you don't understand the core intention of LSP – Flater Jul 26 '18 at 13:54
  • In the famous LSP demonstration, Square and Rectangle, the client code does not handle anything differently, the results are just not what the caller expected (a*a instead of a*b). It just shows that the reasoning about behavior is crucial. – Ezoela Vacca Jul 26 '18 at 13:55
  • 2
    @EzoelaVacca: If you apparently already know the answer, why ask the question? I'm done repeating myself. – Flater Jul 26 '18 at 13:56
  • @Flater So according to you the square and rectangle example, shown by Liskov herself, or Robert C. Martin, is incorrect? And the precondition there, explicitly stated, is that w>0... – Ezoela Vacca Jul 26 '18 at 13:58
  • 1
    @EzoelaVacca I believe you may be misunderstanding the meaning of “precondition”. A precondition is not the internal check you’ve shown in your example. A precondition is what must be true prior to calling a method in order to avoid exceptions/undefined behavior. In other words, a precondition is the state of the system *prior* to calling a method. It’s that state of the system that can not be made stricter. – RubberDuck Jul 26 '18 at 21:52
  • 1
    @RubberDuck: That is a much better explanation than I've managed to give. I'll add that to the answer. – Flater Jul 27 '18 at 07:17
  • @RubberDuck I do understand that, I included the check so that it is visible what precondition is expected. It does not change the fact that the precondition is stronger in the derived class. I already found the answer in an original example from Liskov - in the base class, she used W<=0. In the child, W<-1, showing LSP violating in making the condition stronger. In most examples, the check is included with assert. – Ezoela Vacca Jul 27 '18 at 11:23
  • @Flater As I explained above, I understand that. The precondition normally (paraphrasing Liskov) is a value of an argument, arguments or instance members that must be true before the method is executed. In this case, you can simply think that life<=0 must hold before you call the method. Calling code should make sure that the precondition holds. But often in real world, the called code does check the precondition again (according to DbC e.g. when the called is an external code). – Ezoela Vacca Jul 27 '18 at 11:26
  • Please check the highly voted answer here, showing several examples of LSP violation (runtime switching is one, as well as condition strengthening same as I have shown here): https://stackoverflow.com/questions/20861107/can-anyone-provide-an-example-of-the-liskov-substitution-principle-lsp-using-v – Ezoela Vacca Jul 27 '18 at 11:30
  • @EzoelaVacca `I included the check so that it is visible what precondition is expected. It does not change the fact that the precondition is stronger in the derived class.` This proves that you do **not** understand what RubberDuck meant. **The check is not a precondition**. Your example does not have a different precondition between Boss and Enemy. The calling code should not have any expectation of the `life` value, nor whether it passes the `<0` check _before_ calling the method. The entire point of the method is so that the method can be called _without_ needing to pre-check. – Flater Jul 27 '18 at 11:41
  • @Flater Read the paper by Liskov please. The called method assumes the precondition is met! Then imagine it is not an IF but Assert. And it is stronger in the subtype. In Liskov paper, the precondition for calling SetWidth is that the value of argument W>0. So you are saying Liskov is wrong. Because her strengthening example is that base requires W<=0, child requires W<-1 and that is a violation as the precondition is stronger. Even the MIT papers DO include precondition checks in the called method, as a means of defense programming. – Ezoela Vacca Jul 27 '18 at 11:46
  • @EzoelaVacca: Read the answers that are posted please. **THE CHECK IS NOT A PRECONDITION**. Regardless of what the paper says on preconditions, your check is not a precondition, and therefore the explanation given by the paper does not apply. We can't make it any more clear than that. – Flater Jul 27 '18 at 11:46
  • @Flater, I am not saying the check is the precondition itself, I just expressed it. It would be the same if I put a comment there saying "requires life<=0". – Ezoela Vacca Jul 27 '18 at 11:48
  • @EzoelaVacca: It'd help if you actually tried to understand the answer instead of supplant it with your own ideas. I'm done with this. The top 3 answers all boil down to you misunderstanding the intention of LSP and still you argue that you understand it perfectly. You are not interested in a solution, you are only interested in being told you're right. No matter how much you rephrase your own idea, it remains flawed. End of discussion. – Flater Jul 27 '18 at 11:49
  • @EzoelaVacca: Copy/paste your comment again, see if that helps. Every point you raise has already been addressed several times by several people. Instead of re-reading the paper you misunderstand, maybe re-read the answers to your question. – Flater Jul 27 '18 at 11:53
  • No, both Deduplicator and user970.. agree that LSP is violated, just the contract is not stated explicitly and it should be... – Ezoela Vacca Jul 27 '18 at 11:56
  • Then you should go and downvote the answers in SO which claim that with a base precondition 0<100, the child precondition 0<50 is strengthened. This is my only point. – Ezoela Vacca Jul 27 '18 at 11:57
  • Ah, so Liskov is wrong in her example, where a method precondition is a value of the argument being > 0. Right..what could she know about that.. – Ezoela Vacca Jul 27 '18 at 12:00
  • @EzoelaVacca: I'm really just responding so that the mods move this entire shitshow to chat. I'm surprised they haven't already. – Flater Jul 27 '18 at 12:02
  • @Flater They definitely should, as having somebody here who is smarter than Liskov and openly claims she is wrong in her theory..If you ever checked the original LSP, you would never had claimed that. Bye – Ezoela Vacca Jul 27 '18 at 12:03
  • If I even get noticed: It is not unusual to have a precondition verified in the callee. Value of a parameter is probably the most common precondition along with a null reference check. Overridden method must not require more than its base version which is why OP broke the principle. – John V Jul 27 '18 at 12:16
  • @user970696: `Value of a parameter is probably the most common precondition along with a null reference check.` Of an input parameter. Not just any state variable of the object You can't just define a precondition as "anything that can be checked", so where do you draw the line? The **context** helps draw that line. OP's method clearly intends for the method to internally decide if it needs to do something. _Regardless of the decision made_, the calling code can continue. There is no violation of LSP as the calling code does not need to change its handling of a `Boss` which is cast to `Enemy`. – Flater Jul 27 '18 at 12:22
  • LSP is broken not only when you have to change the client code but also when the behavior is not consistent with what you expect based on the base class behavior. If the arguments (or values of instance variables) are sufficient for the parent, they must be sufficient also for the child to achieve the same postcondition. When a method accepts char[10] then its overridden version must not accept char[9]. Not sure I can explain better but I did write a paper about LSP in German. – John V Jul 27 '18 at 12:37
  • @user970696: Your example still uses an input parameter - I agree that input parameter validity should not be changed. I agree with you in theory. If OP's method were a simple `ExecuteDispose()`, it would be wrong for the `Boss` to _refuse_ to dispose for a reason that does not apply to `Enemy`. However, that's not what OP has. OP has written a method which is tasked with _independently deciding to do something_. When he overrides that method, it too needs to _independently make a decision_. Changing the decision making process (without changing the possible outcomes) is not a violation of LSP – Flater Jul 27 '18 at 12:40
  • @user970696: In OP's case: The input is not being validated more strictly. The possible outcomes remain the same (none were added or removed). The only thing that was changed is _encapsulated logic inside of the method_. That is as encapsulated as it can ever be. If you don't allow method bodies to be different even when they are not changing in- or output states, then you're effectively saying that methods should never be overridden and then LSP (and inheritance) just goes out the window altogether. – Flater Jul 27 '18 at 12:43
  • Agree, internal logic has nothing to do with that. When I say it violates LSP I mean it when I consider those values to form a precondition. If the base method accepted Live and expected it to be greater than 50,the overriding method requesting it to be greater than 100 would be a glaring violation. (Corrected a mistake in edit) – John V Jul 27 '18 at 12:50
  • I think the OP wanted to express the following: in a base class, a method requires (from a contract view) a value that is within a certain range. In the overriden method, it will require a bit more to achieve the same postcondition and that is indeed breaking LSP as the code working with the base class object will not work with the subtype object. If he used Java annotations instead of a code resembling flow processing, it would be much clearer. – John V Jul 27 '18 at 12:59
5

if something is supposed to be provable then I think only the actual representation matters,

Nope. If you read just below the fragment you quoted from the paper by Liskov and Wing you will find:

A type’s specification determines what properties we can prove about objects.

So it is not the actual representation but the specification what matters.

Since the actual representation satisfies several, different specifications, you need to make the specification (the contract) explicit in order to tell whether the Subtype Requirement (aka LSP) holds.

Stop harming Monica
  • 835
  • 1
  • 8
  • 11
2

Precondition is something that the calling code is responsible for, not the callee who actually should rely on the calling code to have met the preconditions. With regards to your code, the derived class imposed more restrictions for Dispose method to be called.

To be LSP compliant (and DbC, where the caller has to make sure preconditions apply), I would suggest a solution like this:

class Enemy

{

public virtual bool CanBeDestroyed()

{

  return life<=0;

}

    public void CheckHealth()

    {       assert.CanBeDestroyed(“true”); //precondition: CanBeDestroyed is true, caller’s responsibility to check before the method is called

            Dispose();

    }

}



class Boss : Enemy

{

public override bool CanBeDestroyed()

{

  return life<=0 && PowerDestroyed;

}

}
Ezoela Vacca
  • 881
  • 1
  • 6
  • 12
John V
  • 4,898
  • 10
  • 47
  • 73
1

If there is no explicitly stated contract, you are basically on your own:

  1. As an implementer, you should consider all working code which does use your interface, past, present, and future, as sacrosanct.
    Thus, all that code you might not know, or even ever learn, about constrains what you might consider your contract. Put another way, just about all changes are breaking changes.

  2. As a consumer, you have to try divining the future, specifically all the code, past, present, and future, whether you will ever know about it or not, which implements or extends the contract.
    That basically means all you can say is "it exists, and using it might have some effect". Yes, that's a completely unsatisfactory state of affairs.

Luckily, there are conventions, hints in the names, and the principle of least surprise to let you guess at a somewhat more useful contract. The problem is you might guess wrong, and while you might never know it, there might be quite a fallout from that.


So, does the derived class violate the LSP?

Strictly yes, practically who knows, the contract was never codified.

Deduplicator
  • 8,591
  • 5
  • 31
  • 50
-2

The LSP is not violated.

There is nothing really provable about CheckHealth(), (being a void doesn't help) You could argue that "sometimes Dispose is called" but that won't change the result of the Method call and is equally true for Boss.

The LSP is broken when a change in a child class would cause the program to crash when using the child class rather than its parent.

Let's expand you example to:

void CheckHealth(int percentageLife) {}

Now you have an unwritten expectation of percentageLife being between 0 and 100. which could cause issues if broken in a child class.

Lets go further, we would want to add bounds checking

void CheckHealth(int percentageLife) {
    if(percentageLife < 0) {throw new NotAPercentageException();}
    if(percentageLife > 100) {throw new NotAPercentageException();}
    ...
}

So now in c# its arguably the same, because we cant specify exceptions in the compiler defined contract. But in Java we would state that these might be thrown and the compiler would know about it.

Clearly the LSP must be language agnostic. So it must be arguable that the origional

//percentage is a percentage, dont send > 100 or < 0 !! obviously!
void CheckHealth(int percentageLife) {}

Is just as much a 'contract' as

void CheckHealth(SpecialPercentageDataType percentageLife) {}

or similar

Ewan
  • 70,664
  • 5
  • 76
  • 161
  • 1
    No, the provable property is that when you call the method CheckHealth with life<=0, the function Destroy() is called. This is not true for the child. It is clearly violating the requirement of not strengthened pre-conditions. – Ezoela Vacca Jul 26 '18 at 08:40
  • how would you prove that? – Ewan Jul 26 '18 at 08:40
  • LSP is about behavior - subtypes must be behaviorally substitutable for their base types (from Liskov's original presentation). In this example, if you use the subtype when the bass class is used, it won't work because there is additional condition in the child defined. – Ezoela Vacca Jul 26 '18 at 08:43
  • It will work just fine, you are confusing the logic with the conditions for functionality – Ewan Jul 26 '18 at 08:44
  • Please read about LSP, ideally in the original paper. This is the classic violation. LSP forbids making preconditions stronger - and this is what you made when CheckHealth requires additional check. Here the original paper: http://stg-tud.github.io/sedc/Lecture/ws13-14/3.3-LSP.html#mode=document – Ezoela Vacca Jul 26 '18 at 08:47
  • An example from a book: Base class requires an INT to be < 100. The child requires it to be < 50. LSP is violated. – Ezoela Vacca Jul 26 '18 at 08:49
  • 2
    clearly you think you are right, but then why ask a question – Ewan Jul 26 '18 at 08:50
  • 1
    I do not discuss whether LSP is violated by itself (it is, as I took this example from another source) but whether a contract is required to judge that, or not. Some say it is, some say it is not. – Ezoela Vacca Jul 26 '18 at 08:50
  • 1
    Take a look here, this explains it better (my English sucks): https://softwareengineering.stackexchange.com/questions/187613/how-does-strengthening-of-preconditions-and-weakening-of-postconditions-violate – Ezoela Vacca Jul 26 '18 at 08:52
  • re do your methods to require a uint and then have a child class work with int and that would be a good example – Ewan Jul 26 '18 at 08:53
  • you are right, that would be better. Anyway the concept should hold here as well. – Ezoela Vacca Jul 26 '18 at 08:54
  • Life isn't even exposed, any calling code would have to check whether Dispose had been called after calling CheckHealth before using the object further. so the exact logic about when its called can be anythign you like – Ewan Jul 26 '18 at 08:57
  • @Ewan - I suspect that `Dispose()` is meant to have an observable side effect. – Jules Jul 26 '18 at 09:04
  • @Jules yes, lets assume it has, but even so. calling code cant know whether dispose has been called – Ewan Jul 26 '18 at 09:05
  • While I think you are correct that LSP is not violated, your explanation as to why that is the case is not correct. The fact that you don't even touch on inheritance in any of your code examples/explanation very quickly shows that you're not focusing on the correct error case. – Flater Jul 26 '18 at 10:40
  • @Flater hmm, I thought it was obvious that I was still talking about the same function in the same inheritance example. just adding a parameter – Ewan Jul 26 '18 at 10:42