Di recente ho ereditato una base di codice che contiene alcuni importanti violatori di Liskov. In classi importanti. Questo mi ha causato enormi quantità di dolore. Lasciami spiegare perché.
Ho Class A
, da cui deriva Class B
. Class A
e Class B
condividi un gruppo di proprietà che hanno Class A
la precedenza sulla propria implementazione. L'impostazione o il recupero di una Class A
proprietà ha un effetto diverso sull'impostazione o il recupero della stessa proprietà esatta Class B
.
public Class A
{
public virtual string Name
{
get; set;
}
}
Class B : A
{
public override string Name
{
get
{
return TranslateName(base.Name);
}
set
{
base.Name = value;
FunctionWithSideEffects();
}
}
}
Mettendo da parte il fatto che questo è un modo assolutamente terribile di fare traduzione in .NET, ci sono una serie di altri problemi con questo codice.
In questo caso Name
viene utilizzato come indice e variabile di controllo del flusso in più punti. Le classi precedenti sono disseminate in tutto il codebase sia nella loro forma grezza che derivata. Violare il principio di sostituzione di Liskov in questo caso significa che devo conoscere il contesto di ogni singola chiamata a ciascuna delle funzioni che accettano la classe base.
Il codice utilizza oggetti di entrambi Class A
e Class B
, quindi non posso semplicemente fare un Class A
abstract per costringere le persone a usare Class B
.
Ci sono alcune funzioni di utilità molto utili che operano su Class A
e altre funzioni di utilità molto utili su cui operare Class B
. Idealmente mi piacerebbe essere in grado di utilizzare qualsiasi funzione di utilità che può operare in Class A
su Class B
. Molte delle funzioni che prendono a Class B
potrebbero facilmente prendere un Class A
se non fosse per la violazione dell'LSP.
La cosa peggiore di questo è che questo caso particolare è davvero difficile da riformattare poiché l'intera applicazione dipende da queste due classi, opera continuamente su entrambe le classi e si spezzerebbe in cento modi se cambio questo (cosa che farò Comunque).
Quello che dovrò fare per risolvere questo problema è creare una NameTranslated
proprietà, che sarà la Class B
versione della Name
proprietà e cambierà molto, molto attentamente ogni riferimento alla Name
proprietà derivata per usare la mia nuova NameTranslated
proprietà. Tuttavia, sbagliando persino uno di questi riferimenti, l'intera applicazione potrebbe esplodere.
Dato che il codebase non ha test unitari, questo è abbastanza vicino ad essere lo scenario più pericoloso che uno sviluppatore può affrontare. Se non cambio la violazione, devo spendere enormi quantità di energia mentale per tenere traccia di quale tipo di oggetto viene operato in ciascun metodo e se correggo la violazione potrei far esplodere l'intero prodotto in un momento inopportuno.