Come verificare il principio di sostituzione di Liskov in una gerarchia ereditaria?


14

Ispirato da questa risposta:

Liskov principio di sostituzione richiede che

  • 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.
  • Vincolo storico (la "regola della storia"). Gli oggetti sono considerati modificabili solo attraverso i loro metodi (incapsulamento). Poiché i sottotipi possono introdurre metodi che non sono presenti nel supertipo, l'introduzione di questi metodi può consentire cambiamenti di stato nel sottotipo che non sono ammessi nel supertipo. Il vincolo storico lo proibisce.

Speravo che qualcuno pubblicasse una gerarchia di classi che viola questi 4 punti e come risolverli di conseguenza.
Sto cercando una spiegazione elaborata per scopi educativi su come identificare ciascuno dei 4 punti nella gerarchia e il modo migliore per risolverlo.

Nota:
speravo di pubblicare un esempio di codice su cui le persone lavorassero, ma la domanda stessa riguarda come identificare le gerarchie difettose :)


Ci sono altri esempi di violazioni di LSP nelle risposte a questa domanda SO
StuartLC

Risposte:


17

È molto più semplice di quanto la citazione faccia sembrare, precisa com'è.

Quando guardi una gerarchia ereditaria, immagina un metodo che riceve un oggetto della classe base. Ora chiediti, ci sono dei presupposti che qualcuno potrebbe modificare questo metodo e che non sarebbero validi per quella classe.

Ad esempio ( originariamente visto sul sito di zio Bob ):

public class Square : Rectangle
{
    public Square(double width) : base(width, width)
    {
    }

    public override double Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Width;
        }
    }

    public override double Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Height;
        }
    }
}

Sembra abbastanza giusto, vero? Ho creato un tipo di rettangolo specializzato chiamato Square, che sostiene che la larghezza deve essere sempre uguale all'altezza. Un quadrato è un rettangolo, quindi si adatta ai principi OO, non è vero?

Ma aspetta, cosa succede se qualcuno ora scrive questo metodo:

public void Enlarge(Rectangle rect, double factor)
{
    rect.Width *= factor;
    rect.Height *= factor;
}

Non fico. Ma non c'è motivo per cui l'autore di questo metodo avrebbe dovuto sapere che potrebbe esserci un potenziale problema.

Ogni volta che derivate una classe da un'altra, pensate alla classe base e a ciò che le persone potrebbero assumere al riguardo (come "ha una larghezza e un'altezza e sarebbero entrambe indipendenti"). Quindi pensa "quelle ipotesi rimangono valide nella mia sottoclasse?" In caso contrario, ripensare il tuo design.


Esempio molto buono e sottile. +1. Quello che potresti fare è rendere Enlarge un metodo della classe Rectangle e sovrascriverlo nella classe Square.
marco-fiset,

@ marco-fiset: preferirei vedere Square e Rectangle disaccoppiati, Square con una sola dimensione, ma ognuno implementabile IResizable. È vero che se esistesse un metodo Draw, sarebbero simili, ma preferirei che entrambi incapsulassero una classe RectangleDrawer, che include il codice comune.
pdr

1
Non penso che questo sia un buon esempio. Il problema è che un quadrato non ha larghezza o altezza. Ha solo una lunghezza dei suoi lati. Il problema non sarebbe lì se la larghezza e l'altezza fossero solo leggibili, ma in questo caso sono scrivibili. Quando si introduce lo stato modificabile, è sempre molto più difficile mantenere LSP.
SpaceTrucker,

@pdr Grazie per l'esempio, ma riguardo alle 4 condizioni che ho citato nel mio post quale parte della Squareclasse le viola?
Songo,

1
@Songo: è il vincolo della storia. Meglio spiegato qui: blackwasp.co.uk/LSP.aspx "Per loro natura, le sottoclassi includono tutti i metodi e le proprietà delle loro superclassi. Possono anche aggiungere altri membri. Il vincolo storico dice che i membri nuovi o modificati non dovrebbero modificare il stato di un oggetto in un modo che non sarebbe consentito dalla classe base . Ad esempio, se la classe base rappresenta un oggetto con una dimensione fissa, la sottoclasse non dovrebbe consentire la modifica di questa dimensione. "
pdr,
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.