Recentemente ho ereditato un codice base che contiene alcuni violatori di Liskov. In classi importanti. Questo mi ha causato enormi quantità di dolore. Lasciami spiegare perché.
Ho Class A
, che deriva da Class B
. Class A
e Class B
condividono un gruppo di proprietà che Class A
sovrascrive con la propria implementazione. L'impostazione o il recupero di una proprietà Class A
ha un effetto diverso sull'impostazione o sul recupero della stessa proprietà da 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 la 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 un numero di punti. Le classi di cui sopra sono disseminate in tutto il codebase sia nella loro forma grezza che in quella derivata. Violare il principio di sostituzione di Liskov in questo caso significa che ho bisogno di conoscere il contesto di ogni singola chiamata a ciascuna delle funzioni che prendono la classe base.
Il codice usa oggetti sia di Class A
sia di Class B
, quindi non posso semplicemente creare Class A
abstract per forzare le persone a usare Class B
.
Ci sono alcune utilissime funzioni di utilità che funzionano su Class A
e altre utilissime funzioni di utilità che operano su Class B
. Idealmente mi piacerebbe essere in grado di utilizzare qualsiasi funzione di utilità che possa operare su Class A
su Class B
. Molte delle funzioni che prendono un Class B
potrebbero facilmente prendere un Class A
se non fosse per la violazione dell'LSP.
La cosa peggiore è che questo caso particolare è davvero difficile da refactoring dato che l'intera applicazione dipende da queste due classi, opera su entrambe le classi continuamente e si rompe in cento modi se cambio questo (che sono intenzione di fare comunque).
Quello che dovrò fare per risolvere questo problema è creare una proprietà NameTranslated
, che sarà la versione Class B
della proprietà Name
e, con molta attenzione, cambiare ogni riferimento con la proprietà Name
derivata a usa la mia nuova proprietà NameTranslated
. Tuttavia, ottenere persino uno di questi riferimenti errati potrebbe far esplodere l'intera applicazione.
Dato che il codebase non ha test unitari attorno ad esso, questo è piuttosto vicino ad essere lo scenario più pericoloso che uno sviluppatore possa affrontare. Se non cambio la violazione, devo spendere enormi quantità di energia mentale per tenere traccia di quale tipo di oggetto viene utilizzato in ogni metodo e se risolvo la violazione potrei far esplodere l'intero prodotto in un momento inopportuno.