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


27

Stavo seguendo questa domanda molto votata su una possibile violazione del principio di sostituzione di Liskov. So qual è il principio di sostituzione di Liskov, ma ciò che non è ancora chiaro nella mia mente è ciò che potrebbe andare storto se io come sviluppatore non penso al principio durante la scrittura di codice orientato agli oggetti.


6
Cosa può andare storto se non segui LSP? Scenario peggiore: finisci per evocare Code-thulhu! ;)
FrustratedWithFormsDesigner,

1
Come autore di quella domanda originale, devo aggiungere che si trattava piuttosto di una domanda accademica. Sebbene le violazioni possano causare errori nel codice, non ho mai avuto un bug grave o un problema di manutenzione che possa essere ricondotto a una violazione di LSP.
Paul T Davies,

2
@Paul Quindi non hai mai avuto problemi con i tuoi programmi a causa delle contorte gerarchie OO (che non hai progettato tu stesso, ma forse hai dovuto estendere) in cui i contratti erano rotti a destra ea sinistra da persone che erano incerte sullo scopo della classe base iniziare con? Ti invidio! :)
Andres F.

@PaulTDavies la gravità delle conseguenze dipende dal fatto che gli utenti (programmatori che usano la libreria) abbiano una conoscenza dettagliata dell'implementazione della libreria (cioè abbiano accesso e familiarità con il codice della libreria.) Alla fine gli utenti effettueranno dozzine di controlli condizionali o costruiranno wrapper intorno alla libreria per tenere conto di non LSP (comportamento specifico della classe). Lo scenario peggiore si verificherebbe se la libreria è un prodotto commerciale di origine chiusa.
rwong

@Andres e rwong, ti preghiamo di illustrare quei problemi con una risposta. La risposta accettata supporta praticamente Paul Davies in quanto le conseguenze sembrano minori (un'eccezione) che verranno rapidamente notate e corrette se si dispone di un buon compilatore, analizzatore statico o un test unitario minimo.
user949300,

Risposte:


31

Penso che sia stato espresso molto bene in quella domanda che è uno dei motivi che è stato votato così tanto.

Ora quando si chiama Close () su un'attività, è possibile che la chiamata fallisca se si tratta di un ProjectTask con lo stato avviato, quando non lo sarebbe se si trattasse di un'attività di base.

Immagina se vuoi:

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

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

A causa di violazioni della sostituzione di liskov, il codice che utilizza il tuo tipo dovrà avere una conoscenza esplicita del funzionamento interno dei tipi derivati ​​per trattarli in modo diverso. Questo codice strettamente associato e in genere rende l'implementazione più difficile da utilizzare in modo coerente.


Ciò significa che una classe figlio non può avere i propri metodi pubblici che non sono dichiarati nella classe genitore?
Songo,

@Songo: Non necessariamente: può, ma quei metodi sono "irraggiungibili" da un puntatore di base (o riferimento o variabile o qualunque sia il linguaggio che usi lo chiama) e hai bisogno di alcune informazioni sul tipo di runtime per interrogare il tipo di oggetto prima di poter chiamare quelle funzioni. Ma questa è una questione fortemente correlata alla sintassi e alla semantica delle lingue.
Emilio Garavaglia,

2
No. Questo è quando si fa riferimento a una classe figlio come se fosse un tipo di classe genitore, nel qual caso i membri che non sono dichiarati nella classe genitore sono inaccessibili.
Chewy Gumball,

1
@Phil Yep; questa è la definizione di accoppiamento stretto: cambiare una cosa provoca cambiamenti ad altre cose. A una classe debolmente accoppiata può essere modificata l'implementazione senza richiedere la modifica del codice al di fuori di essa. Ecco perché i contratti sono buoni, ti guidano su come non richiedere modifiche ai consumatori dei tuoi oggetti: rispetta il contratto e i consumatori non avranno bisogno di modifiche, quindi si ottiene un accoppiamento lento. Quando i tuoi consumatori hanno bisogno di codificare per la tua implementazione piuttosto che per il tuo contratto, questo è un accoppiamento stretto, e richiesto quando viola LSP.
Jimmy Hoffa,

1
@ user949300 Il successo di qualsiasi software per realizzare il suo lavoro non è una misura della sua qualità, costi a lungo termine o a breve termine. I principi di progettazione sono tentativi di realizzare linee guida per ridurre i costi a lungo termine del software, non per far "funzionare" il software. Le persone possono seguire tutti i principi che desiderano pur non riuscendo a implementare una soluzione funzionante o seguirne nessuna e implementare una soluzione funzionante. Sebbene le raccolte java possano funzionare per molte persone, ciò non significa che il costo per lavorare con loro a lungo termine sia il più economico possibile.
Jimmy Hoffa il

13

Se non si adempie al contratto che è stato definito nella classe base, le cose possono fallire silenziosamente quando si ottengono risultati non validi.

LSP negli stati di Wikipedia

  • Le condizioni preliminari 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 dovesse essere valido, il chiamante potrebbe ottenere un risultato che non si aspetta.


1
Riesci a pensare a qualche esempio concreto per dimostrarlo?
Mark Booth,

1
@MarkBooth Il problema circle-ellipse / square-rectangle potrebbe essere utile per dimostrarlo; l'articolo di Wikipedia è un buon punto di partenza: en.wikipedia.org/wiki/Circle-ellipse_problem
Ed Hastings,

7

Prendi in considerazione un caso classico dagli annali delle domande del colloquio: hai ricavato Circle da Ellipse. Perché? Perché un cerchio è un'ellisse IS-AN, ovviamente!

Tranne ... ellisse ha due funzioni:

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

Chiaramente, questi devono essere ridefiniti per Circle, perché un Circle 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 delle lingue OO non supporta la seconda, e per una buona ragione: sarebbe sorprendente scoprire che la tua cerchia non è più una cerchia. Quindi la prima opzione è la migliore. Ma considera la seguente funzione:

some_function(Ellipse byref e)

Immagina che some_function chiama e.set_alpha_radius. Ma poiché e era davvero un cerchio, sorprendentemente ha anche il suo raggio beta impostato.

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


1
Penso che potresti incorrere in guai se usi oggetti mutabili. Un cerchio è anche un'ellisse. Ma se si sostituisce un'ellisse che è anche un cerchio con un'altra ellisse (che è ciò che si sta facendo usando un metodo setter) non c'è garanzia che anche la nuova ellisse sarà un cerchio (i cerchi sono un sottoinsieme proprio di ellissi).
Giorgio,

2
In un mondo puramente funzionale (con oggetti immutabili), il metodo set_alpha_radius (d) avrebbe un'ellisse di tipo restituito (sia nell'ellisse che nella classe del cerchio).
Giorgio,

@Giorgio Sì, avrei dovuto menzionare che questo problema si verifica solo con oggetti mutabili.
Kaz Dragon,

@KazDragon: Perché qualcuno dovrebbe sostituire un'ellisse con un oggetto cerchio quando sappiamo che un'ellisse NON È un cerchio? Se qualcuno lo fa, non hanno una comprensione corretta delle entità che stanno cercando di modellare. Consentendo questa sostituzione, non stiamo incoraggiando una chiara comprensione del sistema sottostante che stiamo cercando di modellare nel nostro software e creando così un software cattivo in effetti?
Maverick,

@maverick Credo che tu abbia letto la relazione che ho descritto al contrario. La relazione is-a proposta è il contrario: un cerchio è un'ellisse. In particolare, un cerchio è un'ellisse in cui i raggi alfa e beta sono identici. E così, l'aspettativa potrebbe essere che qualsiasi funzione che si aspetta un'ellisse come parametro possa ugualmente prendere un cerchio. Considera calcola_area (Ellisse). Passare un cerchio a quello produrrebbe lo stesso risultato. Ma il problema è che il comportamento delle funzioni di mutazione di Ellipse non è sostituibile a quello di Circle.
Kaz Dragon,

6

Nelle parole di laici:

Il tuo codice avrà moltissime clausole CASE / 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 è così scalabile e gestibile come dovrebbe essere.

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

Non è necessario modificare il tuo iPod perché hai acquistato una nuova coppia di altoparlanti esterni, poiché sia ​​i vecchi che i nuovi altoparlanti esterni rispettano la stessa interfaccia, sono intercambiabili senza che l'iPod abbia perso la funzionalità desiderata.


2
-1: risposta negativa
tuttofare

3
@Thomas Non sono d'accordo. È una buona analogia. Parla di non infrangere le aspettative, ed è di questo che parla LSP. (anche se la parte su case / switch è un po 'debole, sono d'accordo)
Andres F.

2
E poi Apple ha rotto LSP cambiando i connettori. Questa risposta sopravvive.
Magus,

Non capisco cosa hanno a che fare le istruzioni switch con LSP. se ti riferisci al passaggio typeof(someObject)per decidere cosa sei "autorizzato a fare", allora certo, ma questo è un altro anti-pattern.
Sara,

Una drastica riduzione della quantità di istruzioni switch è un effetto collaterale desiderabile di LSP. Poiché gli oggetti possono rappresentare qualsiasi altro oggetto che estende la stessa interfaccia, non è necessario occuparsi di casi speciali.
Tulains Córdova,

1

per dare un esempio di vita reale con UndoManager di java

eredita dal AbstractUndoableEditcui contratto si specifica che ha 2 stati (annullati e rifatti) e può passare tra loro con singole chiamate a undo()eredo()

tuttavia UndoManager ha più stati e si comporta come un buffer di annullamento (ogni chiamata undoannulla alcune ma non tutte le modifiche, indebolendo la postcondizione)

questo porta all'ipotetica situazione in cui si aggiunge un UndoManager a CompoundEdit prima di chiamare, end()quindi chiamare Annulla su quel CompoundEdit lo porterà a chiamare undo()su ogni modifica una volta che le modifiche sono state parzialmente annullate

Ho fatto il mio UndoManagerper evitarlo (probabilmente dovrei rinominarlo UndoBufferperò)


1

Esempio: stai lavorando con un framework UI e crei il tuo controllo UI personalizzato eseguendo la sottoclasse della Controlclasse base. La Controlclasse base definisce un metodo getSubControls()che dovrebbe restituire una raccolta di controlli nidificati (se presenti). Ma si ignora il metodo per restituire effettivamente un elenco di date di nascita dei presidenti degli Stati Uniti.

Quindi cosa può andare storto con questo? È ovvio che il rendering del controllo fallirà, poiché non si restituisce un elenco di controlli come previsto. Molto probabilmente l'interfaccia utente si arresterà in modo anomalo. Stai infrangendo il contratto a cui ci si aspetta che le sottoclassi di Controllo aderiscano.


0

Puoi anche guardarlo dal punto di vista della modellazione. Quando si dice che un'istanza di classe Aè anche un'istanza di classe, Bsi implica che "il comportamento osservabile di un'istanza di classe Apuò anche essere classificato come comportamento osservabile di un'istanza di classe B" (Ciò è possibile solo se la classe Bè meno specifica di classe A.)

Quindi, la violazione di LSP significa che c'è qualche contraddizione nel tuo design: stai definendo alcune categorie per i tuoi oggetti e poi non le stai rispettando nella tua implementazione, qualcosa deve essere sbagliato.

Come fare una scatola con un tag: "Questa scatola contiene solo palline blu", e poi lanciarvi una palla rossa. A che serve un tale tag se mostra informazioni errate?


0

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 Ae Class Bcondividi un gruppo di proprietà che hanno Class Ala precedenza sulla propria implementazione. L'impostazione o il recupero di una Class Aproprietà 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 Nameviene 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 Ae Class B, quindi non posso semplicemente fare un Class Aabstract per costringere le persone a usare Class B.

Ci sono alcune funzioni di utilità molto utili che operano su Class Ae 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 Asu Class B. Molte delle funzioni che prendono a Class Bpotrebbero facilmente prendere un Class Ase 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 NameTranslatedproprietà, che sarà la Class Bversione della Nameproprietà e cambierà molto, molto attentamente ogni riferimento alla Nameproprietà derivata per usare la mia nuova NameTranslatedproprietà. 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.


Cosa accadrebbe se all'interno della classe derivata oscurassi la proprietà ereditata con un diverso tipo di cose che aveva lo stesso nome [ad esempio una classe nidificata] e creasse nuovi identificatori BaseNamee TranslatedNameaccedesse sia allo stile di classe A Nameche al significato di classe B? Quindi qualsiasi tentativo di accesso Namea una variabile di tipo Bverrebbe rifiutato con un errore del compilatore, in modo da poter garantire che tutti i riferimenti siano stati convertiti in uno degli altri moduli.
supercat il

Non lavoro più in quel posto. Sarebbe stato molto scomodo da risolvere. :-)
Stephen,

-4

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


1
Questo apre solo più domande anziché essere una risposta.
Frank,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.