Qual è un esempio del principio di sostituzione di Liskov?


908

Ho sentito che il principio di sostituzione di Liskov (LSP) è un principio fondamentale della progettazione orientata agli oggetti. Che cos'è e quali sono alcuni esempi del suo utilizzo?


Altri esempi di aderenza e violazione di LSP qui
StuartLC,

1
Questa domanda ha infinitamente molte buone risposte ed è quindi troppo ampia .
Raedwald,

Risposte:


892

Un ottimo esempio che illustra LSP (fornito da Zio Bob in un podcast che ho ascoltato di recente) è stato il modo in cui a volte qualcosa che suona nel linguaggio naturale non funziona perfettamente nel codice.

In matematica, a Squareè a Rectangle. In effetti è una specializzazione di un rettangolo. La "è una" ti fa venire voglia di modellare questo con eredità. Tuttavia, se nel codice da cui Squarederivate Rectangle, allora Squaredovrebbe essere utilizzabile ovunque ci si aspetti a Rectangle. Questo rende alcuni comportamenti strani.

Immagina di avere metodi SetWidthe SetHeightmetodi nella tua Rectangleclasse base; questo sembra perfettamente logico. Tuttavia, se il tuo Rectangleriferimento indicava a Square, allora SetWidthe SetHeightnon ha senso perché impostarne uno cambierebbe l'altro per adattarlo. In questo caso Squarefallisce il test di sostituzione di Liskov con Rectanglee l'astrazione di aver Squareereditato da Rectangleè una cattiva.

inserisci qui la descrizione dell'immagine

Dovresti dare un'occhiata agli altri inestimabili poster motivazionali sui principi solidi .


19
@ m-sharp Cosa succede se si tratta di un rettangolo immutabile tale che invece di SetWidth e SetHeight, abbiamo invece i metodi GetWidth e GetHeight?
Pacerier,

140
Morale della storia: modella le tue classi in base a comportamenti e non a proprietà; modella i tuoi dati in base alle proprietà e non ai comportamenti. Se si comporta come un'anatra, è sicuramente un uccello.
Sklivvz,

193
Bene, un quadrato È chiaramente un tipo di rettangolo nel mondo reale. Se possiamo modellarlo nel nostro codice dipende dalle specifiche. Ciò che indica LSP è che il comportamento del sottotipo deve corrispondere al comportamento del tipo di base come definito nelle specifiche del tipo di base. Se le specifiche del tipo di base del rettangolo indicano che l'altezza e la larghezza possono essere impostate in modo indipendente, allora LSP afferma che il quadrato non può essere un sottotipo di rettangolo. Se le specifiche del rettangolo indicano che un rettangolo è immutabile, un quadrato può essere un sottotipo di rettangolo. Si tratta di sottotipi che mantengono il comportamento specificato per il tipo di base.
SteveT

63
@Pacerier non c'è problema se è immutabile. Il vero problema qui è che non stiamo modellando rettangoli, ma piuttosto "rettangoli rimodellabili", cioè rettangoli la cui larghezza o altezza possono essere modificati dopo la creazione (e lo consideriamo ancora come lo stesso oggetto). Se osserviamo la classe rettangolo in questo modo, è chiaro che un quadrato non è un "rettangolo rimodellabile", perché un quadrato non può essere rimodellato ed essere comunque un quadrato (in generale). Matematicamente, non vediamo il problema perché la mutabilità non ha nemmeno senso in un contesto matematico.
asmeurer

14
Ho una domanda sul principio. Perché sarebbe il problema se Square.setWidth(int width)fosse implementato in questo modo this.width = width; this.height = width;:? In questo caso è garantito che la larghezza è uguale all'altezza.
MC Emperor

488

Il principio di sostituzione di Liskov (LSP, ) è un concetto di programmazione orientata agli oggetti che afferma:

Le funzioni che utilizzano puntatori o riferimenti a classi di base devono essere in grado di utilizzare oggetti di classi derivate senza saperlo.

Fondamentalmente LSP riguarda interfacce e contratti, oltre a come decidere quando estendere una classe anziché utilizzare un'altra strategia come la composizione per raggiungere il tuo obiettivo.

Il modo più efficace che ho visto per illustrare questo punto era in Head First OOA & D . Presentano uno scenario in cui sei uno sviluppatore di un progetto per creare un framework per giochi di strategia.

Presentano una classe che rappresenta una tavola simile a questa:

Diagramma di classe

Tutti i metodi prendono le coordinate X e Y come parametri per localizzare la posizione della piastrella nella matrice bidimensionale di Tiles. Ciò consentirà a uno sviluppatore di giochi di gestire le unità nel tabellone durante il corso del gioco.

Il libro continua a cambiare i requisiti per dire che il lavoro del frame di gioco deve supportare anche le schede di gioco 3D per ospitare i giochi che hanno il volo. Quindi ThreeDBoardviene introdotta una classe che si estende Board.

A prima vista sembra una buona decisione. Boardfornisce sia la Heighte Widthproprietà e ThreeDBoardfornisce l'asse Z.

Il punto in cui si rompe è quando guardi tutti gli altri membri ereditati Board. I metodi per AddUnit, GetTile, GetUnitse così via, tutti prendono entrambi i parametri X e Y nella Boardclasse ma il ThreeDBoardbisogno di un parametro Z pure.

Quindi è necessario implementare nuovamente questi metodi con un parametro Z. Il parametro Z non ha contesto per la Boardclasse e i metodi ereditati dalla Boardclasse perdono il loro significato. Un'unità di codice che tenta di utilizzare la ThreeDBoardclasse come classe base Boardsarebbe molto sfortunata.

Forse dovremmo trovare un altro approccio. Invece di estenderlo Board, ThreeDBoarddovrebbe essere composto da Boardoggetti. Un Boardoggetto per unità dell'asse Z.

Questo ci consente di utilizzare buoni principi orientati agli oggetti come l'incapsulamento e il riutilizzo e non viola LSP.


10
Vedi anche Problema Circle-Ellipse su Wikipedia per un esempio simile ma più semplice.
Brian,

Requote di @NotMySelf: "Penso che l'esempio sia semplicemente quello di dimostrare che l'ereditarietà dalla scheda non ha senso nel contesto di ThreeDBoard e che tutte le firme del metodo sono prive di significato con un asse Z".
Contango,

1
Quindi, se aggiungessimo un altro metodo a una classe Child ma tutte le funzionalità di Parent abbiano ancora senso nella classe Child, infrangerebbe LSP? Dato che da un lato abbiamo modificato l'interfaccia per usare un po 'il figlio dall'altro se avessimo scelto il figlio come genitore, il codice che prevede che un genitore funzionerebbe bene.
Nickolay Kondratyev,

5
Questo è un esempio anti-Liskov. Liskov ci fa derivare il Rettangolo dalla Piazza. Classe più parametri da classe meno parametri. E hai ben dimostrato che è male. È davvero una buona battuta aver segnato come una risposta ed essere stato votato 200 volte una risposta anti-liskov per la domanda di liskov. Il principio di Liskov è davvero un errore?
Gangnus,

3
Ho visto l'eredità funzionare nel modo sbagliato. Ecco un esempio La classe base dovrebbe essere 3DBoard e la classe derivata Board. La
scacchiera

169

La sostituibilità è un principio nella programmazione orientata agli oggetti che afferma che, in un programma per computer, se S è un sottotipo di T, allora gli oggetti di tipo T possono essere sostituiti con oggetti di tipo S

facciamo un semplice esempio in Java:

Cattivo esempio

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

L'anatra può volare perché è un uccello, ma che dire di questo:

public class Ostrich extends Bird{}

Lo struzzo è un uccello, ma non può volare, la classe di struzzo è un sottotipo di classe uccello, ma non può usare il metodo fly, ciò significa che stiamo infrangendo il principio LSP.

Buon esempio

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

3
Bell'esempio, ma cosa faresti se il client lo avesse Bird bird. Devi lanciare l'oggetto su FlyingBirds per usare fly, che non è carino vero?
Moody,

17
No. Se il client ha Bird bird, ciò significa che non può utilizzare fly(). Questo è tutto. Passare a Ducknon cambia questo fatto. Se il client ha FlyingBirds bird, quindi anche se viene superato Duck, dovrebbe sempre funzionare allo stesso modo.
Steve Chamaillard,

9
Questo non servirebbe anche da buon esempio per la segregazione dell'interfaccia?
Saharsh

Eccellente esempio Grazie
Abdelhadi Abdo,

6
Che ne dici di usare l'interfaccia 'Flyable' (non riesco a pensare a un nome migliore). In questo modo non ci impegniamo in questa rigida gerarchia ... A meno che non ne sappiamo davvero bisogno.
Terzo

132

LSP riguarda gli invarianti.

L'esempio classico è dato dalla seguente dichiarazione di pseudo-codice (implementazioni omesse):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Ora abbiamo un problema nonostante l'interfaccia corrisponda. Il motivo è che abbiamo violato gli invarianti derivanti dalla definizione matematica di quadrati e rettangoli. Il modo in cui funzionano getter e setter Rectangledovrebbe soddisfare il seguente invariante:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Tuttavia, questo invariante deve essere violato da una corretta implementazione di Square, pertanto non è un valido sostituto di Rectangle.


35
E quindi la difficoltà di usare "OO" per modellare tutto ciò che potremmo voler effettivamente modellare.
DrPizza,

9
@DrPizza: Assolutamente. Tuttavia, due cose. In primo luogo, tali relazioni possono ancora essere modellate in OOP, anche se in modo incompleto o utilizzando deviazioni più complesse (scegli quale si adatta al tuo problema). In secondo luogo, non esiste alternativa migliore. Altre mappature / modellazioni hanno problemi uguali o simili. ;-)
Konrad Rudolph il

7
@NickW In alcuni casi (ma non in quanto sopra) puoi semplicemente invertire la catena di ereditarietà - logicamente parlando, un punto 2D è un punto 3D, in cui la terza dimensione viene ignorata (o 0 - tutti i punti si trovano sullo stesso piano in Spazio 3D). Ma questo ovviamente non è molto pratico. In generale, questo è uno dei casi in cui l'ereditarietà non aiuta davvero e non esiste alcuna relazione naturale tra le entità. Modellali separatamente (almeno non conosco un modo migliore).
Konrad Rudolph,

7
OOP ha lo scopo di modellare i comportamenti e non i dati. Le tue classi violano l'incapsulamento anche prima di violare LSP.
Sklivvz,

2
@AustinWBryan Yep; più tempo lavoro in questo campo, più tendo a usare l'ereditarietà solo per interfacce e classi di base astratte e composizione per il resto. A volte è un po 'più di lavoro (digitando saggiamente) ma evita un sacco di problemi ed è ampiamente echeggiato dai consigli di altri programmatori esperti.
Konrad Rudolph,

77

Robert Martin ha un eccellente articolo sul principio di sostituzione di Liskov . Discute i modi sottili e non così sottili in cui il principio può essere violato.

Alcune parti rilevanti del documento (si noti che il secondo esempio è fortemente condensato):

Un semplice esempio di violazione di LSP

Una delle violazioni più eclatanti di questo principio è l'uso delle informazioni di tipo run-time (RTTI) C ++ per selezionare una funzione basata sul tipo di un oggetto. vale a dire:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Chiaramente la DrawShapefunzione è mal formata. Deve conoscere ogni possibile derivato della Shapeclasse e deve essere modificato ogni volta che Shapevengono creati nuovi derivati ​​di . In effetti, molti vedono la struttura di questa funzione come un anatema del design orientato agli oggetti.

Quadrato e rettangolo, una violazione più sottile.

Tuttavia, ci sono altri modi, molto più sottili, di violare l'LSP. Considera un'applicazione che utilizza la Rectangleclasse come descritto di seguito:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Immagina che un giorno gli utenti richiedano la possibilità di manipolare i quadrati oltre ai rettangoli. [...]

Chiaramente, un quadrato è un rettangolo per tutti gli scopi e scopi normali. Poiché la relazione ISA è valida, è logico modellare la Square classe come derivata Rectangle. [...]

Squareerediterà le funzioni SetWidthe SetHeight. Queste funzioni sono assolutamente inadeguate per a Square, poiché la larghezza e l'altezza di un quadrato sono identiche. Questo dovrebbe essere un indizio significativo che ci sia un problema con il design. Tuttavia, esiste un modo per eludere il problema. Potremmo scavalcare SetWidthe SetHeight[...]

Ma considera la seguente funzione:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Se passiamo un riferimento a un Squareoggetto in questa funzione, l' Squareoggetto verrà danneggiato perché l'altezza non verrà modificata. Questa è una chiara violazione di LSP. La funzione non funziona per i derivati ​​dei suoi argomenti.

[...]


14
Molto tardi, ma ho pensato che questa fosse una citazione interessante in quel documento: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. se una pre-condizione di classe figlio è più forte di una pre-condizione di classe genitore, non si può sostituire un figlio con un genitore senza violare la pre-condizione. Da qui LSP.
user2023861

@ user2023861 Hai perfettamente ragione. Scriverò una risposta basata su questo.
inf3rno,

40

LSP è necessario quando un codice pensa che stia chiamando i metodi di un tipo Te può inconsapevolmente chiamare i metodi di un tipo S, dove S extends T(cioè Seredita, deriva o è un sottotipo del supertipo T).

Ad esempio, ciò si verifica quando una funzione con un parametro di input di tipo Tviene chiamata (ovvero invocata) con un valore argomento di tipo S. Oppure, dove un identificatore di tipo T, viene assegnato un valore di tipo S.

val id : T = new S() // id thinks it's a T, but is a S

LSP richiede le aspettative (es. Invarianti) per i metodi di tipo T(es. Rectangle), Non essere violati quando si chiamano invece i metodi di tipo S(es. Square).

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Anche un tipo con campi immutabili ha ancora invarianti, ad esempio i setter immutabili Rectangle si aspettano che le dimensioni vengano modificate indipendentemente, ma i setter Square immutabili violano questa aspettativa.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP richiede che ciascun metodo del sottotipo sia Sdotato di parametri di input contraddittori e di un output covariante.

Contravveniente significa che la varianza è contraria alla direzione dell'ereditarietà, ovvero il tipo Si, di ciascun parametro di input di ciascun metodo del sottotipo S, deve essere uguale o un supertipo del tipo Tidel parametro di input corrispondente del metodo corrispondente del supertipo T.

Covarianza significa che la varianza è nella stessa direzione dell'ereditarietà, ovvero il tipo So, dell'output di ciascun metodo del sottotipo S, deve essere lo stesso o un sottotipo del tipo Todell'output corrispondente del metodo corrispondente del supertipo T.

Questo perché se il chiamante pensa di avere un tipo T, pensa di chiamare un metodo T, quindi fornisce argomenti di tipo Tie assegna l'output al tipo To. Quando in realtà chiama il metodo corrispondente di S, ogni Tiargomento di input viene assegnato a un Siparametro di input e l' Sooutput viene assegnato al tipo To. Quindi, se Sinon fossero contagiosi Ti, allora potrebbe essere assegnato un sottotipo Xi- che non sarebbe un sottotipo di Si- Ti.

Inoltre, per le lingue (ad es. Scala o Ceylon) che hanno annotazioni di varianza del sito di definizione sui parametri del polimorfismo del tipo (cioè generici), la co-o contro-direzione dell'annotazione di varianza per ciascun parametro di tipo del tipo Tdeve essere opposta o nella stessa direzione rispettivamente per ogni parametro di input o output (di ogni metodo di T) che ha il tipo del parametro type.

Inoltre, per ogni parametro di input o output che ha un tipo di funzione, la direzione della varianza richiesta viene invertita. Questa regola viene applicata in modo ricorsivo.


Il sottotipo è appropriato laddove è possibile elencare gli invarianti.

Sono in corso molte ricerche su come modellare gli invarianti, in modo che vengano applicati dal compilatore.

Typestate (vedi pagina 3) dichiara e impone invarianti di stato ortogonali al tipo. In alternativa, gli invarianti possono essere applicati convertendo le asserzioni in tipi . Ad esempio, per affermare che un file è aperto prima di chiuderlo, File.open () potrebbe restituire un tipo OpenFile, che contiene un metodo close () che non è disponibile in File. Un API tic-tac-toe può essere un altro esempio di impiegare tipizzazione di applicare invarianti a tempo di compilazione. Il sistema di tipi può persino essere completo di Turing, ad esempio Scala . I linguaggi e i dimostratori di teoremi tipicamente dipendenti formalizzano i modelli di tipizzazione di ordine superiore.

A causa della necessità che la semantica si astragga sull'estensione , mi aspetto che l'impiego della tipizzazione per modellare gli invarianti, ovvero la semantica denotazionale unificata di ordine superiore, sia superiore al Typestate. "Estensione" indica la composizione illimitata e permutata di uno sviluppo modulare non coordinato. Perché mi sembra essere l'antitesi dell'unificazione e quindi dei gradi di libertà, avere due modelli reciprocamente dipendenti (ad esempio tipi e Typestate) per esprimere la semantica condivisa, che non possono essere unificati tra loro per una composizione estensibile . Ad esempio, l' estensione simile a Problema di espressione è stata unificata nei domini di sottotipo, sovraccarico di funzioni e digitazione parametrica.

La mia posizione teorica è che affinché la conoscenza esista (vedere la sezione "La centralizzazione è cieca e inadatta"), non ci sarà mai un modello generale in grado di imporre una copertura del 100% di tutti i possibili invarianti in un linguaggio informatico completo di Turing. Affinché la conoscenza esista, esistono molte possibilità inaspettate, cioè disordine ed entropia devono sempre aumentare. Questa è la forza entropica. Dimostrare tutti i possibili calcoli di una potenziale estensione, è calcolare a priori tutte le possibili estensioni.

Questo è il motivo per cui esiste il teorema di Halting, ovvero è indecifrabile la conclusione di ogni possibile programma in un linguaggio di programmazione completo di Turing. Si può dimostrare che alcuni programmi specifici terminano (uno in cui tutte le possibilità sono state definite e calcolate). Ma è impossibile dimostrare che tutte le possibili estensioni di quel programma terminano, a meno che le possibilità di estensione di quel programma non siano complete (ad esempio tramite la digitazione dipendente). Poiché il requisito fondamentale per la completezza di Turing è la ricorsione illimitata , è intuitivo capire come i teoremi di incompletezza di Gödel e il paradosso di Russell si applicano all'estensione.

Un'interpretazione di questi teoremi li incorpora in una comprensione concettuale generalizzata della forza entropica:

  • Teoremi di incompletezza di Gödel : qualsiasi teoria formale, in cui tutte le verità aritmetiche possono essere dimostrate, è incoerente.
  • Paradosso di Russell : ogni regola di appartenenza a un set che può contenere un set, enumera il tipo specifico di ciascun membro o contiene se stesso. Pertanto, gli insiemi non possono essere estesi o sono ricorsioni illimitate. Ad esempio, l'insieme di tutto ciò che non è una teiera, include se stesso, che include se stesso, che include se stesso, ecc…. Pertanto una regola è incoerente se (può contenere un set e) non enumera i tipi specifici (ovvero consente tutti i tipi non specificati) e non consente l'estensione illimitata. Questo è l'insieme di insiemi che non sono membri di se stessi. Questa incapacità di essere sia coerente che completamente elencata su tutte le possibili estensioni, sono i teoremi di incompletezza di Gödel.
  • Principio della sottostazione di Liskov : generalmente è un problema indecidibile se una serie è il sottoinsieme di un'altra, cioè l'eredità è generalmente indecidibile.
  • Riferimenti di Linsky : è indecidibile quale sia il calcolo di qualcosa, quando è descritto o percepito, cioè la percezione (realtà) non ha un punto di riferimento assoluto.
  • Teorema di Coase : non esiste un punto di riferimento esterno, quindi qualsiasi barriera a possibilità esterne illimitate fallirà.
  • Seconda legge della termodinamica : l'intero universo (un sistema chiuso, cioè tutto) tende al massimo disordine, cioè alle massime possibilità indipendenti.

17
@Shelyby: hai mescolato troppe cose. Le cose non sono così confuse come le dici tu. Gran parte delle tue asserzioni teoriche si basano su motivi fragili, come "Perché esista la conoscenza, esistono molte possibilità inaspettate, ........." E "in generale è un problema indecidibile se un insieme è il sottoinsieme di un altro, vale a dire l'eredità è generalmente indecidibile '. È possibile avviare un blog separato per ciascuno di questi punti. Comunque, le tue affermazioni e assunzioni sono altamente discutibili. Non si devono usare cose di cui non si è a conoscenza!
Aknon il

1
@aknon Ho un blog che spiega queste questioni in modo più approfondito. Il mio modello TOE di spaziotempo infinito sono frequenze illimitate. Non mi confonde che una funzione induttiva ricorsiva abbia un valore iniziale noto con un limite di fine infinito, o una funzione induttiva ha un valore finale sconosciuto e un limite di inizio noto. La relatività è il problema una volta introdotta la ricorsione. Questo è il motivo per cui Turing complete equivale a una ricorsione illimitata .
Shelby Moore III,

4
@ShelbyMooreIII Stai andando in troppe direzioni. Questa non è una risposta
Soldalma,

1
@Soldalma è una risposta. Non lo vedi nella sezione Risposta. Il tuo è un commento perché è nella sezione dei commenti.
Shelby Moore III,

1
Come il tuo mixaggio con scala world!
Ehsan M. Kermani,

24

Vedo rettangoli e quadrati in ogni risposta e come violare LSP.

Vorrei mostrare come l'LSP può essere conformato con un esempio reale:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Questo design è conforme a LSP perché il comportamento rimane invariato indipendentemente dall'implementazione che scegliamo di utilizzare.

E sì, puoi violare LSP in questa configurazione facendo una semplice modifica in questo modo:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Ora i sottotipi non possono essere usati allo stesso modo poiché non producono più lo stesso risultato.


6
L'esempio non viola LSP solo fintanto che limitiamo la semantica di Database::selectQuerysupportare solo il sottoinsieme di SQL supportato da tutti i motori DB. Non è pratico ... Detto questo, l'esempio è ancora più facile da comprendere rispetto alla maggior parte degli altri qui usati.
Palec,

5
Ho trovato questa risposta la più facile da capire dal resto.
Malcolm Salvador,

23

C'è una lista di controllo per determinare se stai violando Liskov.

  • Se si viola uno dei seguenti elementi -> si viola Liskov.
  • Se non violi alcun -> non posso concludere nulla.

Elenco di controllo:

  • Non devono essere generate nuove eccezioni nella classe derivata : se la classe base ha generato ArgumentNullException, le classi secondarie sono state autorizzate a generare eccezioni di tipo ArgumentNullException o eventuali eccezioni derivate da ArgumentNullException. Lanciare IndexOutOfRangeException è una violazione di Liskov.
  • Le condizioni preliminari non possono essere rafforzate : supponi che la tua classe di base funzioni con un membro int. Ora il tuo sottotipo richiede che int sia positivo. Questo è un prerequisito rafforzato, e ora qualsiasi codice che ha funzionato perfettamente prima con ints negativi è rotto.
  • Le post-condizioni non possono essere indebolite : supponiamo che la classe di base richiesta tutte le connessioni al database debbano essere chiuse prima che il metodo restituito. Nella tua sottoclasse hai annullato quel metodo e hai lasciato aperta la connessione per un ulteriore riutilizzo. Hai indebolito le post-condizioni di quel metodo.
  • Gli invarianti devono essere preservati : il vincolo più difficile e doloroso da soddisfare. Gli invarianti sono da tempo nascosti nella classe base e l'unico modo per rivelarli è leggere il codice della classe base. Fondamentalmente devi essere sicuro quando esegui l'override di un metodo qualsiasi cosa immutabile deve rimanere invariata dopo l'esecuzione del metodo override. La cosa migliore che mi viene in mente è di applicare questi vincoli invarianti nella classe base, ma non sarebbe facile.
  • Vincolo di cronologia : quando si esegue l'override di un metodo non è consentito modificare una proprietà non modificabile nella classe base. Dai un'occhiata a questo codice e puoi vedere Nome definito come non modificabile (set privato) ma Sottotipo introduce un nuovo metodo che consente di modificarlo (tramite la riflessione):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Vi sono altri 2 elementi: contraddizione tra argomenti di metodo e covarianza di tipi restituiti . Ma non è possibile in C # (sono uno sviluppatore C #), quindi non mi interessa.

Riferimento:


Sono anche uno sviluppatore C # e dirò che la tua ultima affermazione non è vera a partire da Visual Studio 2010, con il framework .Net 4.0. La covarianza dei tipi di ritorno consente un tipo di ritorno più derivato rispetto a quanto definito dall'interfaccia. Esempio: Esempio: IEnumerable <T> (T è covariante) IEnumerator <T> (T è covariante) IQueryable <T> (T è covariante) IGrouping <TKey, TElement> (TKey e TElement sono covariant) IComparer <T> (T è contravariant) IEqualityComparer <T> (T è controvariante) IComparable <T> (T è controvariante) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter

1
Risposta eccezionale e mirata (anche se le domande originali riguardavano esempi più che regole).
Mike,

22

L'SPL è una regola relativa al contratto delle classe: se una classe di base soddisfa un contratto, allora anche le classi derivate da LSP devono soddisfare tale contratto.

In pseudo-pitone

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

soddisfa LSP se ogni volta che chiami Foo su un oggetto Derivato, dà esattamente gli stessi risultati della chiamata Foo su un oggetto Base, purché arg sia lo stesso.


9
Ma ... se hai sempre lo stesso comportamento, che senso ha avere la classe derivata?
Leonid,

2
Hai perso un punto: è lo stesso comportamento osservato . Ad esempio, potresti sostituire qualcosa con prestazioni O (n) con qualcosa di funzionalmente equivalente, ma con prestazioni O (lg n). Oppure potresti sostituire qualcosa che accede ai dati implementati con MySQL e sostituirlo con un database in memoria.
Charlie Martin,

@Charlie Martin, codifica per un'interfaccia piuttosto che un'implementazione - lo scavo. Questo non è unico per OOP; anche i linguaggi funzionali come Clojure lo promuovono. Anche in termini di Java o C #, penso che usare un'interfaccia piuttosto che usare una classe astratta più le gerarchie di classi sarebbe naturale per gli esempi forniti. Python non è fortemente tipizzato e non ha davvero interfacce, almeno non esplicitamente. La mia difficoltà è che ho fatto OOP per diversi anni senza aderire a SOLID. Ora che l'ho trovato, sembra limitante e quasi contraddittorio.
Hamish Grubijan,

Bene, devi tornare indietro e controllare il documento originale di Barbara. reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Non è realmente dichiarato in termini di interfacce, ed è una relazione logica che mantiene (o non) in alcun linguaggio di programmazione che ha una qualche forma di eredità.
Charlie Martin,

1
@HamishGrubijan Non so chi ti abbia detto che Python non è fortemente tipizzato, ma ti stavano mentendo (e se non mi credi, avvia un interprete Python e prova 2 + "2"). Forse confondi "fortemente tipizzato" con "tipicamente statico"?
asmeurer,

21

Per farla breve, lasciamo rettangoli rettangoli e quadrati quadrati, esempio pratico quando si estende una classe genitore, è necessario PRESERVARE l'API genitore esatta o ESPENDERLA.

Supponiamo che tu abbia un archivio articoli di base .

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

E una sottoclasse che la estende:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Quindi potresti avere un client che lavora con l'API del repository Items di base e che fa affidamento su di esso.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

L' LSP si interrompe quando la sostituzione della classe genitore con una sottoclasse infrange il contratto dell'API .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Puoi saperne di più sulla scrittura di software gestibile nel mio corso: https://www.udemy.com/enterprise-php/


20

Le funzioni che utilizzano puntatori o riferimenti a classi di base devono essere in grado di utilizzare oggetti di classi derivate senza saperlo.

Quando ho letto per la prima volta di LSP, ho pensato che ciò fosse inteso in un senso molto rigoroso, essenzialmente equiparandolo a interfacciamento dell'implementazione e cast sicuro. Ciò significherebbe che LSP è garantito o meno dalla lingua stessa. Ad esempio, in questo senso stretto, ThreeDBoard è sicuramente sostituibile a Board, per quanto riguarda il compilatore.

Dopo aver letto di più sul concetto, ho scoperto che LSP è generalmente interpretato in modo più ampio di così.

In breve, ciò che significa per il codice client "sapere" che l'oggetto dietro il puntatore è di un tipo derivato piuttosto che il tipo di puntatore non è limitato alla sicurezza del tipo. L'adesione a LSP è anche verificabile sondando il comportamento reale degli oggetti. Cioè, esaminando l'impatto dello stato di un oggetto e gli argomenti del metodo sui risultati delle chiamate al metodo o sui tipi di eccezioni generati dall'oggetto.

Tornando indietro all'esempio, in teoria i metodi Board possono essere fatti funzionare perfettamente su ThreeDBoard. In pratica, tuttavia, sarà molto difficile prevenire differenze di comportamento che il client potrebbe non gestire correttamente, senza compromettere la funzionalità che ThreeDBoard intende aggiungere.

Con queste conoscenze a portata di mano, la valutazione dell'adesione a LSP può essere un ottimo strumento per determinare quando la composizione è il meccanismo più appropriato per estendere la funzionalità esistente, piuttosto che l'ereditarietà.


19

Immagino che tutti abbiano trattato ciò che LSP è tecnicamente: in pratica vuoi essere in grado di astrarre dai dettagli dei sottotipi e usare i supertipi in modo sicuro.

Quindi Liskov ha 3 regole sottostanti:

  1. Firma Regola: dovrebbe esserci un'implementazione valida di ogni operazione del supertipo nel sottotipo in modo sintattico. Qualcosa che un compilatore sarà in grado di verificare per te. Vi è una piccola regola sul lancio di meno eccezioni e sull'essere almeno accessibili quanto i metodi del supertipo.

  2. Regola dei metodi: l'implementazione di tali operazioni è semanticamente corretta.

    • Presupposti più deboli: le funzioni del sottotipo dovrebbero prendere almeno quello che il supertipo ha preso come input, se non di più.
    • Postcondizioni più forti: dovrebbero produrre un sottoinsieme dell'output prodotto dai metodi supertipo.
  3. Regola proprietà: va oltre le singole chiamate di funzione.

    • Invarianti: le cose che sono sempre vere devono rimanere vere. Per esempio. la dimensione di un set non è mai negativa.
    • Proprietà evolutive: di solito qualcosa a che fare con l'immutabilità o il tipo di stati in cui può trovarsi l'oggetto. O forse l'oggetto cresce e non si restringe mai, quindi i metodi del sottotipo non dovrebbero farlo.

Tutte queste proprietà devono essere preservate e la funzionalità extra del sottotipo non dovrebbe violare le proprietà del supertipo.

Se queste tre cose vengono prese in considerazione, ti sei sottratto alle cose sottostanti e stai scrivendo un codice liberamente accoppiato.

Fonte: Sviluppo di programmi in Java - Barbara Liskov


18

Un esempio importante dell'uso di LSP è nei test del software .

Se ho una classe A che è una sottoclasse di B conforme a LSP, posso riutilizzare la suite di test di B per testare A.

Per testare completamente la sottoclasse A, probabilmente ho bisogno di aggiungere alcuni altri casi di test, ma come minimo posso riutilizzare tutti i casi di test della superclasse B.

Un modo per rendersene conto è quello di costruire quella che McGregor chiama una "gerarchia parallela per i test": la mia ATestclasse erediterà BTest. È quindi necessaria una forma di iniezione per garantire che il caso di test funzioni con oggetti di tipo A anziché di tipo B (funzionerà un modello di modello semplice).

Si noti che il riutilizzo della suite di super-test per tutte le implementazioni di sottoclassi è in realtà un modo per testare che queste implementazioni di sottoclassi sono conformi a LSP. Pertanto, si può anche sostenere che si dovrebbe eseguire la suite di test della superclasse nel contesto di qualsiasi sottoclasse.

Vedi anche la risposta alla domanda StackOverflow " Posso implementare una serie di test riutilizzabili per testare l'implementazione di un'interfaccia? "


14

Illustriamo in Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Non ci sono problemi qui, giusto? Un'auto è sicuramente un dispositivo di trasporto, e qui possiamo vedere che sovrascrive il metodo startEngine () della sua superclasse.

Aggiungiamo un altro dispositivo di trasporto:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Non tutto va come previsto ora! Sì, una bicicletta è un dispositivo di trasporto, tuttavia non ha un motore e quindi il metodo startEngine () non può essere implementato.

Questi sono i tipi di problemi a cui porta la violazione del principio di sostituzione di Liskov, e di solito possono essere riconosciuti con un metodo che non fa nulla, o che addirittura non può essere implementato.

La soluzione a questi problemi è una corretta gerarchia ereditaria e nel nostro caso risolveremmo il problema differenziando le classi di dispositivi di trasporto con e senza motori. Anche se una bicicletta è un dispositivo di trasporto, non ha un motore. In questo esempio la nostra definizione di dispositivo di trasporto è errata. Non dovrebbe avere un motore.

Possiamo riformattare la nostra classe TransportationDevice come segue:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Ora possiamo estendere TransportationDevice per dispositivi non motorizzati.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

Ed estendi TransportationDevice per dispositivi motorizzati. Qui è più appropriato aggiungere l'oggetto Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

In tal modo la nostra classe di auto diventa più specializzata, aderendo al principio di sostituzione di Liskov.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

E anche la nostra classe Bicycle è conforme al principio di sostituzione di Liskov.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

9

Questa formulazione dell'LSP è troppo forte:

Se per ogni oggetto o1 di tipo S esiste un oggetto o2 di tipo T tale che per tutti i programmi P definiti in termini di T, il comportamento di P rimane invariato quando o1 viene sostituito con o2, allora S è un sottotipo di T.

Il che significa sostanzialmente che S è un'altra implementazione completamente incapsulata della stessa identica cosa di T. E potrei essere audace e decidere che le prestazioni sono parte del comportamento di P ...

Quindi, fondamentalmente, qualsiasi uso di associazione tardiva viola l'LSP. L'obiettivo principale di OO è ottenere un comportamento diverso quando sostituiamo un oggetto di un tipo con uno di un altro tipo!

La formulazione citata da Wikipedia è migliore poiché la proprietà dipende dal contesto e non include necessariamente l'intero comportamento del programma.


2
Ehm, quella formulazione appartiene a Barbara Liskov. Barbara Liskov, "Estrazione dei dati e gerarchia", SIGPLAN Avvisi, 23,5 (maggio 1988). Non è "troppo forte", è "esattamente giusto", e non ha le implicazioni che pensi che abbia. È forte, ma ha la giusta quantità di forza.
DrPizza,

Quindi, ci sono pochissimi sottotipi nella vita reale :)
Damien Pollet l'

3
"Il comportamento è invariato" non significa che un sottotipo fornisca esattamente gli stessi valori di risultato concreti. Significa che il comportamento del sottotipo corrisponde a quanto previsto nel tipo di base. Esempio: il tipo base Shape potrebbe avere un metodo draw () e stabilire che questo metodo dovrebbe renderizzare la forma. Due sottotipi di Shape (ad esempio Square e Circle) implementerebbero entrambi il metodo draw () e i risultati sembrerebbero diversi. Ma fintanto che il comportamento (rendering della forma) corrispondesse al comportamento specificato di Forma, Quadrato e Cerchio sarebbero sottotipi di Forma in conformità con LSP.
SteveT

9

In una frase molto semplice, possiamo dire:

La classe figlio non deve violare le sue caratteristiche di classe base. Deve esserne capace. Possiamo dire che è lo stesso del sottotipo.


9

Principio di sostituzione di Liskov (LSP)

Per tutto il tempo progettiamo un modulo di programma e creiamo alcune gerarchie di classi. Quindi estendiamo alcune classi creando alcune classi derivate.

Dobbiamo assicurarci che le nuove classi derivate si estendano senza sostituire la funzionalità delle vecchie classi. Altrimenti, le nuove classi possono produrre effetti indesiderati quando vengono utilizzate in moduli di programma esistenti.

Il Principio di sostituzione di Liskov afferma che se un modulo di programma utilizza una classe Base, il riferimento alla classe Base può essere sostituito con una classe derivata senza influire sulla funzionalità del modulo del programma.

Esempio:

Di seguito è riportato il classico esempio per cui viene violato il Principio di sostituzione di Liskov. Nell'esempio vengono utilizzate 2 classi: Rettangolo e Quadrato. Supponiamo che l'oggetto Rectangle sia usato da qualche parte nell'applicazione. Estendiamo l'applicazione e aggiungiamo la classe Square. La classe quadrata viene restituita da un modello di fabbrica, basato su alcune condizioni e non sappiamo esattamente quale tipo di oggetto verrà restituito. Ma sappiamo che è un rettangolo. Otteniamo l'oggetto rettangolo, impostiamo la larghezza su 5 e l'altezza su 10 e otteniamo l'area. Per un rettangolo con larghezza 5 e altezza 10, l'area dovrebbe essere 50. Il risultato sarà invece 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Conclusione:

Questo principio è solo un'estensione del principio Open Close e significa che dobbiamo assicurarci che nuove classi derivate stiano estendendo le classi base senza modificarne il comportamento.

Vedi anche: Apri Chiudi Principio

Alcuni concetti simili per una migliore struttura: convenzione sulla configurazione


8

Il principio di sostituzione di Liskov

  • Il metodo ignorato non deve rimanere vuoto
  • Il metodo ignorato non dovrebbe generare un errore
  • Il comportamento della classe base o dell'interfaccia non dovrebbe essere modificato (rielaborato) come a causa dei comportamenti derivati ​​della classe.

7

Qualche addendum:
mi chiedo perché nessuno abbia scritto dell'Invariante, delle precondizioni e delle condizioni post della classe base che devono essere rispettate dalle classi derivate. Affinché una classe derivata D sia completamente sostituibile dalla classe B di base, la classe D deve rispettare determinate condizioni:

  • Le varianti In della classe base devono essere conservate dalla classe derivata
  • Le pre-condizioni della classe base non devono essere rafforzate dalla classe derivata
  • Le post-condizioni della classe base non devono essere indebolite dalla classe derivata.

Quindi il derivato deve essere a conoscenza delle tre condizioni precedenti imposte dalla classe base. Quindi, le regole del sottotipo sono pre-decise. Il che significa che la relazione "IS A" deve essere rispettata solo quando determinate regole sono rispettate dal sottotipo. Queste regole, sotto forma di invarianti, precondizioni e postcondizioni, dovrebbero essere decise da un " contratto di progettazione " formale .

Ulteriori discussioni su questo disponibili sul mio blog: principio di sostituzione di Liskov


6

L'LSP in termini semplici afferma che gli oggetti della stessa superclasse dovrebbero poter essere scambiati tra loro senza rompere nulla.

Ad esempio, se abbiamo Catuna Dogclasse e una derivata da una Animalclasse, qualsiasi funzione che utilizza la classe Animale dovrebbe essere in grado di usare Cato Dogcomportarsi normalmente.


4

L'implementazione di ThreeDBoard in termini di una serie di schede sarebbe così utile?

Forse potresti voler trattare fette di ThreeDBoard su vari piani come una scheda. In tal caso potresti voler estrarre un'interfaccia (o classe astratta) per Board per consentire implementazioni multiple.

In termini di interfaccia esterna, potresti voler fattorizzare un'interfaccia Board per TwoDBoard e ThreeDBoard (sebbene nessuno dei metodi sopra indicati sia adatto).


1
Penso che l'esempio sia semplicemente quello di dimostrare che l'ereditarietà dalla scheda non ha senso nel contesto di ThreeDBoard e che tutte le firme del metodo sono prive di significato con un asse Z.
NotMyself,

4

Un quadrato è un rettangolo in cui la larghezza è uguale all'altezza. Se il quadrato imposta due dimensioni diverse per la larghezza e l'altezza, viola il quadrato invariante. Questo è risolto introducendo effetti collaterali. Ma se il rettangolo avesse un setSize (altezza, larghezza) con precondizione 0 <altezza e 0 <larghezza. Il metodo del sottotipo derivato richiede height == width; una precondizione più forte (e che viola lsp). Ciò dimostra che sebbene il quadrato sia un rettangolo non è un sottotipo valido perché il prerequisito è rafforzato. Il lavoro intorno (in generale una cosa negativa) causa un effetto collaterale e questo indebolisce la condizione post (che viola lsp). setWidth sulla base ha condizione post 0 <larghezza. Il derivato lo indebolisce con altezza == larghezza.

Pertanto un quadrato ridimensionabile non è un rettangolo ridimensionabile.


4

Questo principio è stato introdotto da Barbara Liskov nel 1987 ed estende il principio Open-Closed concentrandosi sul comportamento di una superclasse e sui suoi sottotipi.

La sua importanza diventa evidente quando consideriamo le conseguenze della violazione. Considera un'applicazione che utilizza la seguente classe.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

Immagina che un giorno il cliente richieda la capacità di manipolare i quadrati oltre ai rettangoli. Poiché un quadrato è un rettangolo, la classe quadrata dovrebbe essere derivata dalla classe Rettangolo.

public class Square : Rectangle
{
} 

Tuttavia, facendo ciò incontreremo due problemi:

Un quadrato non ha bisogno di variabili sia in altezza che in larghezza ereditate dal rettangolo e questo potrebbe creare uno spreco significativo nella memoria se dobbiamo creare centinaia di migliaia di oggetti quadrati. Le proprietà del settatore di larghezza e altezza ereditate dal rettangolo non sono appropriate per un quadrato poiché la larghezza e l'altezza di un quadrato sono identiche. Per impostare sia l'altezza che la larghezza sullo stesso valore, possiamo creare due nuove proprietà come segue:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Ora, quando qualcuno imposterà la larghezza di un oggetto quadrato, la sua altezza cambierà di conseguenza e viceversa.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Andiamo avanti e consideriamo questa altra funzione:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

Se passiamo un riferimento a un oggetto quadrato in questa funzione, violeremmo l'LSP perché la funzione non funziona per le derivate dei suoi argomenti. Le proprietà larghezza e altezza non sono polimorfiche perché non sono dichiarate virtuali nel rettangolo (l'oggetto quadrato verrà danneggiato perché l'altezza non verrà modificata).

Tuttavia, dichiarando le proprietà del setter come virtuali dovremo affrontare un'altra violazione, l'OCP. In effetti, la creazione di un quadrato di classe derivato sta causando modifiche al rettangolo di classe base.


3

La spiegazione più chiara per LSP che ho trovato finora è stata "Il principio di sostituzione di Liskov afferma che l'oggetto di una classe derivata dovrebbe essere in grado di sostituire un oggetto della classe base senza portare alcun errore nel sistema o modificare il comportamento della classe base "da qui . L'articolo fornisce un esempio di codice per violare LSP e risolverlo.


1
Fornisci gli esempi di codice su StackOverflow.
sebenalern,

3

Diciamo che usiamo un rettangolo nel nostro codice

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

Nella nostra classe di geometria abbiamo appreso che un quadrato è un tipo speciale di rettangolo perché la sua larghezza ha la stessa lunghezza della sua altezza. Facciamo anche un corso Squarebasato su queste informazioni:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Se sostituiamo il Rectanglecon Squarenel nostro primo codice, allora si romperà:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Questo perché il Squareha un nuovo presupposto non abbiamo avuto nella Rectangleclasse: width == height. Secondo LSP le Rectangleistanze dovrebbero essere sostituibili con Rectangleistanze di sottoclasse. Questo perché queste istanze superano il controllo del tipo per le Rectangleistanze e quindi causeranno errori imprevisti nel codice.

Questo è stato un esempio per le "precondizioni non può essere rafforzata in un sottotipo" parte nel articolo wiki . Quindi, per riassumere, la violazione di LSP probabilmente causerà errori nel tuo codice ad un certo punto.


3

LSP afferma che "Gli oggetti dovrebbero essere sostituibili con i loro sottotipi". D'altra parte, questo principio indica

Le classi figlio non devono mai interrompere le definizioni del tipo della classe genitore.

e il seguente esempio aiuta ad avere una migliore comprensione di LSP.

Senza LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Riparazione tramite LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}


2

IL PRINCIPIO DI SOSTITUZIONE DI LISKOV (Dal libro di Mark Seemann) afferma che dovremmo essere in grado di sostituire un'implementazione di un'interfaccia con un'altra senza rompere né il client né l'implementazione. È questo principio che consente di soddisfare i requisiti che si verificano in futuro, anche se possiamo ' li prevedo oggi.

Se scolleghiamo il computer dal muro (implementazione), né la presa a muro (interfaccia) né il computer (client) si guastano (infatti, se si tratta di un computer portatile, può persino funzionare con le batterie per un periodo di tempo) . Con il software, tuttavia, un client si aspetta spesso che un servizio sia disponibile. Se il servizio è stato rimosso, otteniamo una NullReferenceException. Per far fronte a questo tipo di situazione, possiamo creare un'implementazione di un'interfaccia che non fa "nulla". Questo è un modello noto come Null Object, [4] e corrisponde approssimativamente allo scollegamento del computer dalla parete. Poiché stiamo usando un accoppiamento libero, possiamo sostituire un'implementazione reale con qualcosa che non fa nulla senza causare problemi.


2

Il Principio di sostituzione di Likov afferma che se un modulo di programma utilizza una classe Base, il riferimento alla classe Base può essere sostituito con una classe Derivata senza influire sulla funzionalità del modulo del programma.

Intento: i tipi derivati ​​devono essere completamente sostituibili ai loro tipi di base.

Esempio: tipi di restituzione di co-variante in java.


1

Ecco un estratto da questo post che chiarisce bene le cose:

[..] al fine di comprendere alcuni principi, è importante capire quando è stato violato. Questo è quello che farò ora.

Che cosa significa la violazione di questo principio? Implica che un oggetto non adempia al contratto imposto da un'astrazione espressa con un'interfaccia. In altre parole, significa che hai identificato le tue astrazioni in modo errato.

Considera il seguente esempio:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

È una violazione di LSP? Sì. Questo perché il contratto dell'account ci dice che un account verrebbe ritirato, ma non è sempre così. Quindi, cosa devo fare per risolverlo? Ho appena modificato il contratto:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, ora il contratto è soddisfatto.

Questa sottile violazione impone spesso a un cliente la capacità di distinguere tra oggetti concreti impiegati. Ad esempio, dato il primo contratto dell'Account, potrebbe essere simile al seguente:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

E questo viola automaticamente il principio aperto-chiuso [vale a dire, per il requisito di prelievo di denaro. Perché non sai mai cosa succede se un oggetto che viola il contratto non ha abbastanza soldi. Probabilmente non restituisce nulla, probabilmente verrà generata un'eccezione. Quindi devi controllare sehasEnoughMoney() - che non fa parte di un'interfaccia. Quindi questo controllo forzato dipendente dalla classe concreta è una violazione di OCP].

Questo punto affronta anche un malinteso che incontro abbastanza spesso sulla violazione di LSP. Dice che "se il comportamento di un genitore è cambiato in un bambino, allora viola LSP". Tuttavia, non lo è, a condizione che un bambino non violi il contratto dei genitori.

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.