Skip to main content
Tweeted twitter.com/StackSoftEng/status/917761945784209408
added 2450 characters in body
Source Link
inf3rno
  • 1.3k
  • 11
  • 28

How to losenloosen input contracts by inheritance?

you can losenloosen 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 losenloosen preconditions while keeping the same behavior? For example if I write a unit test for argument validation by a method, then loseningloosening the preconditions mean that the unit test will fail by the instances of the subclass. So by loseningloosening 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){
        // loseningloosening 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 losenedloosened 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 loseningloosening?

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.

How to losen input contracts by inheritance?

you can losen 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 losen preconditions while keeping the same behavior? For example if I write a unit test for argument validation by a method, then losening the preconditions mean that the unit test will fail by the instances of the subclass. So by losening 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){
        // losening 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 losened 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 losening?

How to loosen input contracts by inheritance?

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.

Source Link
inf3rno
  • 1.3k
  • 11
  • 28

How to losen input contracts by inheritance?

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 losen 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 losen preconditions while keeping the same behavior? For example if I write a unit test for argument validation by a method, then losening the preconditions mean that the unit test will fail by the instances of the subclass. So by losening 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){
        // losening 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 losened 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 losening?