Fondamentalmente vogliamo che le cose si comportino in modo sensato.
Considera il seguente problema:
Mi viene dato un gruppo di rettangoli e voglio aumentare la loro area del 10%. Quindi quello che faccio è impostare la lunghezza del rettangolo a 1,1 volte rispetto a prima.
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
Ora in questo caso, tutti i miei rettangoli ora hanno una lunghezza aumentata del 10%, che aumenterà la loro area del 10%. Sfortunatamente, qualcuno mi ha effettivamente passato un misto di quadrati e rettangoli, e quando la lunghezza del rettangolo è stata cambiata, così è stata la larghezza.
I miei test unitari superano perché ho scritto tutti i test unitari per utilizzare una raccolta di rettangoli. Ora ho introdotto un bug sottile nella mia applicazione che può passare inosservato per mesi.
Peggio ancora, Jim dalla contabilità vede il mio metodo e scrive qualche altro codice che usa il fatto che se passa quadrati nel mio metodo, ottiene un aumento del 21% delle dimensioni. Jim è felice e nessuno è più saggio.
Jim viene promosso per un lavoro eccellente in una divisione diversa. Alfred entra a far parte dell'azienda come junior. Nella sua prima segnalazione di bug, Jill di Advertising ha riferito che passare quadrati a questo metodo comporta un aumento del 21% e vuole che il bug sia risolto. Alfred vede che Quadrati e Rettangoli sono usati ovunque nel codice e si rende conto che è impossibile spezzare la catena ereditaria. Inoltre, non ha accesso al codice sorgente di contabilità. Quindi Alfred corregge il bug in questo modo:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle)
{
rectangle.Length = rectangle.Length * 1.1;
}
if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
Alfred è contento delle sue superbe abilità di hacking e Jill firma che il bug è stato corretto.
Il mese prossimo nessuno viene pagato perché la contabilità dipendeva dalla possibilità di passare quadrati al IncreaseRectangleSizeByTenPercent
metodo e ottenere un aumento dell'area del 21%. L'intera azienda passa in modalità "priorità 1 bugfix" per rintracciare l'origine del problema. Tracciano il problema alla correzione di Alfred. Sanno che devono rendere felici sia la contabilità che la pubblicità. Quindi risolvono il problema identificando l'utente con la chiamata del metodo in questo modo:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
IncreaseRectangleSizeByTenPercent(
rectangles,
new User() { Department = Department.Accounting });
}
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
{
rectangle.Length = rectangle.Length * 1.1;
}
else if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
E così via e così via.
Questo aneddoto si basa su situazioni del mondo reale che affrontano i programmatori quotidianamente. Le violazioni del principio di sostituzione di Liskov possono introdurre bug molto sottili che vengono rilevati solo anni dopo che sono stati scritti, a quel punto correggere la violazione romperà un mucchio di cose e non risolverà farà arrabbiare il tuo più grande cliente.
Esistono due modi realistici per risolvere questo problema.
Il primo modo è di rendere immutabile Rectangle. Se l'utente di Rectangle non può modificare le proprietà Lunghezza e Larghezza, questo problema scompare. Se vuoi un rettangolo con lunghezza e larghezza diverse, ne crei uno nuovo. I quadrati possono ereditare felicemente dai rettangoli.
Il secondo modo è spezzare la catena di eredità tra quadrati e rettangoli. Se si definisce un quadrato con una sola SideLength
proprietà e i rettangoli hanno una Length
e Width
proprietà e non c'è eredità, è impossibile rompere accidentalmente le cose aspettandosi un rettangolo e ottenendo un quadrato. In termini di C #, potresti avere la seal
tua classe rettangolo, il che assicura che tutti i rettangoli che ottieni siano in realtà rettangoli.
In questo caso, mi piace il modo "oggetti immutabili" di risolvere il problema. L'identità di un rettangolo è la sua lunghezza e larghezza. Ha senso che quando vuoi cambiare l'identità di un oggetto, quello che vuoi veramente è un nuovo oggetto. Se perdi un vecchio cliente e guadagni un nuovo cliente, non cambi il Customer.Id
campo dal vecchio cliente a quello nuovo, crei un nuovoCustomer
.
Le violazioni del principio di sostituzione di Liskov sono comuni nel mondo reale, soprattutto perché un sacco di codice là fuori è scritto da persone che sono incompetenti / sotto la pressione del tempo / non si preoccupano / commettono errori. Può e porta ad alcuni problemi molto cattivi. Nella maggior parte dei casi, si preferisce invece la composizione rispetto all'eredità .