Domanda

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

È stato utile?

Soluzione 2

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

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

Fondamentalmente LSP riguarda le interfacce e i contratti, nonché come decidere quando estendere una classe o quando estenderla.usa un'altra strategia come la composizione per raggiungere il tuo obiettivo.

Il modo più efficace che ho visto per illustrare questo punto è stato in Testa prima OOA&D.Presentano uno scenario in cui sei uno sviluppatore di un progetto per costruire un framework per giochi di strategia.

Presentano una classe che rappresenta una scheda simile a questa:

Class Diagram

Tutti i metodi accettano le coordinate X e Y come parametri per individuare la posizione della tessera nella matrice bidimensionale di Tiles.Ciò consentirà allo sviluppatore del gioco di gestire le unità nel tabellone durante il corso del gioco.

Il libro prosegue modificando i requisiti affermando che la struttura del gioco deve supportare anche i tabelloni di gioco 3D per accogliere i giochi che hanno il volo.Quindi a ThreeDBoard viene introdotta la classe che si estende Board.

A prima vista sembra una buona decisione. Board fornisce sia il Height E Width proprietà e ThreeDBoard fornisce l'asse Z.

Il punto in cui si interrompe è quando guardi tutti gli altri membri ereditati Board.I metodi per AddUnit, GetTile, GetUnits e così via, accettano tutti i parametri X e Y nel file Board classe ma il ThreeDBoard necessita anche di un parametro Z.

Quindi è necessario implementare nuovamente questi metodi con un parametro Z.Il parametro Z non ha alcun contesto per il Board classe e i metodi ereditati da Board classe perdono il loro significato.Un'unità di codice che tenta di utilizzare il file ThreeDBoard class come classe base Board sarebbe davvero sfortunato.

Forse dovremmo trovare un altro approccio.Invece di estendere Board, ThreeDBoard dovrebbe essere composto da Board oggetti.Uno Board oggetto per unità dell'asse Z.

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

Altri suggerimenti

Un ottimo esempio che illustra l'LSP (fornito da zio Bob in un podcast che ho ascoltato di recente) è stato come a volte qualcosa che suona bene nel linguaggio naturale non funziona del tutto nel codice.

In matematica, a Square è un Rectangle.In effetti è una specializzazione di un rettangolo.Il "is a" ti fa venire voglia di modellarlo con l'ereditarietà.Tuttavia, se nel codice hai creato Square derivano dalla Rectangle, poi un Square dovrebbe essere utilizzabile ovunque ti aspetti a Rectangle.Questo crea alcuni comportamenti strani.

Immagina di averlo fatto SetWidth E SetHeight metodi sul tuo Rectangle classe base;questo sembra perfettamente logico.Tuttavia se il tuo Rectangle il riferimento indicava a Square, Poi SetWidth E SetHeight non ha senso perché l'impostazione di uno modificherebbe l'altro per adattarlo.In questo caso Square fallisce il test di sostituzione di Liskov con Rectangle e l'astrazione dell'avere Square ereditare da Rectangle è brutto.

enter image description here

Dovreste dare un'occhiata all'altro inestimabile Poster motivazionali sui principi SOLID.

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 anche se l'interfaccia corrisponde.Il motivo è che abbiamo violato gli invarianti derivanti dalla definizione matematica di quadrati e rettangoli.Il modo in cui funzionano getter e setter, a Rectangle dovrebbe 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 dovere essere violato da una corretta implementazione di Square, pertanto non è un valido sostituto di Rectangle.

La sostituibilità è un principio della programmazione orientata agli oggetti secondo cui, 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 Struzzo è un sottotipo della classe Uccello, ma non può usare il metodo mosca, 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{} 

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

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

Un semplice esempio di violazione dell'LSP

Una delle violazioni più evidenti di questo principio è l'uso delle informazioni di tipo runt-time C ++ (RTTI) per selezionare una funzione basata sul tipo di oggetto.cioè.:

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 il DrawShape la funzione è mal formata.Deve sapere su ogni possibile derivato del Shape classe, e deve essere cambiato ogni volta che nuovi derivati ​​di Shape sono creati.In effetti, molti vedono la struttura di questa funzione come un anatema per la progettazione orientata agli oggetti.

Quadrato e rettangolo, una violazione più sottile.

Tuttavia, esistono altri modi, molto più subdoli, per violare l’LSP.Considera un'applicazione che utilizza il file Rectangle Classe 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 a tutti gli effetti.Poiché vale la relazione ISA, è logico modellare la relazione ISA Squareclasse come derivata da Rectangle. [...]

Square erediterà il SetWidth E SetHeight funzioni.Queste funzioni sono assolutamente inappropriate per a Square, poiché la larghezza e l'altezza di un quadrato sono identiche.Questo dovrebbe essere un indizio significativo che c'è un problema con il design.Tuttavia, c'è un modo per eludere il problema.Potremmo ignorare SetWidth E SetHeight [...]

Consideriamo però la seguente funzione:

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

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

[...]

LSP è necessario laddove parte del codice ritiene di chiamare i metodi di un tipo T, e potrebbe inconsapevolmente chiamare i metodi di un tipo S, Dove S extends T (cioè. S eredita, deriva da o è un sottotipo del supertipo T).

Ciò si verifica, ad esempio, quando è presente una funzione con un parametro di input di tipo T, si chiama (cioèrichiamato) con un valore di 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 (ad es.invarianti) per metodi di tipo T (per esempio. Rectangle), non essere violato quando i metodi di tipo S (per esempio. Square) vengono invece chiamati.

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 es.IL immutabile Gli incastonatori di rettangoli si aspettano che le dimensioni vengano modificate in modo indipendente, ma il file immutabile Gli incastonatori quadrati 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 S deve avere parametri di input controvarianti e un output covariante.

Controvariante significa che la varianza è contraria alla direzione dell'eredità, cioèIl tipo Si, di ciascun parametro di input di ciascun metodo del sottotipo S, deve essere uguale o a supertipo del tipo Ti del corrispondente parametro di input del corrispondente metodo del supertipo T.

Covarianza significa che la varianza è nella stessa direzione dell'eredità, cioèIl tipo So, dell'output di ciascun metodo del sottotipo S, deve essere uguale o a sottotipo del tipo To dell'output corrispondente del metodo corrispondente del supertipo T.

Questo perché se il chiamante pensa di avere un tipo T, pensa di chiamare un metodo di T, quindi fornisce argomenti di tipo Ti e assegna l'output al tipo To.Quando in realtà sta chiamando il metodo corrispondente di S, poi ciascuno Ti l'argomento di input è assegnato a a Si parametro di input e il So l'uscita è assegnata al tipo To.Quindi se Si non erano controvarianti rispetto aA Ti, quindi un sottotipo Xi-che non sarebbe un sottotipo di Si-potrebbe essere assegnato a Ti.

Inoltre, per le lingue (ad es.Scala o Ceylon) che hanno annotazioni sulla varianza del sito di definizione sui parametri del polimorfismo del tipo (ad es.generics), la co- o contro-direzione dell'annotazione della varianza per ogni parametro di tipo del tipo T deve essere opposto o stessa direzione rispettivamente ad ogni parametro di input o output (di ogni metodo di T) che ha il tipo del parametro type.

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


La sottotipizzazione è appropriata dove si possono enumerare gli invarianti.

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

Stato tipo (vedi pagina 3) dichiara e applica invarianti di stato ortogonali al tipo.In alternativa, gli invarianti possono essere applicati da convertire 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 tris può essere un altro esempio di utilizzo della tipizzazione per imporre invarianti in fase di compilazione.Il sistema dei tipi può anche essere completo di Turing, ad es. Scala.I linguaggi tipizzati in modo dipendente e i dimostratori di teoremi formalizzano i modelli di tipizzazione di ordine superiore.

A causa della necessità che la semantica astratto sull’estensione, mi aspetto che l'utilizzo della digitazione per modellare gli invarianti, ad es.semantica denotazionale unificata di ordine superiore, è superiore allo stato tipografico.Per “estensione” si intende la composizione illimitata e permutata di uno sviluppo modulare e non coordinato.Perché mi sembra che l’antitesi dell’unificazione e quindi dei gradi di libertà sia avere due modelli mutuamente dipendenti (ad es.tipi e Typestate) per esprimere la semantica condivisa, che non possono essere unificati tra loro per una composizione estensibile.Per esempio, Problema di espressioneL'estensione simile è stata unificata nei domini di sottotipizzazione, sovraccarico di funzioni e tipizzazione parametrica.

La mia posizione teorica è quella per conoscenza per esistere (vedi sezione “La centralizzazione è cieca e inadeguata”), ci sarà Mai essere un modello generale in grado di imporre una copertura del 100% di tutti i possibili invarianti in un linguaggio informatico completo di Turing.Perché esista la conoscenza, esistono molte possibilità inaspettate, ad es.il disordine e l’entropia devono essere sempre in aumento.Questa è la forza entropica.Dimostrare tutti i possibili calcoli di un'estensione potenziale significa calcolare a priori tutte le possibili estensioni.

Questo è il motivo per cui esiste il Teorema dell'Arresto, cioèè indecidibile se ogni possibile programma in un linguaggio di programmazione completo di Turing termina.Si può dimostrare che qualche programma specifico termina (quello di cui tutte le possibilità sono state definite e calcolate).Ma è impossibile dimostrare che ogni possibile estensione di quel programma termini, a meno che le possibilità di estensione di quel programma non siano Turing complete (ad es.tramite tipizzazione dipendente).Poiché il requisito fondamentale per la completezza di Turing è ricorsione illimitata, è intuitivo comprendere come i teoremi di incompletezza di Gödel e il paradosso di Russell si applichino 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.
  • Il paradosso di Russell:ogni regola di appartenenza per un insieme che può contenere un insieme, enumera il tipo specifico di ciascun membro oppure contiene se stessa.Pertanto gli insiemi non possono essere estesi oppure sono ricorsioni illimitati.Ad esempio, l'insieme di tutto ciò che non è una teiera, comprende sé stesso, che comprende sé stesso, che comprende sé stesso, ecc….Pertanto una regola è incoerente se (può contenere un insieme e) non enumera i tipi specifici (ad es.consente tutti i tipi non specificati) e non consente l'estensione illimitata.Questo è l'insieme degli insiemi che non sono membri di se stessi.Questa incapacità di essere sia coerente che completamente enumerabile su ogni estensione possibile, sono i teoremi di incompletezza di Gödel.
  • Principio di sostituzione di Liskov:generalmente è un problema indecidibile se un qualsiasi insieme sia sottoinsieme di un altro, cioèl'ereditarietà è generalmente indecidibile.
  • Riferimento a Linsky:è indecidibile quale sia il computo di qualcosa, quando viene descritto o percepito, cioèla percezione (la realtà) non ha un punto di riferimento assoluto.
  • Il teorema di Coase:non esiste un punto di riferimento esterno, quindi qualsiasi barriera alle possibilità esterne illimitate verrà meno.
  • Seconda legge della termodinamica:l’intero universo (un sistema chiuso, cioètutto) tende al massimo disordine, cioèmassime possibilità indipendenti.

La LSP è una regola relativa al contratto delle clasi:se una classe base soddisfa un contratto, allora anche le classi derivate dall'LSP devono soddisfare quel 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 si chiama Foo su un oggetto Derived, si ottengono esattamente gli stessi risultati della chiamata Foo su un oggetto Base, purché arg sia lo stesso.

Le funzioni che utilizzano puntatori o riferimenti a classi 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 fosse inteso in senso molto stretto, equiparandolo essenzialmente all'implementazione dell'interfaccia e al casting sicuro per i tipi.Ciò significherebbe che l'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.

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

Tornando ancora 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 ostacolare le funzionalità che ThreeDBoard intende aggiungere.

Con queste conoscenze in mano, valutare l’aderenza all’LSP può essere un ottimo strumento per determinare quando la composizione è il meccanismo più appropriato per estendere le funzionalità esistenti, piuttosto che l’ereditarietà.

Esiste una lista di controllo per determinare se stai violando o meno Liskov.

  • Se violi uno dei seguenti elementi -> violi Liskov.
  • Se non ne violi nessuno -> non puoi concludere nulla.

Lista di controllo:

  • Nessuna nuova eccezione dovrebbe essere generata nella classe derivata:Se la tua classe base lanciava ArgumentNullException, le tue sottoclassi potevano lanciare solo eccezioni di tipo ArgumentNullException o qualsiasi eccezione derivata da ArgumentNullException.Lanciare IndexOutOfRangeException è una violazione di Liskov.
  • Le precondizioni non possono essere rafforzate:Supponiamo che la tua classe base funzioni con un membro int.Ora il tuo sottotipo richiede che l'int sia positivo.Si tratta di precondizioni rafforzate e ora qualsiasi codice che prima funzionava perfettamente con numeri interi negativi viene interrotto.
  • Le post-condizioni non possono essere indebolite:Supponiamo che la classe base richieda che tutte le connessioni al database vengano chiuse prima che il metodo venga restituito.Nella tua sottoclasse hai sovrascritto quel metodo e hai lasciato la connessione aperta 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 nascosti per qualche tempo nella classe base e l'unico modo per rivelarli è leggere il codice della classe base.Fondamentalmente devi essere sicuro che quando esegui l'override di un metodo, qualsiasi cosa immutabile debba rimanere invariata dopo l'esecuzione del metodo sovrascritto.La cosa migliore che mi viene in mente è applicare questi vincoli invarianti nella classe base, ma non sarebbe facile.
  • Vincolo storico:Quando si sovrascrive un metodo non è consentito modificare una proprietà non modificabile nella classe base.Dai un'occhiata a questo codice e puoi vedere che Name è definito come non modificabile (set privato) ma SubType introduce un nuovo metodo che consente di modificarlo (attraverso 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);
        }
    }
    

Ci sono altri 2 articoli: Controvarianza degli argomenti del metodo E Covarianza dei tipi di rendimento.Ma non è possibile in C# (sono uno sviluppatore C#), quindi non mi interessano.

Riferimento:

Un importante esempio di utilizzo di LSP è presente test del software.

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

Per testare completamente la sottoclasse A, probabilmente dovrò aggiungere qualche altro caso di test, ma come minimo posso riutilizzare tutti i casi di test della superclasse B.

Un modo per realizzarlo è costruire quella che McGregor chiama una "gerarchia parallela per i test":Mio ATest la classe erediterà da BTest.È quindi necessaria una qualche forma di iniezione per garantire che il caso di test funzioni con oggetti di tipo A anziché di tipo B (andrà bene un semplice modello di metodo modello).

Si noti che riutilizzare la suite di super-test per tutte le implementazioni di sottoclassi è in realtà un modo per testare che queste implementazioni di sottoclassi siano conformi a LSP.Quindi si può anche sostenere quella cosa 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?"

Immagino che tutti abbiano coperto cosa sia tecnicamente LSP:Fondamentalmente vuoi essere in grado di astrarre dai dettagli del sottotipo e utilizzare i supertipi in modo sicuro.

Quindi Liskov ha 3 regole sottostanti:

  1. Regola della firma:Dovrebbe esserci un'implementazione valida di ogni operazione del supertipo nel sottotipo sintatticamente.Qualcosa che un compilatore sarà in grado di verificare per te.Esiste una piccola regola per generare meno eccezioni ed essere accessibili almeno quanto i metodi supertipo.

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

    • Precondizioni 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 delle proprietà:Questo 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 ha qualcosa a che fare con l'immutabilità o con il tipo di stati in cui può trovarsi l'oggetto.O forse l'oggetto cresce e non si riduce mai, quindi i metodi del sottotipo non dovrebbero farcela.

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

Se queste tre cose vengono curate, ti sei allontanato dalle cose sottostanti e stai scrivendo codice liberamente accoppiato.

Fonte:Sviluppo di programmi in Java - Barbara Liskov

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

Diciamo che hai un base Repository degli elementi.

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

        return $numberOfDeletedRows;
    }
}

E una sottoclasse che lo 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;
    }
}

Allora potresti avere un Cliente lavorare con l'API Base ItemsRepository e fare affidamento su di essa.

/**
 * 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();
    }
} 

IL LSP è rotto quando sostituendo genitore classe con a la sottoclasse interrompe 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 imparare di più sulla scrittura di software manutenibile nel mio corso: https://www.udemy.com/enterprise-php/

Questa formulazione del 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 a o2, allora S è un sottotipo di T.

Ciò significa sostanzialmente che S è un'altra implementazione completamente incapsulata della stessa identica cosa di T.E potrei essere audace e decidere che la performance è parte del comportamento di P...

Quindi, in sostanza, qualsiasi utilizzo del late-binding viola l'LSP.Lo scopo principale dell'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.

Qualche aggiunta:
Mi chiedo perché nessuno abbia scritto sull'Invariant, sulle precondizioni e sulle condizioni post della classe base a cui devono obbedire le classi derivate.Affinché una classe derivata D sia completamente sostituibile dalla classe Base B, la classe D deve obbedire a determinate condizioni:

  • Le varianti interne della classe base devono essere preservate dalla classe derivata
  • Le precondizioni 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 consapevole delle tre condizioni sopra imposte dalla classe base.Pertanto, le regole del sottotipo sono predeterminate.Ciò significa che la relazione "IS A" deve essere rispettata solo quando il sottotipo rispetta determinate regole.Queste regole, sotto forma di invarianti, precodizioni e postcondizioni, dovrebbero essere decise da un processo formalecontratto di progettazione'.

Ulteriori discussioni su questo argomento sono disponibili sul mio blog: Principio di sostituzione di Liskov

In una frase molto semplice possiamo dire:

La classe figlia non deve violare le caratteristiche della classe base.Deve esserne capace.Possiamo dire che è uguale al sottotipo.

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

Mi piacerebbe mostrare come è possibile conformarsi all'LSP con un esempio del mondo 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 progetto è conforme all'LSP perché il comportamento rimane invariato indipendentemente dall'implementazione che scegliamo di utilizzare.

E sì, puoi violare LSP in questa configurazione apportando 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 utilizzati allo stesso modo poiché non producono più lo stesso risultato.

Principio di sostituzione di Liskov (LSP)

Per tutto il tempo progettiamo un modulo di programma e creiamo alcune gerarchie di classe.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 nei moduli del programma esistenti.

Il principio di sostituzione di Liskov afferma che se un modulo di programma utilizza una classe di 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 in cui viene violato il Principio di Sostituzione di Liskov.Nell'esempio vengono utilizzate 2 classi:Rettangolo e quadrato.Supponiamo che l'oggetto Rectangle venga utilizzato da qualche parte nell'applicazione.Estendiamo l'applicazione e aggiungiamo la classe Square.La classe quadrata viene restituita da un pattern factory, 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 aperto aperto e significa che dobbiamo assicurarci che le nuove classi derivate stiano estendendo le classi di base senza cambiare il loro comportamento.

Guarda anche: Principio di apertura e chiusura

Alcuni concetti simili per una migliore struttura: Convenzione sulla configurazione

L'implementazione di ThreeDBoard in termini di array di Board sarebbe utile?

Forse potresti voler trattare le porzioni di ThreeDBoard su vari piani come una tavola.In tal caso potresti voler astrarre un'interfaccia (o una classe astratta) per Board per consentire più implementazioni.

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

Un quadrato è un rettangolo in cui la larghezza è uguale all'altezza.Se il quadrato imposta due dimensioni diverse per larghezza e altezza viola l'invariante del quadrato.Questo problema viene 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 altezza == larghezza;una precondizione più forte (e che viola lsp).Ciò dimostra che sebbene il quadrato sia un rettangolo non è un sottotipo valido perché la precondizione è rafforzata.La soluzione (in generale una cosa negativa) causa un effetto collaterale e questo indebolisce la post condizione (che viola lsp).setWidth sulla base ha la condizione post 0 <larghezza.Il derivato lo indebolisce con altezza == larghezza.

Pertanto un quadrato ridimensionabile non è un rettangolo ridimensionabile.

Diciamo che utilizziamo un rettangolo nel nostro codice

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

Nella nostra lezione di geometria abbiamo imparato che un quadrato è un tipo speciale di rettangolo perché la sua larghezza è pari alla lunghezza della sua altezza.Facciamo un Square lezione anche sulla base di queste informazioni:

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

Se sostituiamo il Rectangle con Square nel 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 Square ha una nuova precondizione che non avevamo in Rectangle classe: width == height.Secondo LSP il Rectangle le istanze dovrebbero essere sostituibili con Rectangle istanze di sottoclassi.Questo perché queste istanze superano il controllo del tipo Rectangle istanze e quindi causeranno errori imprevisti nel codice.

Questo è stato un esempio per il "le precondizioni non possono essere rafforzate in un sottotipo" parte nel articolo wiki.Quindi, per riassumere, la violazione dell'LSP probabilmente causerà errori nel codice ad un certo punto.

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 c'è nessun problema qui, giusto?Un'auto è sicuramente un mezzo 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!*/
}

Ora non tutto sta andando come previsto!Sì, la bicicletta è un mezzo 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 addirittura non può essere implementata.

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

Possiamo rifattorizzare 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 i dispositivi motorizzati.Qui è più appropriato aggiungere l'oggetto Engine.

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

   void startEngine() { ... }
}

Pertanto la nostra classe 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() { ... }
}

Ti incoraggio a leggere l'articolo: Violazione del principio di sostituzione di Liskov (LSP).

Qui puoi trovare una spiegazione di cos'è il principio di sostituzione di Liskov, indizi generali che ti aiutano a indovinare se lo hai già violato e un esempio di approccio che ti aiuterà a rendere più sicura la tua gerarchia di classi.

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 introdurre errori nel sistema o modificare il comportamento della classe base " da Qui.L'articolo fornisce un esempio di codice per violare l'LSP e risolverlo.

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 interrompere né il client né l'implementazione. È questo principio che consente di soddisfare i requisiti che si verificano in futuro, anche se non possiamo Non prevederli oggi.

Se scolleghiamo il computer dalla presa a muro (Implementazione), né la presa a muro (Interfaccia) né il computer (Client) si guastano (infatti, se si tratta di un computer portatile, può funzionare anche con le batterie per un periodo di tempo) .Con il software, tuttavia, il cliente spesso si aspetta che un servizio sia disponibile.Se il servizio è stato rimosso, otteniamo una NullReferenceException.Per affrontare questo tipo di situazione, possiamo creare un'implementazione di un'interfaccia che non fa "nulla". Questo è un modello di design noto come oggetto null, [4] e corrisponde approssimativamente a scollegare il computer dal muro.Poiché utilizziamo un accoppiamento lento, possiamo sostituire un'implementazione reale con qualcosa che non fa nulla senza causare problemi.

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

Scopo: i tipi derivati ​​devono essere completamente sostituibili ai relativi tipi di base.

Esempio: tipi di restituzione co-variante in Java.

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

Le classi figlie non dovrebbero mai infrangere le definizioni di tipo della classe genitore.

e l'esempio seguente aiuta a comprendere meglio l'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();
}

Correzione 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();
}

Fammi provare, considera un'interfaccia:

interface Planet{
}

Questo è implementato dalla classe:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Utilizzerai la Terra come:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Consideriamo ora un'altra classe che estende la Terra:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

Ora, secondo LSP, dovresti essere in grado di utilizzare LiveablePlanet al posto di Earth e questo non dovrebbe danneggiare il tuo sistema.Come:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Esempi tratti da Qui

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

[..] per comprendere alcuni principi è importante rendersi conto quando sono stati violati.Questo è quello che farò adesso.

Cosa significa la violazione di questo principio?Ciò implica che un oggetto non adempie al contratto imposto da un’astrazione espressa con un’interfaccia.In altre parole, significa che hai identificato erroneamente le tue astrazioni.

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);
    }
}

Si tratta di una violazione dell'LSP?SÌ.Questo perché il contratto del conto ci dice che un conto verrebbe ritirato, ma non è sempre così.Quindi, cosa dovrei fare per risolverlo?Modifico semplicemente 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 spesso impone al cliente la capacità di distinguere tra gli oggetti concreti utilizzati.Ad esempio, dato il contratto del primo 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 [cioè il requisito del prelievo di denaro.Perché non si sa 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 verificare se è così hasEnoughMoney() - che non fa parte di un'interfaccia.Quindi questo controllo forzato dipendente dalla classe del calcestruzzo è una violazione OCP].

Questo punto affronta anche un malinteso che incontro abbastanza spesso sulla violazione dell'LSP.Dice che "se il comportamento di un genitore è cambiato in un bambino, allora viola LSP". Tuttavia, non è, purché un bambino non viola il contratto dei suoi genitori.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top