Fondamentalmente vogliamo che le cose si comportino in modo ragionevole.
Considera il seguente problema:
Mi viene assegnato un gruppo di rettangoli e voglio aumentare la loro area del 10%. Quindi, quello che faccio è impostare la lunghezza del rettangolo su 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 una miscela di quadrati e rettangoli, e quando è stata cambiata la lunghezza del rettangolo, lo era anche la larghezza.
I miei test di unità passano perché ho scritto tutti i miei test di unità per utilizzare una raccolta di rettangoli. Ora ho introdotto un piccolo bug nella mia applicazione che può passare inosservato per mesi.
Peggio ancora, Jim dalla contabilità vede il mio metodo e scrive un altro codice che usa il fatto che se passa i quadrati al mio metodo, ottiene un bel 21% di aumento delle dimensioni. Jim è felice e nessuno è più saggio.
Jim viene promosso per un lavoro eccellente in una divisione diversa. Alfred si unisce alla compagnia da junior. Nella sua prima segnalazione di bug, Jill from Advertising ha riportato che il passaggio dei quadrati a questo metodo si traduce in un aumento del 21% e vuole che il bug sia corretto. Alfred vede che Squares e Rectangles sono usati ovunque nel codice e si rendono conto che rompere la catena ereditaria è impossibile. Inoltre, non ha accesso al codice sorgente di Accounting. Così Alfred risolve 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 abilità di hacker super e Jill firma che il bug è stato corretto.
Il prossimo mese nessuno viene pagato perché la contabilità dipende dalla possibilità di passare i quadrati al metodo IncreaseRectangleSizeByTenPercent
e ottenere un aumento dell'area del 21%. L'intera azienda entra in modalità "priorità 1 bugfix" per rintracciare la fonte del problema. Tracciano il problema alla correzione di Alfred. Sanno che devono tenere felici sia la contabilità che la pubblicità. Quindi risolvono il problema identificando l'utente con la chiamata al 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 quotidianamente i programmatori. Le violazioni del principio di sostituzione di Liskov possono introdurre bug molto sottili che vengono rilevati solo anni dopo che sono stati scritti, in quel momento la correzione della violazione interromperà un sacco di cose e non risolvendo farà arrabbiare il più grande cliente.
Esistono due modi realistici per risolvere questo problema.
Il primo modo è rendere il rettangolo immutabile. Se l'utente di Rectangle non può modificare le proprietà Length e Width, questo problema scompare. Se vuoi un rettangolo con una lunghezza e una larghezza diverse, ne crei uno nuovo. I quadrati possono ereditare felicemente dai rettangoli.
Il secondo modo è di rompere la catena ereditaria tra quadrati e rettangoli. Se un quadrato è definito come avente una sola proprietà SideLength
e i rettangoli hanno una proprietà Length
e Width
e non c'è ereditarietà, è impossibile rompere accidentalmente le cose aspettandosi un rettangolo e ottenendo un quadrato. In termini C #, potresti seal
della tua classe di rettangolo, che garantisce che tutti i rettangoli che ottieni siano in realtà rettangoli.
In questo caso, mi piace il modo in cui gli "oggetti immutabili" risolvono il problema. L'identità di un rettangolo è la sua lunghezza e larghezza. Ha senso che quando vuoi cambiare l'identità di un oggetto, ciò che vuoi veramente è un nuovo oggetto. Se perdi un vecchio cliente e guadagni un nuovo cliente, non cambi il campo Customer.Id
dal vecchio cliente a quello nuovo, crei un nuovo Customer
.
Le violazioni del principio di sostituzione di Liskov sono comuni nel mondo reale, soprattutto perché un sacco di codice è scritto da persone che sono incompetenti / sotto pressione / non si preoccupano / sbagliano. Può e causa problemi molto sgradevoli. Nella maggior parte dei casi, desideri preferire la composizione sull'ereditarietà .