Qual è un esempio del principio di sostituzione di Liskov?

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?

Un grande esempio che illustra LSP (dato da Uncle Bob in un podcast che ho sentito di recente) era il modo in cui a volte qualcosa che suoni nel linguaggio naturale non funziona abbastanza nel codice.

In matematica, un Square è un Rectangle . In effetti è una specializzazione di un rettangolo. Il “è un” ti fa desiderare di modellarlo con l’ereditarietà. Tuttavia se nel codice che hai creato Square deriva da Rectangle , allora un Square dovrebbe essere utilizzabile ovunque ti aspetti un Rectangle . Questo rende strano un comportamento.

Immagina di avere i metodi SetWidth e SetHeight sulla tua class base Rectangle ; questo sembra perfettamente logico. Tuttavia, se il tuo riferimento a Rectangle punta a un Square , SetWidth e SetHeight non hanno senso perché l’impostazione uno cambierebbe l’altro per abbinarlo. In questo caso Square fallisce il test di sostituzione di Liskov con Rectangle e l’astrazione di avere Square eredita da Rectangle è negativa.

Dovresti controllare gli altri inestimabili SOLID Principles Motivational Posters .

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

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

Al centro di tutto ciò, LSP riguarda le interfacce e i contratti, nonché il modo in cui decidere quando estendere una class e utilizzare un’altra strategia come la composizione per raggiungere il proprio objective.

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

Presentano una class 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 nell’array 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 frame del gioco funziona anche per supportare le tabs di gioco 3D per ospitare i giochi che hanno il volo. Quindi viene introdotta una class ThreeDBoard che estende Board .

A prima vista sembra una buona decisione. Board fornisce entrambe le proprietà Height e Width e ThreeDBoard fornisce l’asse Z.

Dove si rompe è quando guardi tutti gli altri membri ereditati da Board . I metodi per AddUnit , GetTile , GetUnits e così via, prendono tutti i parametri X e Y nella class Board , ma anche la ThreeDBoard bisogno di un parametro Z.

Quindi è necessario implementare nuovamente questi metodi con un parametro Z. Il parametro Z non ha alcun contesto per la class Board e i metodi ereditati dalla class Board perdono il loro significato. Un’unità di codice che tenta di utilizzare la class ThreeDBoard come Board class base sarebbe molto sfortunata.

Forse dovremmo trovare un altro approccio. Invece di estendere la Board , ThreeDBoard dovrebbe essere composto da oggetti Board . Un object Board per unità dell’asse Z.

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

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 sebbene 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, un 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 deve essere violato da una corretta implementazione di Square , quindi non è un valido sostituto di Rectangle .

Robert Martin ha un documento eccellente 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 (notare che il secondo esempio è fortemente condensato):

Un semplice esempio di violazione di LSP

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

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

Chiaramente la funzione DrawShape è mal formata. Deve conoscere ogni ansible derivato della class Shape e deve essere cambiato ogni volta che vengono create nuove derivate di Shape . In effetti, molti vedono la struttura di questa funzione come un anatema per il design orientato agli oggetti.

Quadrato e rettangolo, una violazione più sottile.

Tuttavia, ci sono altri, molto più sottili, modi di violare l’LSP. Considera un’applicazione che utilizza la class Rectangle 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 esigano la possibilità di manipolare i quadrati oltre ai rettangoli. […]

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

Square erediterà le funzioni SetWidth e SetHeight . Queste funzioni sono assolutamente inappropriate per un 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 sovrascrivere SetWidth e SetHeight […]

Ma considera la seguente funzione:

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

Se passiamo un riferimento a un object Square in questa funzione, l’object Square verrà danneggiato perché l’altezza non verrà modificata. Questa è una chiara violazione di LSP. La funzione non funziona per le derivate dei suoi argomenti.

[…]

LSP è necessario laddove alcuni codici pensino che stia chiamando i metodi di un tipo T , e possa chiamare inconsapevolmente i metodi di un tipo S , dove S extends T (cioè S eredita, o deriva da, un sottotipo di, il supertipo T ) .

Ad esempio, ciò si verifica quando una funzione con un parametro di input di tipo T viene chiamata (cioè invocata) con un valore argomento di tipo S Oppure, se a 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 (cioè invarianti) per i metodi di tipo T (ad es. Rectangle ), non viene violato quando vengono chiamati 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 gli immutabili rettangoli setter si aspettano che le dimensioni vengano modificate in modo indipendente, ma gli immutabili impostori di Piazza 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 ogni metodo del sottotipo S debba avere parametri di input controvarianti e un output covariante.

Controvariante significa che la varianza è contraria alla direzione dell’ereditarietà, cioè il tipo Si , di ciascun parametro di input di ciascun metodo del sottotipo S , deve essere uguale o supertipo del tipo Ti del parametro di input corrispondente del metodo corrispondente del supertipo T

Covarianza significa che la varianza è nella stessa direzione dell’ereditarietà, cioè il tipo So , dell’output di ciascun metodo del sottotipo S , deve essere lo stesso o un 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 che stia chiamando un metodo di T , quindi fornisce argomento (s) di tipo Ti e assegna l’output al tipo To Quando in realtà sta chiamando il metodo corrispondente di S , ogni argomento di input Ti viene assegnato a un parametro di input Si e l’output So viene assegnato al tipo To Quindi se il Si non fosse controverso rispetto a Ti , allora un sottotipo Xi che non sarebbe un sottotipo di Si potrebbe essere assegnato a Ti .

Inoltre, per le lingue (es. Scala o Ceylon) che hanno annotazioni sulla varianza del sito di definizione sui parametri del tipo polimorfismo (es. Generici), la co- o contro-direzione dell’annotazione della varianza per ogni parametro di tipo del tipo T deve essere opposta o uguale 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.


La sottotipizzazione è appropriata dove possono essere enumerati gli invarianti.

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

Typestate (vedi pagina 3) dichiara e applica gli 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, quindi 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 utilizzo della tipizzazione per applicare gli invarianti in fase di compilazione. Il sistema dei tipi potrebbe anche essere completato da Turing, ad esempio Scala . Linguaggi e dimostratori di teoremi con typescript formalizzano i modelli di digitazione di ordine superiore.

A causa della necessità che la semantica si astragga sull’estensione , mi aspetto che l’uso della tipizzazione per modellare gli invarianti, vale a dire la semantica denotazionale unificata di ordine superiore, sia superiore al Typestate. Per “estensione” si intende la composizione illimitata e permutata di sviluppo modulare non coordinato. Perché mi sembra l’antitesi dell’unificazione e quindi dei gradi di libertà, di avere due modelli mutuamente dipendenti (es. Tipi e Typestate) per esprimere la semantica condivisa, che non può essere unificata l’una con l’altra per la composizione estensibile . Ad esempio, l’estensione simile a Expression Problem è stata unificata nei domini di tipo subtyping, overloading delle funzioni e tipizzazione parametrica.

La mia posizione teorica è che la conoscenza esista (vedi la sezione “La centralizzazione è cieca e inadatta”), non ci sarà mai un modello generale che possa imporre il 100% di copertura di tutti i possibili invarianti in un linguaggio informatico completo di Turing. Perché la conoscenza esista, esistono molte possibilità inaspettate, cioè il disordine e l’entropia devono sempre aumentare. Questa è la forza entropica. Per dimostrare tutti i possibili calcoli di una potenziale estensione, è necessario calcolare a priori tutte le possibili estensioni.

Questo è il motivo per cui il Teorema di Arresto esiste, ovvero non è determinabile se ogni programma ansible in un linguaggio di programmazione completo di Turing termina. Può essere dimostrato che alcuni programmi specifici terminano (uno che tutte le possibilità sono state definite e calcolate). Ma è imansible dimostrare che tutta la ansible estensione di quel programma termina, a meno che le possibilità di estensione di quel programma non siano complete di Turing (ad esempio tramite digitando dipendente). Poiché il requisito fondamentale per la completezza di Turing è la 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:

  • I teoremi di incompletezza di Gödel : qualsiasi teoria formale, in cui tutte le verità aritmetiche possono essere provate, è incoerente.
  • Il paradosso di Russell : ogni regola di appartenenza per un insieme che può contenere un insieme, enumera il tipo specifico di ciascun membro o contiene se stesso. Quindi, gli insiemi non possono essere estesi o sono ricorsione illimitata. Ad esempio, l’insieme di tutto ciò che non è una teiera, include se stesso, che include se stesso, che include se stesso, ecc …. Quindi una regola è incoerente se (può contenere un insieme e) non enumera i tipi specifici (cioè 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 coerenti e completamente enumerati su tutte le possibili estensioni, sono i teoremi di incompletezza di Gödel.
  • Principio della sottosezione di Liskov : generalmente è un problema indecidibile se un insieme è il sottoinsieme di un altro, cioè l’eredità è generalmente indecidibile.
  • Riferimento Linsky : è indecidibile quale sia il calcolo di qualcosa, quando è descritto o percepito, cioè la percezione (la realtà) non ha un punto di riferimento assoluto.
  • Teorema di Coase : non esiste un punto di riferimento esterno, quindi qualsiasi barriera alle possibilità esterne illimitate fallirà.
  • Seconda legge della termodynamic : l’intero universo (un sistema chiuso, cioè tutto), le tendenze al massimo disordine, cioè le possibilità massime indipendenti.

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 a causa del suo uccello, ma che dire di questo:

 public class Ostrich extends Bird{} 

Lo struzzo è un uccello, ma non può volare, la class di struzzo è un sottotipo della class Uccello, ma non può usare il metodo della mosca, il che significa che rompiamo il principio dell’LSP.

Buon esempio

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

L’LSP è una regola sul contratto delle clases: se una class 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 object derivato, dà esattamente gli stessi risultati di chiamare Foo su un object Base, purché arg sia lo stesso.

Le funzioni che usano 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 su LSP, ho assunto che ciò fosse inteso in senso molto stretto, essenzialmente equiparandolo all’implementazione dell’interfaccia e al casting sicuro per il tipo. Ciò significherebbe che LSP è garantito o meno dal linguaggio stesso. Ad esempio, in questo senso stretto, ThreeDBoard è certamente sostituibile per Board, per quanto riguarda il compilatore.

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

In breve, cosa vuol dire che il codice client “sa” che l’object dietro il puntatore è di tipo derivato piuttosto che il tipo di puntatore non è limitato a type-safety. L’aderenza a LSP è anche verificabile attraverso il sondaggio del comportamento effettivo degli oggetti. Cioè, esaminando l’impatto dello stato di un object e degli argomenti del metodo sui risultati delle chiamate al metodo o sui tipi di eccezioni lanciate dall’object.

Tornando all’esempio di nuovo, in teoria i metodi di Board possono essere fatti funzionare bene su ThreeDBoard. In pratica, tuttavia, sarà molto difficile evitare differenze di comportamento che il client potrebbe non gestire correttamente, senza ostacolare la funzionalità che è prevista l’aggiunta di ThreeDBoard.

Con questa conoscenza in mano, la valutazione dell’aderenza LSP può essere un ottimo strumento per determinare quando la composizione è il meccanismo più appropriato per estendere la funzionalità esistente, piuttosto che l’ereditarietà.

Stranamente, nessuno ha pubblicato il documento originale che descriveva lsp. Non è una lettura facile come quella di Robert Martin, ma ne vale la pena.

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

Se ho una class A che è una sottoclass di B compatibile con LSP, allora posso riutilizzare la suite di test di B per testare A.

Per testare completamente la sottoclass A, probabilmente ho bisogno di aggiungere altri casi di test, ma al minimo posso riutilizzare tutti i casi di test della superclass B.

Un modo per realizzare questo è costruendo ciò che McGregor chiama “Gerarchia parallela per il test”: la mia class ATest erediterà da BTest . È quindi necessaria una forma di iniezione per garantire che il caso di test funzioni con oggetti di tipo A anziché di tipo B (un modello di metodo template semplice lo farà).

Si noti che il riutilizzo della suite di super-test per tutte le implementazioni di sottoclassi è in realtà un modo per verificare che queste implementazioni di sottoclassi siano conformi a LSP. Quindi, si può anche sostenere che si dovrebbe eseguire la suite di test della superclass nel contesto di qualsiasi sottoclass.

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

C’è una lista di controllo per determinare se stai violando Liskov o meno.

  • Se si viola uno dei seguenti oggetti -> si viola Liskov.
  • Se non violi nulla -> non puoi concludere nulla.

Lista di controllo:

  • Non devono essere generate nuove eccezioni nella class derivata : se la tua class base ha lanciato ArgumentNullException, le tue sottoclassi potevano solo lanciare eccezioni di tipo ArgumentNullException o qualsiasi eccezione derivata da ArgumentNullException. Lanciare IndexOutOfRangeException è una violazione di Liskov.
  • Le pre-condizioni non possono essere rafforzate : supponi che la tua class base funzioni con un membro int. Ora il tuo sottotipo richiede che int sia positivo. Questo è un pre-condizioni rafforzate, e ora qualsiasi codice che ha funzionato perfettamente bene prima con gli inti negativi è rotto.
  • Le post-condizioni non possono essere indebolite : si supponga che la class base richieda che tutte le connessioni al database vengano chiuse prima del metodo restituito. Nella tua sottoclass hai annullato quel metodo e la connessione leaved 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 un po ‘di tempo nascosti nella class base e l’unico modo per rivelarli è leggere il codice della class base. Fondamentalmente devi essere sicuro che quando si sostituisce un metodo, qualcosa di immodificabile deve rimanere invariato dopo l’esecuzione del metodo sottoposto a override. La cosa migliore che posso pensare è di far rispettare questi vincoli invarianti nella class base, ma non sarebbe facile.
  • Vincolo cronologico : quando si sostituisce un metodo non è consentito modificare una proprietà non modificabile nella class base. Dai un’occhiata a questo codice e puoi vedere che Nome è 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 oggetti: controvarianza degli argomenti del metodo e covarianza dei tipi di ritorno . Ma non è ansible in C # (sono uno sviluppatore C #) quindi non mi interessa di loro.

Riferimento:

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

Quindi Liskov ha 3 regole sottostanti:

  1. Signature Rule : There should be a valid implementation of every operation of the supertype in the subtype syntactically. Something a compiler will be able to check for you. There is a little rule about throwing fewer exceptions and being at least as accessible as the supertype methods.

  2. Methods Rule: The implementation of those operations is semantically sound.

    • Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more.
    • Stronger Postconditions: They should produce a subset of the output the supertype methods produced.
  3. Properties Rule : This goes beyond individual function calls.

    • Invariants : Things that are always true must remain true. Per esempio. a Set’s size is never negative.
    • Evolutionary Properties : Usually something to do with immutability or the kind of states the object can be in. Or maybe the object only grows and never shrinks so the subtype methods shouldn’t make it.

All these properties need to be preserved and the extra subtype functionality shouldn’t violate supertype properties.

If these three things are taken care of , you have abstracted away from the underlying stuff and you are writing loosely coupled code.

Source: Program Development in Java – Barbara Liskov

This formulation of the LSP is way too strong:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.

Which basically means that S is another, completely encapsulated implementation of the exact same thing as T. And I could be bold and decide that performance is part of the behavior of P…

So, basically, any use of late-binding violates the LSP. It’s the whole point of OO to to obtain a different behavior when we substitute an object of one kind for one of another kind!

The formulation cited by wikipedia is better since the property depends on the context and does not necessarily include the whole behavior of the program.

Some addendum:
I wonder why didn’t anybody write about the Invariant , preconditions and post conditions of the base class that must be obeyed by the derived classs. For a derived class D to be completely sustitutable by the Base class B, class D must obey certain conditions:

  • In-variants of base class must be preserved by the derived class
  • Pre-conditions of the base class must not be strengthened by the derived class
  • Post-conditions of the base class must not be weakened by the derived class.

So the derived must be aware of the above three conditions imposed by the base class. Hence, the rules of subtyping are pre-decided. Which means, ‘IS A’ relationship shall be obeyed only when certain rules are obeyed by the subtype. These rules, in the form of invariants, precoditions and postcondition, should be decided by a formal ‘ design contract ‘.

Further discussions on this available at my blog: Liskov Substitution principle

Long story short, let’s leave rectangles rectangles and squares squares, practical example when extending a parent class, you have to either PRESERVE the exact parent API or to EXTEND IT.

Let’s say you have a base ItemsRepository.

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

And a sub class extending it:

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

Then you could have a Client working with the Base ItemsRepository API and relying on it.

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

The LSP is broken when substituting parent class with a sub class breaks the API’s contract .

 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 :( } } 

In a very simple sentence, we can say:

The child class must not violate its base class characteristics. It must be capable with it. We can say it’s same as subtyping.

A square is a rectangle where the width equals the height. If the square sets two different sizes for the width and height it violates the square invariant. This is worked around by introducing side effects. But if the rectangle had a setSize(height, width) with precondition 0 < height and 0 < width. The derived subtype method requires height == width; a stronger precondition (and that violates lsp). This shows that though square is a rectangle it is not a valid subtype because the precondition is strengthened. The work around (in general a bad thing) cause a side effect and this weakens the post condition (which violates lsp). setWidth on the base has post condition 0 < width. The derived weakens it with height == width.

Therefore a resizable square is not a resizable rectangle.

I see rectangles and squares in every answer, and how to violate the LSP.

I’d like to show how the LSP can be conformsd to with a real world example :

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

This design conforms to the LSP because the behavior remains unchanged regardless of the implementation we choose to use.

And yes, you can violate LSP in this configuration doing one simple change like so :

 < ?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 ! } } 

Now the subtypes cannot be used the same way since they don't produce the same result anymore.

Would implementing ThreeDBoard in terms of an array of Board be that useful?

Perhaps you may want to treat slices of ThreeDBoard in various planes as a Board. In that case you may want to abstract out an interface (or abstract class) for Board to allow for multiple implementations.

In terms of external interface, you might want to factor out a Board interface for both TwoDBoard and ThreeDBoard (although none of the above methods fit).

I encourage you to read the article: Violating Liskov Substitution Principle (LSP) .

You can find there an explanation what is the Liskov Substitution Principle, general clues helping you to guess if you have already violated it and an example of approach that will help you to make your class hierarchy be more safe.

The clearest explanation for LSP I found so far has been “The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class” from here . The article gives code example for violating LSP and fixing it.

LISKOV SUBSTITUTION PRINCIPLE (From Mark Seemann book) states that we should be able to replace one implementation of an interface with another without breaking either client or implementation.It’s this principle that enables to address requirements that occur in the future, even if we can’t foresee them today.

If we unplug the computer from the wall (Implementation), neither the wall outlet (Interface) nor the computer (Client) breaks down (in fact, if it’s a laptop computer, it can even run on its batteries for a period of time). With software, however, a client often expects a service to be available. If the service was removed, we get a NullReferenceException. To deal with this type of situation, we can create an implementation of an interface that does “nothing.” This is a design pattern known as Null Object,[4] and it corresponds roughly to unplugging the computer from the wall. Because we’re using loose coupling, we can replace a real implementation with something that does nothing without causing trouble.

Let’s say we use a rectangle in our code

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

In our geometry class we learned that a square is a special type of rectangle because its width is the same length as its height. Let’s make a Square class as well based on this info:

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

If we replace the Rectangle with Square in our first code, then it will break:

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

This is because the Square has a new precondition we did not have in the Rectangle class: width == height . According to LSP the Rectangle instances should be substitutable with Rectangle subclass instances. This is because these instances pass the type check for Rectangle instances and so they will cause unexpected errors in your code.

This was an example for the “preconditions cannot be strengthened in a subtype” part in the wiki article . So to sum up, violating LSP will probably cause errors in your code at some point.

Liskov’s Substitution Principle(LSP)

All the time we design a program module and we create some class hierarchies. Then we extend some classs creating some derived classs.

We must make sure that the new derived classs just extend without replacing the functionality of old classs. Otherwise, the new classs can produce undesired effects when they are used in existing program modules.

Liskov’s Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.

Esempio:

Below is the classic example for which the Liskov’s Substitution Principle is violated. In the example, 2 classs are used: Rectangle and Square. Let’s assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don’t know the exact what type of object will be returned. But we know it’s a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10, the area should be 50. Instead, the result will be 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:

This principle is just an extension of the Open Close Principle and it means that we must make sure that new derived classs are extending the base classs without changing their behavior.

See also: Open Close Principle

Some similar concepts for better structure: Convention over configuration

Likov’s Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.

Intent – Derived types must be completely substitute able for their base types.

Example – Co-variant return types in java.

Here is an excerpt from this post that clarifies things nicely:

[..] in order to comprehend some principles, it’s important to realize when it’s been violated. This is what I will do now.

What does the violation of this principle mean? It implies that an object doesn’t fulfill the contract imposed by an abstraction expressed with an interface. In other words, it means that you identified your abstractions wrong.

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

Is this a violation of LSP? Sì. This is because the account’s contract tells us that an account would be withdrawn, but this is not always the case. So, what should I do in order to fix it? I just modify the contract:

 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à, now the contract is satisfied.

This subtle violation often imposes a client with the ability to tell the difference between concrete objects employed. For example, given the first Account’s contract, it could look like the following:

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

And, this automatically violates the open-closed principle [that is, for money withdrawal requirement. Because you never know what happens if an object violating the contract doesn’t have enough money. Probably it just returns nothing, probably an exception will be thrown. So you have to check if it hasEnoughMoney() — which is not part of an interface. So this forced concrete-class-dependent check is an OCP violation].

This point also addresses a misconception that I encounter quite often about LSP violation. It says the “if a parent’s behavior changed in a child, then, it violates LSP.” However, it doesn’t — as long as a child doesn’t violate its parent’s contract.