Cosa può andare storto se viene violato il principio di sostituzione di Liskov?

26

Stavo seguendo questa domanda altamente votata sulla possibile violazione del principio di sostituzione di Liskov. So qual è il principio di sostituzione di Liskov, ma quello che non è ancora chiaro nella mia mente è cosa potrebbe andare storto se io come sviluppatore non penso al principio mentre scrivo codice orientato agli oggetti.

    
posta Geek 17.10.2012 - 17:04
fonte

9 risposte

30

Penso che sia affermato molto bene in quella domanda, che è uno dei motivi per cui è stato votato così bene.

Now when calling Close() on a Task, there is a chance the call will fail if it is a ProjectTask with the started status, when it wouldn't if it was a base Task.

Immagina se vuoi:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

In questo metodo, occasionalmente la chiamata .Close () esploderà, quindi ora, in base all'implementazione concreta di un tipo derivato, devi cambiare il modo in cui questo metodo si comporta da come questo metodo verrebbe scritto se Task non avesse sottotipi che potrebbe essere consegnato a questo metodo.

A causa delle violazioni della sostituzione di liskov, il codice che usa il tuo tipo dovrà avere una conoscenza esplicita del funzionamento interno dei tipi derivati per trattarli in modo diverso. Questo codice coppia strettamente e solo in generale rende l'implementazione più difficile da usare in modo coerente.

    
risposta data 17.10.2012 - 17:12
fonte
13

Se non rispetti il contratto che è stato definito nella classe base, le cose possono fallire silenziosamente quando ottieni risultati spenti.

LSP negli stati di wikipedia

  • Le precondizioni non possono essere rafforzate in un sottotipo.
  • Le postcondizioni non possono essere indebolite in un sottotipo.
  • Gli invarianti del supertipo devono essere conservati in un sottotipo.

Se qualcuno di questi non fosse valido, il chiamante potrebbe ottenere un risultato che non si aspetta.

    
risposta data 17.10.2012 - 17:09
fonte
7

Considera un caso classico degli annali delle domande dell'intervista: hai derivato Circle da Ellipse. Perché? Perché un cerchio è un'ellisse IS-AN, ovviamente!

Tranne ... l'ellisse ha due funzioni:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

Chiaramente, questi devono essere ridefiniti per Cerchio, perché un Cerchio ha un raggio uniforme. Hai due possibilità:

  1. Dopo aver chiamato set_alpha_radius o set_beta_radius, entrambi sono impostati sullo stesso importo.
  2. Dopo aver chiamato set_alpha_radius o set_beta_radius, l'oggetto non è più un Cerchio.

La maggior parte dei linguaggi OO non supporta il secondo, e per una buona ragione: sarebbe sorprendente scoprire che il tuo Cerchio non è più un Cerchio. Quindi la prima opzione è la migliore. Ma considera la seguente funzione:

some_function(Ellipse byref e)

Immagina che qualche_funzione chiama e.set_alpha_radius. Ma siccome era veramente un Cerchio, sorprendentemente ha impostato anche il suo raggio beta.

E qui sta il principio di sostituzione: una sottoclasse deve essere sostituibile per una superclasse. Altrimenti succederanno cose sorprendenti.

    
risposta data 18.10.2012 - 14:59
fonte
5

In parole semplici:

Il tuo codice avrà un sacco di CASE / clausole switch dappertutto.

Ognuna di queste clausole CASE / switch avrà bisogno di nuovi casi aggiunti di volta in volta, il che significa che la base di codice non è scalabile e mantenibile come dovrebbe essere.

LSP consente al codice di funzionare più come l'hardware:

Non devi modificare il tuo iPod perché hai acquistato un nuovo paio di altoparlanti esterni, poiché sia i vecchi che i nuovi altoparlanti esterni rispettano la stessa interfaccia, sono intercambiabili senza che l'iPod perda le funzionalità desiderate.

    
risposta data 17.10.2012 - 17:39
fonte
1

per dare un esempio di vita reale con UndoManager di java

eredita da AbstractUndoableEdit il cui contratto specifica che ha 2 stati (annullato e rifatto) e può passare da uno all'altro con chiamate singole a undo() e redo()

tuttavia UndoManager ha più stati e agisce come un buffer di annullamento (ogni chiamata a undo annulla alcune modifiche ma non tutte , indebolendo la postcondizione)

questo porta all'ipotetica situazione in cui aggiungi un UndoManager a un CompoundEdit prima di chiamare end() , quindi chiamare undo su quel CompoundEdit lo condurrà a chiamare undo() su ogni modifica una volta che le tue modifiche saranno parzialmente annullate

Ho fatto rotolare il mio UndoManager per evitarlo (probabilmente dovrei rinominarlo in UndoBuffer )

    
risposta data 17.10.2012 - 21:25
fonte
1

Esempio: stai lavorando con un framework dell'interfaccia utente e crei il tuo controllo dell'interfaccia utente personalizzato sottoclassando la classe base Control . La classe base Control definisce un metodo getSubControls() che dovrebbe restituire una raccolta di controlli annidati (se presenti). Ma tu sostituisci il metodo per restituire effettivamente un elenco di date di nascita dei presidenti degli Stati Uniti.

Quindi cosa può andare storto in questo? È ovvio che il rendering del controllo fallirà, dal momento che non si restituisce un elenco di controlli come previsto. Molto probabilmente l'interfaccia utente si bloccherà. stai violando il contratto a cui sottostanno le sottoclassi di Control.

    
risposta data 03.06.2015 - 13:49
fonte
0

Puoi anche guardarlo dal punto di vista della modellazione. Quando dici che un'istanza di classe A è anche un'istanza di classe B implichi che "il comportamento osservabile di un'istanza di classe A può anche essere classificato come comportamento osservabile di un'istanza di classe B "(Questo è possibile solo se la classe B è meno specifica della classe A .)

Quindi, violare l'LSP significa che c'è qualche contraddizione nel tuo progetto: stai definendo alcune categorie per i tuoi oggetti e quindi non li stai rispettando nella tua implementazione, qualcosa deve essere sbagliato.

Come fare una scatola con un tag: "Questa scatola contiene solo palle blu", e poi lanciare una palla rossa dentro di essa. Qual è l'uso di tale tag se mostra le informazioni sbagliate?

    
risposta data 24.10.2012 - 17:19
fonte
0

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.

    
risposta data 19.07.2013 - 07:05
fonte
-4

Se vuoi sentire il problema di violare LSP, pensa cosa succede se hai solo .dll / .jar della classe base (nessun codice sorgente) e devi creare una nuova classe derivata. Non puoi mai completare questa attività.

    
risposta data 17.10.2012 - 21:52
fonte