4

According to LSP wiki:

Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of T (correctness, task performed, etc.).

...

These are detailed in a terminology resembling that of design by contract methodology, leading to some restrictions on how contracts can interact with inheritance:

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.

and this text about contracts:

If a function in a derived class overrides a function in its super class, then only one of the in contracts of the function and its base functions must be satisfied. Overriding functions then becomes a process of loosening the in contracts.

A function without an in contract means that any values of the function parameters are allowed. This implies that if any function in an inheritance hierarchy has no in contract, then in contracts on functions overriding it have no useful effect.

Conversely, all of the out contracts need to be satisfied, so overriding functions becomes a processes of tightening the out contracts.

you can loosen preconditions in a subtype, but the instances of the ancestor must be substitutable with the instances of the subtype.

I was wondering how is it possible to loosen preconditions while keeping the same behavior? For example if I write a unit test for argument validation by a method, then loosening the preconditions mean that the unit test will fail by the instances of the subclass. So by loosening a precondition I can violate LSP.

class T {
    aMethod(x){
        assert(x !== "invalid");
        const y = this.doSomething(x);
        const z = this.doAnotherThing(y);
        return z;
    },
    doSomething(x){
        // ...
        return y;
    },
    doAnotherThing(y){
        // ...
        return z;
    }
}

class S extends T {
    aMethod(x){
        // loosening preconditions by removing the assertion
        const y = this.doSomething(x);
        const z = this.doAnotherThing(y);
        return z;
    }
}

.

class TestCase {
    testInputValidation(C){
        expect(function (){
            const o = new C();
            o.aMethod("invalid");
        }).toThrow();
    }
}

var tc = new TestCase();
tc.testInputValidation(T); // passes
tc.testInputValidation(S); // fails because I loosened the contract

Maybe I don't understand LSP and contracts, I don't know. Can you write a (preferably not dummy) example which fulfills both substitution and precondition loosening?

Conclusion:

I think most of the LSP descriptions missing the point. The real question is why we need LSP by inheritance? Violating LSP will lead to unexpected errors, because we won't be able to use instances of subclasses where we used instances of the base class. The subclass instances pass the type check, so we will have errors related to their behavior, which is relatively hard to debug. So LSP acts as a preventive measure.

The example was not the best, I mean it did not make much sense, because only the in-contract was removed/loosened. To make this work I should have written something like this:

class S extends T {
    aMethod(x){
        // loosening preconditions by removing the assertion
        const y = this.doSomething(x);
        const z = this.doAnotherThing(y);
        return z;
    },
    doSomething(x){
        if (x == "invalid")
            x = transformToValid(x);
        return super.doSomething(x);
    }
}

To answer the question from my point of view. I was testing for the in-contract of the base class and that contract was loosened in the subclass, so it is natural that the test failed for the subclass. We should not test the subclasses for the same contracts as we test the base class for, if we test for contracts at all. On the other hand we must test the subclasses for inputs that pass the base class in-contract and the outputs for these inputs must pass the out-contract of the base class. By applying LSP the latter is assured, because we can only strengthen the out-contract in subclasses. So it is possible to reuse certain tests by subclasses. We need to write new tests only for the loosened part of the in-contract, which is not part of the out-contract of the base class. To my understanding the assert(x !== "invalid"); is an in-contract in the base class. Using contracts can help by any code modification, because you can read the valid inputs from these contracts, and if you strengthen a precondition, then you will know, that you have to check every usage of the actual method, because the changes can break them. If you loosen a precondtion, then you will know, that these changes should not break existing code except if you have subclasses overriding the actual method. So it is better to make these contracts explicit in the code. Thank you for all your answers! I gave the points to NickL, because he helped the most to understand the relevance of LSP and contracts.

inf3rno
  • 1,209
  • 10
  • 26
  • 2
    By removing the `assert` you actually changed the postcondition. As you can see in your test, you check whether the postcondition `.toThrow()` holds. What the function says: precondition: given an input of "invalid", postcondition: throws assertionerror. You could weaken the precondition by *always* throwing an assertion error. Whay you are doing now us weakening the postcondition. – NickL Oct 07 '17 at 23:40
  • The reason why you can consider the assertion error as a postcondition is this case, is because you expect it to be the output. When you test a function, you use data that satisfies the precondition, call the function and _verify_ whether the postcondition holds for the result. You can weaken the precondition, but should still ensure that the postcondition holds as well. – NickL Oct 07 '17 at 23:58
  • @NickL Probably you should add an answer with references about what weakening a precondition or postcondition means. I cannot judge currently whether you are right. I only found this reference, but I don't know where to find the article: https://en.wikipedia.org/wiki/Precondition#cite_note-2 – inf3rno Oct 08 '17 at 03:02
  • @NickL I think I might found a definition. https://www.cs.umd.edu/~mvz/handouts/weakest-precondition.pdf I am just too tired to think about what it means in this context. I'll check it tomorrow. – inf3rno Oct 08 '17 at 03:12
  • @NickL Ok. I read what I linked, I was right, that losening a precondition means that more arguments will be valid. So there was no misunderstanding about that. Can you write an example where you weaken a precondition and your postconditions still hold? Shouldn't a stronger precondition result in some kind of error by invalid values? – inf3rno Oct 08 '17 at 18:41
  • 1
    `{ x >= 2 } x := x + 1 { x > 2}` now with weaker precondition: `{ x >= 1 } x := x + 2 { x > 2}`. Preconditions are _assumptions_ you are allowed to make. Maybe you should take a look at [Hoare Logic](https://en.m.wikipedia.org/wiki/Hoare_logic). >=1 contains more elements than AND contains the elements of >=2, thus it is weaker. The 'error' you mention only _tests_ the assumption, since the function only provides valid output based on the assumption. – NickL Oct 08 '17 at 23:14
  • @NickL Thanks I think I understand now. The whole point of LSP and these constraints what I wrote in my example. If we violate them, then the instances of the subclasses will cause unexpected errors in the code meant for the instances of the superclass. I can accept this if you want to post it as an answer. – inf3rno Oct 09 '17 at 04:55
  • exactly! That is what the other answers also try to explain, but from different points of view. I've added my comments as an answer. – NickL Oct 09 '17 at 08:56
  • 1
    You do not need to use inheritance at all. By specifying available values through a list or a single variable [**like this**](https://pastebin.com/xHrZhpdj) you can then instantiate a new instance of `T` within your tests, either specifying some disallowed states or not. You are mostly suffering from poor design, not from violation of the LSP. – Andy Oct 09 '17 at 09:15
  • 1
    @DavidPacker I think you missed the questions. It was not about whether to use inheritance or not, but about the meaning of 'weakening preconditions' in the context of LSP. The purpose of the example is to have a sub-class weaken the precondition, without violating LSP. About your example, changing assertions based on the arguments supplied to the constructor? You really think that is a good design? Assertions _assert_ whether the state is correct for the _implementation_ of the function. How does the caller know what the function should assert? – NickL Oct 09 '17 at 15:47
  • @NickL I didn't miss the question. I also didn't think the problem is approached correctly with pointless inheritance and OP is dealing with this issue only because their class is not dynamic. My suggestion is not really that different from OP's example, with the exception of allowing you to freely extend the functionality when needed by injecting disallowed types. Thus yes, I think my proposed design is better - otherwise I wouldn't propose it. If you do not like the constructor injection you can use method arguments, that however kind of defeats the purpose of keeping the method API simple. – Andy Oct 09 '17 at 16:04
  • 1
    @DavidPacker OP is not dealing with any issue at all. His question was _how is it possible to loosen preconditions while keeping the same behavior?_ His example is a made-up example to illustrate his interpretation of loosening preconditions. How do you plan to exemplify LSP without polymorphism? Your suggestion indeed is almost the same as OP's example, but more dynamic. However, how would that example be better for this question than OP's example? – NickL Oct 09 '17 at 17:37
  • 1
    @NickL I was simply suggesting a different approach. I knew it was not related to the question, hence why I posted my suggestion as a comment only, rather than posting an actual answer. Even I when I am designing something I tend to get carried away and get blinded by my own implementation only to end up not seeing an alternative which may or may not be just as good or even better. I posted the alternative as a reminder that there are other approaches to the problem, too. – Andy Oct 09 '17 at 18:27
  • 1
    @DavidPacker The example I posted was just made up and not an actual problem. I was curious about how is it possible to resuse tests by LSP. The short answer you cannot reuse contract tests, if the contracts change in the subclasses, so those tests won't hold, but that is not a problem, because they must not hold. You can reuse implementation tests for the inputs which are valid in the base class. For the inputs, which aren't valid in the base class, but are valid in the subclass you have to write new tests. – inf3rno Oct 09 '17 at 18:55
  • 1
    The wikipedia article has a number of problems: 1) it is not a principle, but an idea. When using contracts, you cannot avoid the concept, as it is needed to perform type substitution. 2) the square-circle discussion is irrelevant (there are no contracts in that example). 3) Liskov and Guttag discussed this before Meyer's book was published, in "Abstraction and Specification in Program Development", 1986. – Frank Hileman Oct 10 '17 at 16:31
  • 1
    @FrankHileman I hadn't seen the wiki example yet, but indeed it is a bad example.. Of course there are some contracts there (the width and height of a square are equal), but the example is bad.. and probably wrong in a number of ways (e.g. "the square invariant would weaken the postconditions for Rectangle". This sounds wrong to me..) – NickL Oct 11 '17 at 11:48
  • 2
    @NickL Maybe the circle/square discussion is actually correct in some places, but everywhere I have seen it used, it makes little sense, treating contracts as something vague, unspecified. Of course an invariant and postcondition are two separate things. – Frank Hileman Oct 11 '17 at 15:29

3 Answers3

5

By removing the assert you actually changed the postcondition. As you can see in your test, you check whether the postcondition .toThrow() holds. What the function says: precondition: given an input of "invalid", postcondition: throws assertionerror.

You could weaken the precondition by always throwing an assertion error. What you are doing now is weakening the postcondition.

The reason why you can consider the assertion error as a postcondition is this case is because you expect it to be the output. When you test a function, you use data that satisfies the precondition, call the function and verify whether the postcondition holds for the result. You can weaken the precondition, but should still ensure that the postcondition holds (it is allowed to be stronger)

Here an example written as a Hoare triple

{ x >= 2 } x := x + 1 { x > 2}

The contract for this function could be given an input greater than or equal to 2, the result will be greater than 2

now with weaker precondition:

{ x >= 1 } x := x + 2 { x > 2}

Or, I could interpret this change as a stronger postcondition (which is also allowed):

{ x >= 2 } x := x + 2 { x > 3}

In both cases, the contract of the original function still holds. The whole point of LSP is that no matter what type you replace it with, you at least get the behavior of what the contract states. My change to the body of the function could be interpreted as either weakening of the precondition or strengthening the postcondition.

Don't confuse this with the behavior of the function. Yes, the behavior will be changed, and so has the output in this particular example. The first function would return f(2) = 3, and the second function would return f(2) = 4. However, remember that we defined the contract as the result will be greater than 2., we did not define the exact output.

When we talk about the strengthening of types, we mean that a more specific type is stronger than a more abstract type. Object is weaker than Number, Double is stronger than Number.

Preconditions are assumptions you are allowed to make. The assumption >=1 contains more elements than AND contains the elements of >=2, thus it is weaker. The 'error' raised by assert only tests the assumption, since the function only provides valid output based on the assumption.

NickL
  • 260
  • 1
  • 7
3

What those texts about LSP and contracts effectively say is:

For all valid inputs to aMethod, the implementation in S must behave the same as the implementation in T. When calling o.aMethod(x), the caller should not have to care if o is an object of type T or type S.

Note that I am only talking about * valid* inputs to aMethod. If "invalid" is considered a valid input to T::aMethod (with the defined result of throwing an exception), then the base class method has the weakest possible contract and there is no weakening that the derived class can do.
If "invalid" is not considered to be valid input (and the assert only exists as a sanity check that the caller does obey the preconditions), then the derived class is allowed to define sensible behavior for "invalid". This is what is meant by weakening the precondition, because a larger set of values is acceptable to the function.

This means that negative tests against the preconditions of the base class (i.e. tests that verify that values that don't meet the preconditions aren't accepted) are not required to show the same result when executed against a derived class.

Bart van Ingen Schenau
  • 71,712
  • 20
  • 110
  • 179
  • Can you write 2 examples, one for the case when the error throwing is part of the output so the postcondition should hold on that, and another for the case when the error throwing is not part of the output? The programming language does not really matter, I just don't understand the difference between the 2 cases. Is there any rule which helps to decide which one we are talking about? – inf3rno Oct 08 '17 at 18:50
  • @inf3rno The difference is not in the code but in the contract. If raising when passed `"invalid"` is part of the contract then you should not change that in subclasses. If it is not, then you can. – Stop harming Monica Oct 08 '17 at 21:58
  • @Goyo How can I decide what should be part of the contract and what shouldn't? – inf3rno Oct 09 '17 at 03:30
  • @inf3rno: Should it be normal, expected behavior of a caller to pass `"invalid"` as an argument? If yes, then it is part of the contract and you need to specify what behavior the caller can expect of your function. If no, then it is not part of the contract and the exception is a sign that somebody made a programming error. – Bart van Ingen Schenau Oct 09 '17 at 06:18
  • @inf3rno If unsure do not make it part of the contract. – Stop harming Monica Oct 09 '17 at 07:06
2

You seem to be confused about the exact meaning of precondition. A precondition is an abstract concept used in formal reasoning about code. It is not an assert() call. Indeed is does not have to appear in the code at all.

The precondition specifies the set of circumstances in which there is a defined meaning for an object. If the precondition does not hold the formal meaning is undefined. The LSP definition starts with Let phi(x) be a property provable about objects x of type T. This implicitly excludes any circumstances that violate the precondition of T, because nothing can be proved starting with undefined.

Usually it is not at all clear what a reasonable precondition for a given piece of code is. The author likely has some more or less fuzzy idea about the circumstances in which the code should have a defined meaning. But most of the time this idea is not explicit in the code (just as in your example). assert() might be used to express the precondition, but it just as well might specify behavior for that input (though I would consider that bad style). Without an explicit language element for preconditions as in Eiffel or an explicit definition of the precondition in the documentation there is always uncertainty and some room for argument.

Your example code is confused about whether there is any precondition at all. If the assert(x !== "invalid") is meant as precondition then any test for that behavior is meaningless / undefined / forbidden. If the assert is not as meant as precondition then the precondition is true for all inputs. Which already is the weakest possible precondition.

Patrick
  • 1,873
  • 11
  • 14
  • According to the text I linked from dlang manual: "The pre contracts specify the preconditions before a statement is executed. The most typical use of this would be in validating the parameters to a function." So either they don't know what they are talking about, or your answer is wrong. – inf3rno Oct 07 '17 at 21:50
  • It's just a different perspective. The dlang contracts - like Eiffel contracts - exists as distinct language elements and have concrete behavior and that document describes that behavior. I took a more abstract formal verification in general perspective in my answer. But there is no conflict between both, the dlang contracts are LSP-compatible to the formal verification perspective. – Patrick Oct 07 '17 at 22:08
  • Hmm I read your answer again. According to it I should not test for precondition checks in my unit tests. I read I think in the TDD book by Kent Beck, that I should test every single line of code. (Maybe type checking can be an exception.) If so, then the preconditions should not appear in the code, because I won't test them. How can I weaken a precondition if it does not even appear in the code? I think I am missing the point here. – inf3rno Oct 08 '17 at 20:06
  • Glad you brought this up. The OP is trying to make do in a language without contracts. – Frank Hileman Oct 09 '17 at 16:51
  • @inf3rno If you want to do real software engineering, without a contract language, you can still do it, but you will have to ignore advice about TDD etc. The preconditions, tested or not, will still be hit when there is an error in the code, and you will find and eliminate the error much faster by having the precondition. – Frank Hileman Oct 10 '17 at 16:36