In base alla wiki di LSP :
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.
e questo testo sui contratti :
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.
puoi allentare le precondizioni in un sottotipo, ma le istanze dell'antenato devono essere sostituibili con le istanze del sottotipo.
Mi stavo chiedendo come sia possibile allentare le precondizioni mantenendo lo stesso comportamento? Per esempio, se scrivo un test unitario per la convalida degli argomenti con un metodo, quindi allentare le precondizioni significa che il test unitario fallirà per le istanze della sottoclasse. Quindi allentando una precondizione posso violare 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
Forse non capisco LSP e contratti, non lo so. Puoi scrivere un esempio (preferibilmente non fittizio) che soddisfi sia la sostituzione che la precondizione?
Conclusione:
Penso che la maggior parte delle descrizioni LSP manchi il punto. La vera domanda è perché abbiamo bisogno di LSP per via ereditaria? Violare LSP porterà a errori imprevisti, perché non saremo in grado di utilizzare istanze di sottoclassi in cui abbiamo usato istanze della classe base. Le istanze di sottoclasse passano il controllo del tipo, quindi avremo degli errori relativi al loro comportamento, che è relativamente difficile da eseguire il debug. Quindi LSP funge da misura preventiva.
L'esempio non era il migliore, voglio dire che non aveva molto senso, perché solo il contratto interno era stato rimosso / allentato. Per fare questo lavoro avrei dovuto scrivere qualcosa del genere:
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);
}
}
Per rispondere alla domanda dal mio punto di vista. Stavo testando l'in-contract della classe base e quel contratto è stato allentato nella sottoclasse, quindi è naturale che il test non abbia funzionato per la sottoclasse. Non dovremmo testare le sottoclassi per gli stessi contratti mentre testiamo la classe base, se testiamo per i contratti. D'altra parte, dobbiamo testare le sottoclassi per gli input che superano la classe base in-contract e le uscite per questi input devono superare l'out-contract della classe base. Applicando LSP, quest'ultimo è assicurato, poiché possiamo solo rafforzare l'out-contract in sottoclassi. Quindi è possibile riutilizzare determinati test per sottoclassi. Dobbiamo scrivere nuovi test solo per la parte allentata del contratto interno, che non fa parte del contratto esterno della classe base. A mio avviso, assert(x !== "invalid");
è un in-contract nella classe base. L'utilizzo dei contratti può essere utile con qualsiasi modifica del codice, in quanto è possibile leggere gli input validi da questi contratti e, se si rafforza una condizione preliminare, allora si saprà che è necessario verificare ogni utilizzo del metodo effettivo, poiché le modifiche possono interromperli . Se si allenta una precondizione, allora saprete che queste modifiche non dovrebbero rompere il codice esistente eccetto se avete sottoclassi che sovrascrivono il metodo attuale. Quindi è meglio rendere espliciti questi contratti nel codice. Grazie per tutte le tue risposte! Ho dato i punti a NickL, perché ha aiutato di più a capire la rilevanza di LSP e dei contratti.