Am auzit că Liskov Substitution Principle (LSP) este un principiu fundamental al proiectării orientate pe obiecte. Ce este și care sunt câteva exemple de utilizare?
Care este un exemplu al principiului substituției Liskov?
Răspunsuri (33)
Un exemplu grozav care ilustrează LSP (dat de unchiul Bob într-un podcast pe care l-am auzit recent) a fost faptul că uneori ceva care sună corect în limbaj natural nu prea funcționează în cod.
În matematică, un Square
este un Rectangle
. Într-adevăr, este o specializare a unui dreptunghi. „Este a” te face să vrei să modelezi asta cu moștenire. Cu toate acestea, dacă în cod ați făcut ca Square
să derive din Rectangle
, atunci un Square
ar trebui să fie utilizabil oriunde vă așteptați la un Rectangle
. Acest lucru duce la un comportament ciudat.
Imaginează-ți că ai avut metodele SetWidth
și SetHeight
pe clasa ta de bază Rectangle
; asta pare perfect logic. Totuși, dacă referința dvs. Rectangle
a indicat un Square
, atunci SetWidth
și SetHeight
nu au sens, deoarece setarea unuia ar schimba cealaltă pentru a se potrivi cu el. În acest caz, Square
eșuează Testul de înlocuire Liskov cu Rectangle
și abstracția de a avea Square
să moștenească de la Rectangle
este una proastă.
Ar trebui să verificați celelalte Afișe motivaționale SOLID Principles.
Square.setWidth(int width)
ar fi implementat astfel: this.width = width; this.height = width;
? În acest caz, se garantează că lățimea este egală cu înălțimea.
- person MC Emperor; 28.10.2015
setHeight()
și setWidth()
în Square
, astfel încât locurile din codul dvs. în care utilizați un Rectangule
nu ar mai funcționa dacă treceți un Square
și acesta este punctul principal despre LSP;
- person sdlins; 26.12.2016
h1
cu Eroare la stabilirea unei conexiuni la baza de date. Am găsit cel mai recent instantaneu WayBack Machine și spune clar că imaginile sunt licențiate sub CC BY-SA. Vă sugerez să conectați la instantaneul WayBack Machine, precum și la adresa URL originală și să încărcați imaginea pe serverele Stack Overflow (prin editor). Astfel, întreruperile nu vor afecta postarea.
- person Palec; 10.06.2017
o.setDimensions(width, height)
ar rezolva acea problemă, dar LSP ar fi încă încălcat, deoarece un pătrat are precondiții mai puternice (width == height
) decât un dreptunghi. Nu cred că postarea dvs. răspunde la nimic despre LSP.
- person inf3rno; 09.10.2017
GetHeight
și GetWidth
dacă vor face exact același lucru. S-ar putea să fie o problemă mică, dar totuși un motiv pentru a o face în alt mod.
- person AustinWBryan; 09.05.2018
SetLength
care ar putea apela intern SetWidth
și SetHeight
- person alancalvitti; 29.03.2019
Principiul substituției Liskov (LSP, lsp) este un concept în programarea orientată pe obiecte care afirmă:
Funcțiile care folosesc pointeri sau referințe la clase de bază trebuie să poată folosi obiecte ale claselor derivate fără să știe.
În esență, LSP este despre interfețe și contracte, precum și despre cum să decideți când să extindeți o clasă față de utilizarea unei alte strategii, cum ar fi compoziția, pentru a vă atinge obiectivul.
Cel mai eficient mod pe care l-am văzut pentru a ilustra acest aspect a fost în Head First OOA&D. Acestea prezintă un scenariu în care sunteți dezvoltator într-un proiect pentru a construi un cadru pentru jocurile de strategie.
Ei prezintă o clasă care reprezintă o tablă care arată astfel:
Toate metodele iau coordonatele X și Y ca parametri pentru a localiza poziția plăcilor în matricea bidimensională a lui Tiles
. Acest lucru va permite unui dezvoltator de joc să gestioneze unitățile de pe tablă în timpul jocului.
Cartea continuă să schimbe cerințele pentru a spune că cadrul jocului trebuie să accepte și table de joc 3D pentru a găzdui jocurile care au zbor. Așadar, este introdusă o clasă ThreeDBoard
care extinde Board
.
La prima vedere, aceasta pare a fi o decizie bună. Board
furnizează atât proprietățile Height
și Width
, iar ThreeDBoard
furnizează axa Z.
Unde se defectează este atunci când te uiți la toți ceilalți membri moșteniți de la Board
. Metodele pentru AddUnit
, GetTile
, GetUnits
și așa mai departe, toate preiau atât parametrii X, cât și Y în clasa Board
, dar ThreeDBoard
are nevoie și de un parametru Z.
Deci, trebuie să implementați acele metode din nou cu un parametru Z. Parametrul Z nu are context în clasa Board
iar metodele moștenite din clasa Board
își pierd sensul. O unitate de cod care încearcă să folosească clasa ThreeDBoard
ca clasă de bază Board
ar avea foarte ghinion.
Poate ar trebui să găsim o altă abordare. În loc să extindă Board
, ThreeDBoard
ar trebui să fie compus din Board
obiecte. Un obiect Board
per unitate a axei Z.
Acest lucru ne permite să folosim principii bune orientate pe obiecte precum încapsularea și reutilizarea și nu încalcă LSP.
GetUnits(int x, int y)
în GetUnits(Position pos)
și același lucru este valabil și pentru celelalte funcții. În acest fel, nu va încălca LSP. Corectează-mă daca greșesc.
- person du369; 22.04.2020
Substituibilitatea este un principiu în programarea orientată pe obiecte care afirmă că, într-un program de calculator, dacă S este un subtip de T, atunci obiectele de tip T pot fi înlocuite cu obiecte de tip S.
Să facem un exemplu simplu în Java:
Exemplu prost
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
Rața poate zbura pentru că este o pasăre, dar ce zici de asta:
public class Ostrich extends Bird{}
Struțul este o pasăre, dar nu poate zbura, clasa struțului este un subtip al clasei Bird, dar nu ar trebui să poată folosi metoda muștei, asta înseamnă că încălcăm principiul LSP.
Bun exemplu
public class Bird{}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
Bird bird
. Trebuie să aruncați obiectul la FlyingBirds pentru a folosi fly, ceea ce nu este frumos, nu?
- person Moody; 20.11.2017
Bird bird
, înseamnă că nu poate folosi fly()
. Asta e. Trecerea unui Duck
nu schimbă acest fapt. Dacă clientul are FlyingBirds bird
, atunci chiar dacă trece un Duck
ar trebui să funcționeze întotdeauna în același mod.
- person Steve Chamaillard; 18.02.2018
LSP se referă la invarianți.
Exemplul clasic este dat de următoarea declarație pseudo-cod (implementări omise):
class Rectangle {
int getHeight()
void setHeight(int value) {
postcondition: width didn’t change
}
int getWidth()
void setWidth(int value) {
postcondition: height didn’t change
}
}
class Square extends Rectangle { }
Acum avem o problemă, deși interfața se potrivește. Motivul este că am încălcat invarianții care decurg din definiția matematică a pătratelor și dreptunghiurilor. În modul în care funcționează getters și setters, un Rectangle
ar trebui să satisfacă următorul invariant:
void invariant(Rectangle r) {
r.setHeight(200)
r.setWidth(100)
assert(r.getHeight() == 200 and r.getWidth() == 100)
}
Totuși, acest invariant (precum și postcondițiile explicite) trebuie să fie încălcat de o implementare corectă a lui Square
, prin urmare nu este un substitut valid al lui Rectangle
.
Square
?
- person ca9163d9; 24.01.2012
setHeight()
și setWidth()
probabil nu ar trebui permise să existe, deoarece asta face ca o clasă să fie mutabilă. Cu toate acestea, dacă ar exista, ar putea doar throw NotSupportedException() / NotImplementedException()
sau ceva similar.
- person Leonid; 03.07.2012
square
codificat corect, acest lucru nu va fi întotdeauna adevărat, ambii setari trebuie să resetați atât lățimea, cât și înălțimea, altfel nu vor păstra invarianții (implicite) ai unui pătrat. Cu alte cuvinte: ai un sistem de tip inconsecvent și nu vrei niciodată asta.
- person Konrad Rudolph; 06.06.2013
SetArea(4); SetPerimeter(10);
ar face un model dreptunghi obișnuit un dreptunghi 1x4. Apelurile ar face un model de clasă Square
mai întâi un 2x2, apoi fie să se schimbe într-un pătrat sqrt(10)xsqrt(10), fie să arunce un InvalidOperationException
sau ceva de genul.
- person Wolfzoon; 07.08.2016
SetDepth(int d) => throw exception
. Dar mă întreb... este un dreptunghi un tip special de pătrat care nu necesită H=W? Alternativ..Cadrilateral ⇒ Trapez ⇒ Paralelogram.
- person Paulustrious; 05.08.2017
Quadrilateral ⇒ Trapezoid ⇒ Parallelogram
. Dreptunghiul și pătratul vor decurge în cele din urmă. din Paralelogram, deși ar fi Paralelogram ⇒ Romb ⇒ Pătrat. Obținerea corectă a derivațiilor este ca și cum ai proiecta tabele de baze de date. Găsiți greșit de la început și vă îndreptați spre o prostie
- person Paulustrious; 05.08.2017
Shape
sau Parallelogram
sau ceva de genul ăsta?
- person AustinWBryan; 09.05.2018
Square
dintr-un Rectangle
, dacă o abordare polimorfă nu este obligatorie, aș pleda pentru un SquareDecorator
care preia obiectul Rectangle
. Ce crezi tu sau altcineva despre asta?
- person Reuel Ribeiro; 11.01.2019
Shape
. Acestea fiind spuse, abordarea dumneavoastră este cu siguranță soluția corectă în multe cazuri de utilizare din viața reală.
- person Konrad Rudolph; 11.01.2019
Rectangle
care asigură că cealaltă valoare nu este modificată. Dar faptul că pătratele au un contract implicit diferit este tocmai motivul pentru care aceasta este o încălcare LSP: ambele sunt valide izolat, dar un pătrat nu este un subtip valid al unui dreptunghi, deoarece contractele lor implicite sunt" t compatibil.
- person Konrad Rudolph; 13.10.2020
Rectangle
. Dar avertisment corect: aș putea sfârși prin a le elimina din nou, deoarece sunt artificiale și, în orice caz, sunt deja capturate de metoda exemplului invariant
. Și în plus, LSP prin definiția sa originală se referă și la invarianții impliciti, nu doar pe cei expliciți. Exemplul original fără postcondiții a fost foarte în conformitate cu definiția originală, formală, a LSP.
- person Konrad Rudolph; 13.10.2020
setHeight()
și setWidth()
sunt operații independente, fiecare în sine nu este o încălcare a niciunui invariant. Numai atunci când încercați să forțați operațiunile independente să fie o singură operație, poate fi văzută viziunea dvs. despre ceea ce credeți că este un invariant implicit. Deci, în cel mai bun caz, acesta este un caz slab pentru o încălcare a LSP
- person wired_in; 19.10.2020
Robert Martin are o lucrare excelentă despre: principiul substituției Liskov. Se discută moduri subtile și nu atât de subtile în care principiul poate fi încălcat.
Câteva părți relevante ale lucrării (rețineți că al doilea exemplu este puternic condensat):
Un exemplu simplu de încălcare a LSP
Una dintre cele mai flagrante încălcări ale acestui principiu este utilizarea C++ Run-Time Type Information (RTTI) pentru a selecta o funcție în funcție de tipul unui obiect. adică:
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)); }
În mod clar, funcția
DrawShape
este prost formată. Trebuie să știe despre fiecare derivată posibilă a claseiShape
și trebuie schimbată ori de câte ori sunt create noi derivate aleShape
. Într-adevăr, mulți văd structura acestei funcții ca o anatemă pentru proiectarea orientată pe obiecte.Pătrat și dreptunghi, o încălcare mai subtilă.
Cu toate acestea, există și alte modalități, mult mai subtile, de a încălca LSP. Luați în considerare o aplicație care utilizează clasa
Rectangle
așa cum este descris mai jos: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; };
[...] Imaginați-vă că într-o zi utilizatorii cer capacitatea de a manipula pătrate pe lângă dreptunghiuri. [...]
În mod clar, un pătrat este un dreptunghi pentru toate scopurile și scopurile normale. Deoarece relația ISA este valabilă, este logic să modelăm clasa
Square
ca fiind derivată dinRectangle
. [...]
Square
va moșteni funcțiileSetWidth
șiSetHeight
. Aceste funcții sunt complet nepotrivite pentru unSquare
, deoarece lățimea și înălțimea unui pătrat sunt identice. Acesta ar trebui să fie un indiciu semnificativ că există o problemă cu designul. Cu toate acestea, există o modalitate de a ocoli problema. Am putea suprascrieSetWidth
șiSetHeight
[...]Dar luați în considerare următoarea funcție:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
Dacă trecem o referință la un obiect
Square
în această funcție, obiectulSquare
va fi corupt deoarece înălțimea nu va fi modificată. Aceasta este o încălcare clară a LSP. Funcția nu funcționează pentru derivatele argumentelor sale.[...]
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.
Dacă o condiție prealabilă a clasei de copil este mai puternică decât o condiție prealabilă a clasei părinte, nu ai putea înlocui un părinte cu un copil fără a încălca condiția prealabilă . De aici LSP.
- person user2023861; 11.02.2015
LSP este necesar acolo unde un cod crede că apelează metodele unui tip T
și poate apela fără să știe metodele unui tip S
, unde S extends T
(adică S
moștenește, derivă din sau este un subtip al supertipului T
).
De exemplu, acest lucru se întâmplă atunci când o funcție cu un parametru de intrare de tip T
este apelată (adică invocată) cu o valoare a argumentului de tip S
. Sau, în cazul în care unui identificator de tip T
, i se atribuie o valoare de tip S
.
val id : T = new S() // id thinks it's a T, but is a S
LSP cere ca așteptările (adică invarianții) pentru metodele de tip T
(de exemplu Rectangle
) să nu fie încălcate atunci când sunt apelate în schimb metodele de tip S
(de ex. 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
Chiar și un tip cu câmpuri imuabile mai are invarianți, de ex. setarii dreptunghiului imuabili se așteaptă ca dimensiunile să fie modificate în mod independent, dar setarii dreptunghiului imuabili încalcă această așteptare.
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 necesită ca fiecare metodă a subtipului S
să aibă parametri de intrare contravarianți și o ieșire covariantă.
Contravariantă înseamnă că varianța este contrară direcției moștenirii, adică tipul Si
, al fiecărui parametru de intrare al fiecărei metode din subtipul S
, trebuie să fie același sau un supertip de tipul Ti
al parametrul de intrare corespunzător al metodei corespunzătoare supertipului T
.
Covarianța înseamnă că varianța este în aceeași direcție a moștenirii, adică tipul So
, al ieșirii fiecărei metode a subtipului S
, trebuie să fie același sau un subtip de tipul To
al celui corespunzătoare. ieșirea metodei corespunzătoare a supertipului T
.
Acest lucru se datorează faptului că, dacă apelantul crede că are un tip T
, crede că apelează o metodă de T
, atunci furnizează argument(e) de tip Ti
și atribuie ieșirea tipului To
. Când apelează de fapt metoda corespunzătoare a lui S
, atunci fiecare argument de intrare Ti
este atribuit unui parametru de intrare Si
, iar ieșirea So
este atribuită tipului To
. Astfel, dacă Si
nu ar fi contravariant w.r.t. la Ti
, apoi un subtip Xi
— care nu ar fi un subtip de Si
— ar putea fi atribuit lui Ti
.
În plus, pentru limbile (de exemplu, Scala sau Ceylon) care au adnotări de variație pentru site-ul de definiție pe parametrii polimorfismului de tip (de exemplu, generice), co- sau contradirecția adnotării de varianță pentru fiecare parametru de tip de tipul T
trebuie să fie opus sau aceeași direcție, respectiv, fiecărui parametru de intrare sau ieșire (al fiecărei metode de T
) care are tipul parametrului de tip.
În plus, pentru fiecare parametru de intrare sau ieșire care are un tip de funcție, direcția de variație necesară este inversată. Această regulă se aplică recursiv.
Subtypingul este adecvat acolo unde invarianții pot fi enumerați.
Există multe cercetări în desfășurare cu privire la modul de modelare a invarianților, astfel încât acestea să fie impuse de compilator.
Typestate (a se vedea pagina 3) declară și impune invarianții de stare ortogonali cu tipul. Alternativ, invariantele pot fi impuse prin conversia aserțiilor în tipuri. De exemplu, pentru a afirma că un fișier este deschis înainte de a-l închide, atunci File.open() ar putea returna un tip OpenFile, care conține o metodă close() care nu este disponibilă în File. Un tic-tac-toe API poate fi un alt exemplu de a folosi tastarea pentru a impune invarianții în timpul compilării. Sistemul de tip poate fi chiar Turing-complet, de ex. Scala. Limbajele tipizate în mod dependent și demonstratorii de teoreme formalizează modelele de tipărire de ordin superior.
Din cauza necesității ca semantică să rezumat peste extensie, mă aștept ca folosirea tastării pentru a modela invarianții, adică semantica denotațională unificată de ordin superior, este superioară Typestate. „Extensie” înseamnă compoziția nelimitată, permutată, a dezvoltării necoordonate, modulare. Pentru că mi se pare a fi antiteza unificării și, prin urmare, a gradelor de libertate, să existe două modele reciproc dependente (de exemplu, tipuri și Typestate) pentru exprimarea semanticii partajate, care nu pot fi unificate între ele pentru o compoziție extensibilă. . De exemplu, extensia asemănătoare Expression Problem a fost unificată în subtipărire, supraîncărcare a funcției și tastare parametrică domenii.
Poziția mea teoretică este aceea că, pentru ca cunoștințe să existe ( vezi secțiunea „Centralizarea este oarbă și nepotrivită”), nu va exista niciodată un model general care să impună o acoperire de 100% a tuturor invarianților posibili într-un limbaj de computer complet Turing. Pentru ca cunoașterea să existe, există multe posibilități neașteptate, adică dezordinea și entropia trebuie să fie mereu în creștere. Aceasta este forța entropică. A demonstra toate calculele posibile ale unei extensii potențiale înseamnă a calcula a priori toate extensiile posibile.
Acesta este motivul pentru care există teorema de oprire, adică este indecidabil dacă fiecare program posibil într-un limbaj de programare complet Turing se termină. Se poate dovedi că un anumit program se termină (unul pentru care au fost definite și calculate toate posibilitățile). Dar este imposibil să se demonstreze că toată extensia posibilă a acelui program se termină, cu excepția cazului în care posibilitățile de extindere a acelui program nu sunt Turing complete (de exemplu, prin tastare dependentă). Întrucât cerința fundamentală pentru completitudinea Turing este recursie nemărginită, este intuitiv să înțelegem cum teoremele de incompletitudine ale lui Gödel și paradoxul lui Russell se aplică extensiei.
O interpretare a acestor teoreme le încorporează într-o înțelegere conceptuală generalizată a forței entropice:
- Teoremele de incompletitudine ale lui Gödel: orice teorie formală, în care toate adevărurile aritmetice pot fi dovedite, este inconsistentă.
- Paradoxul lui Russell: fiecare regulă de membru pentru un set care poate conține un set, fie enumerează tipul specific al fiecărui membru, fie se conține pe sine. Astfel, mulțimile fie nu pot fi extinse, fie sunt recursiuni nemărginite. De exemplu, setul de tot ceea ce nu este un ceainic, se include pe sine, care se include pe sine, care se include pe sine etc... Astfel, o regulă este inconsecventă dacă (poate conține un set și) nu enumeră tipurile specifice (adică permite toate tipurile nespecificate) și nu permite extensie nelimitată. Acesta este setul de seturi care nu sunt membri ai lor. Această incapacitate de a fi atât consistentă, cât și complet enumerată pe toată extensia posibilă, este teoremele de incompletitudine ale lui Gödel.
- Principiul substituției Liskov: în general, este o problemă indecidibilă dacă orice mulțime este submulțimea altuia, adică moștenirea este în general indecidabilă.
- Referințare Linsky: este indecidibil care este calculul a ceva, atunci când este descris sau perceput, adică percepția (realitatea) nu are un punct de referință absolut.
- Teorema lui Coase: nu există un punct de referință extern, astfel încât orice barieră în calea posibilităților externe nelimitate va eșua.
- A doua lege a termodinamicii: întregul univers (un sistem, adică totul) tendințe spre dezordine maximă, adică posibilități independente maxime.
Văd dreptunghiuri și pătrate în fiecare răspuns și cum să încalc LSP.
Aș dori să arăt cum se poate conforma LSP cu un exemplu din lumea reală:
<?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;
}
}
Acest design este conform cu LSP, deoarece comportamentul rămâne neschimbat, indiferent de implementarea pe care alegem să o folosim.
Și da, puteți încălca LSP în această configurație, făcând o schimbare simplă astfel:
<?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 !
}
}
Acum subtipurile nu pot fi folosite în același mod, deoarece nu mai produc același rezultat.
Database::selectQuery
să accepte doar subsetul de SQL acceptat de toate motoarele DB. Asta nu este practic... Acestea fiind spuse, exemplul este încă mai ușor de înțeles decât majoritatea celorlalți folosiți aici.
- person Palec; 25.02.2018
Există o listă de verificare pentru a determina dacă încălcați sau nu Liskov.
- Dacă încalci unul dintre următoarele elemente -› încalci Liskov.
- Dacă nu încălcați niciunul -› nu puteți concluziona nimic.
Lista de verificare:
Nu ar trebui să se introducă noi excepții în clasa derivată: dacă clasa dvs. de bază a lansat ArgumentNullException, atunci subclasele dvs. au avut voie să arunce numai excepții de tip ArgumentNullException sau orice excepții derivate din ArgumentNullException. Aruncarea IndexOutOfRangeException este o încălcare a lui Liskov.
Precondițiile nu pot fi consolidate: presupunem că clasa de bază funcționează cu un membru int. Acum subtipul dvs. necesită ca int să fie pozitiv. Acestea sunt precondiții consolidate, iar acum orice cod care funcționa perfect înainte cu inturi negative este rupt.
Condițiile ulterioare nu pot fi slăbite: să presupunem că clasa de bază a cerut ca toate conexiunile la baza de date să fie închise înainte ca metoda să fie returnată. În subclasa dvs. ați suprasolicitat acea metodă și ați lăsat conexiunea deschisă pentru reutilizare ulterioară. Ați slăbit postcondițiile acelei metode.
Invarianțele trebuie păstrate: cea mai dificilă și mai dureroasă constrângere de îndeplinit. Invarianții sunt uneori ascunși în clasa de bază și singura modalitate de a le dezvălui este să citiți codul clasei de bază. Practic, trebuie să vă asigurați că, atunci când suprascrieți o metodă, orice lucru neschimbabil trebuie să rămână neschimbat după ce metoda dvs. anulată este executată. Cel mai bun lucru la care mă pot gândi este să impun aceste constrângeri invariante în clasa de bază, dar asta nu ar fi ușor.
Constrângere istoric: atunci când suprascrieți o metodă, nu aveți voie să modificați o proprietate nemodificabilă din clasa de bază. Aruncă o privire la acest cod și poți vedea că Numele este definit ca fiind nemodificabil (set privat), dar SubType introduce o nouă metodă care permite modificarea acestuia (prin reflecție):
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); } }
Există încă 2 elemente: Contravarianța argumentelor metodei și Covarianța tipurilor de returnare. Dar nu este posibil în C# (sunt dezvoltator C#), așa că nu-mi pasă de ei.
Pe scurt, să lăsăm dreptunghiuri dreptunghiuri și pătrate pătrate, exemplu practic când extindeți o clasă părinte, trebuie fie să PĂSTRĂȚI API-ul părinte exact, fie să-l EXTINȚI.
Să presupunem că aveți o de bază ItemsRepository.
class ItemsRepository
{
/**
* @return int Returns number of deleted rows
*/
public function delete()
{
// perform a delete query
$numberOfDeletedRows = 10;
return $numberOfDeletedRows;
}
}
Și o subclasă care o extinde:
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;
}
}
Apoi, puteți avea un Client care lucrează cu API-ul Base ItemsRepository și se bazează pe acesta.
/**
* 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();
}
}
LSP este întrerupt atunci când înlocuirea clasa părinte cu o subclasă încalcă contractul 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 :(
}
}
Puteți afla mai multe despre scrierea de software întreținut în cursul meu: https://www.udemy.com/enterprise-php/
LSP este o regulă despre contractul claselor: dacă o clasă de bază satisface un contract, atunci prin LSP clasele derivate trebuie să îndeplinească și acel contract.
În Pseudo-python
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
satisface LSP dacă de fiecare dată când apelați Foo pe un obiect derivat, dă exact aceleași rezultate ca și apelarea lui Foo pe un obiect de bază, atâta timp cât arg este același.
Este mult mai simplu decât crezi:
usr->next = group->users;
group->users = usr;
- person Charlie Martin; 04.07.2012
2 + "2"
). Poate confundați tastarea puternică cu tastarea static?
- person asmeurer; 20.01.2013
u'hello'
cu 'world'
) și uneori pentru a însemna unul dintre multe alte lucruri. en.wikipedia.org/wiki/Strong_and_weak_typing
- person Mark Amery; 27.12.2013
2 + '2'
, tot nu va accepta 2 + 'two'
.
- person asmeurer; 27.12.2013
Bănuiesc că toată lumea a acoperit ce este LSP din punct de vedere tehnic: practic doriți să puteți abstrage de la detaliile subtipurilor și să utilizați supertipurile în siguranță.
Deci Liskov are 3 reguli de bază:
Regula semnăturii: Ar trebui să existe o implementare validă a fiecărei operațiuni a supertipului din subtip din punct de vedere sintactic. Ceva un compilator va putea verifica pentru tine. Există o mică regulă despre a arunca mai puține excepții și a fi cel puțin la fel de accesibil ca metodele de supertip.
Metode Regula: Implementarea acelor operațiuni este solidă din punct de vedere semantic.
- Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more.
- Postcondiții mai puternice: ar trebui să producă un subset al rezultatelor produse de metodele de supertip.
Regulă proprietăți: Aceasta depășește apelurile de funcții individuale.
- Invariants : Things that are always true must remain true. Eg. a Set's size is never negative.
- Proprietăți evolutive: De obicei, ceva de-a face cu imuabilitatea sau tipul de stări în care se poate afla obiectul. Sau poate că obiectul crește și nu se micșorează niciodată, așa că metodele de subtip nu ar trebui să ajungă.
Toate aceste proprietăți trebuie păstrate, iar funcționalitatea suplimentară a subtipului nu ar trebui să încalce proprietățile supertipului.
Dacă aceste trei lucruri sunt luate în considerare, ați făcut abstracție de la lucrurile de bază și scrieți cod cuplat liber.
Sursa: Dezvoltarea programelor în Java - Barbara Liskov
Să ilustrăm în 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() { ... }
}
Nu este nicio problemă aici, nu? O mașină este cu siguranță un dispozitiv de transport și aici putem vedea că suprascrie metoda startEngine() a superclasei sale.
Să adăugăm un alt dispozitiv de transport:
class Bicycle extends TransportationDevice
{
@Override
void startEngine() /*problem!*/
}
Totul nu merge așa cum a fost planificat acum! Da, o bicicletă este un dispozitiv de transport, cu toate acestea, nu are motor și, prin urmare, metoda startEngine() nu poate fi implementată.
Acestea sunt tipurile de probleme la care duce încălcarea Principiului de substituție Liskov și, de obicei, pot fi recunoscute printr-o metodă care nu face nimic sau chiar nu poate fi implementată.
Soluția acestor probleme este o ierarhie corectă a moștenirii, iar în cazul nostru am rezolva problema diferențierea claselor de dispozitive de transport cu și fără motoare. Chiar dacă o bicicletă este un dispozitiv de transport, nu are motor. În acest exemplu, definiția noastră a dispozitivului de transport este greșită. Nu ar trebui să aibă motor.
Putem refactoriza clasa noastră TransportationDevice după cum urmează:
class TrasportationDevice
{
String name;
String getName() { ... }
void setName(String n) { ... }
double speed;
double getSpeed() { ... }
void setSpeed(double d) { ... }
}
Acum putem extinde TransportationDevice pentru dispozitive nemotorizate.
class DevicesWithoutEngines extends TransportationDevice
{
void startMoving() { ... }
}
Și extindeți TransportationDevice pentru dispozitivele motorizate. Aici este mai potrivit să adăugați obiectul Engine.
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
Astfel, clasa noastră de mașini devine mai specializată, respectând în același timp principiul înlocuirii Liskov.
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
Și clasa noastră de biciclete este, de asemenea, în conformitate cu Principiul de înlocuire Liskov.
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
Funcțiile care folosesc pointeri sau referințe la clase de bază trebuie să poată folosi obiecte ale claselor derivate fără să știe.
Când am citit prima dată despre LSP, am presupus că acest lucru a fost înțeles într-un sens foarte strict, echivalând în esență cu implementarea interfeței și casting sigur de tip. Ceea ce ar însemna că LSP este fie asigurat, fie nu de limbajul în sine. De exemplu, în acest sens strict, ThreeDBoard este cu siguranță înlocuibil cu Board, în ceea ce privește compilatorul.
După ce am citit mai multe despre concept, am descoperit că LSP este în general interpretat mai larg decât atât.
Pe scurt, ceea ce înseamnă pentru codul client să „știe” că obiectul din spatele pointerului este de tip derivat, mai degrabă decât tipul pointerului, nu este limitat la siguranța tipului. Aderarea la LSP este de asemenea testabilă prin sondarea comportamentului real al obiectelor. Adică, examinarea impactului stării unui obiect și al argumentelor metodei asupra rezultatelor apelurilor de metodă sau a tipurilor de excepții aruncate de la obiect.
Revenind din nou la exemplu, teoretic metodele Board pot fi făcute să funcționeze bine pe ThreeDBoard. În practică, totuși, va fi foarte dificil să preveniți diferențele de comportament pe care clientul nu le poate gestiona în mod corespunzător, fără a împiedica funcționalitatea pe care ThreeDBoard este destinat să o adauge.
Cu aceste cunoștințe în mână, evaluarea aderării LSP poate fi un instrument excelent pentru a determina când compoziția este mecanismul mai potrivit pentru extinderea funcționalității existente, mai degrabă decât moștenirea.
Un exemplu important de utilizare a LSP este în testarea software.
Dacă am o clasă A care este o subclasă B compatibilă cu LSP, atunci pot reutiliza suita de teste a lui B pentru a testa A.
Pentru a testa complet subclasa A, probabil că trebuie să mai adaug câteva cazuri de testare, dar cel puțin pot reutiliza toate cazurile de testare ale superclasei B.
O modalitate de a realiza este aceasta prin construirea a ceea ce McGregor numește o „ierarhie paralelă pentru testare”: clasa mea ATest
va moșteni de la BTest
. Este necesară apoi o anumită formă de injecție pentru a se asigura că cazul de testare funcționează cu obiecte de tip A mai degrabă decât de tip B (un model simplu de metodă de șablon va fi suficient).
Rețineți că reutilizarea suitei de super-testare pentru toate implementările subclaselor este de fapt o modalitate de a testa dacă aceste implementări ale subclaselor sunt conforme cu LSP. Astfel, se poate argumenta, de asemenea, că se ar trebui să ruleze suita de testare a superclasei în contextul oricărei subclase.
Consultați, de asemenea, răspunsul la întrebarea Stackoverflow „Pot implementa o serie de teste reutilizabile pentru a testa implementarea unei interfețe?"
Într-o propoziție foarte simplă, putem spune:
Clasa copil nu trebuie să-și încalce caracteristicile clasei de bază. Trebuie să fie capabil cu el. Putem spune că este la fel ca subtiparea.
Principiul substituției Liskov
- Metoda suprascrisă nu ar trebui să rămână goală
- Metoda suprascrisă nu ar trebui să arunce o eroare
- Comportamentul clasei de bază sau al interfeței nu ar trebui să fie modificat (relucrat) ca din cauza comportamentelor de clasă derivate.
Această formulare a LSP este mult prea puternică:
Dacă pentru fiecare obiect o1 de tip S există un obiect o2 de tip T astfel încât pentru toate programele P definite în termeni de T, comportamentul lui P este neschimbat atunci când o1 este înlocuit cu o2, atunci S este un subtip al lui T.
Ceea ce înseamnă practic că S este o altă implementare complet încapsulată a exact același lucru ca T. Și aș putea fi îndrăzneț și aș decide că performanța face parte din comportamentul lui P...
Deci, practic, orice utilizare a legăturii tardive încalcă LSP. Scopul OO este să obținem un alt comportament atunci când înlocuim un obiect de un fel cu unul de alt fel!
Formularea citată de wikipedia este mai bună, deoarece proprietatea depinde de context și nu include neapărat întregul comportamentul programului.
Principiul de substituție al lui Liskov (LSP)
Tot timpul proiectăm un modul de program și creăm niște ierarhii de clasă. Apoi extindem unele clase creând niște clase derivate.
Trebuie să ne asigurăm că noile clase derivate se extind fără a înlocui funcționalitatea claselor vechi. În caz contrar, noile clase pot produce efecte nedorite atunci când sunt utilizate în modulele de program existente.
Principiul de substituție al lui Liskov afirmă că, dacă un modul de program folosește o clasă de bază, atunci referința la clasa de bază poate fi înlocuită cu o clasă derivată fără a afecta funcționalitatea modulului de program.
Exemplu:
Mai jos este exemplul clasic pentru care este încălcat principiul substituției lui Liskov. În exemplu, sunt folosite 2 clase: dreptunghi și pătrat. Să presupunem că obiectul Rectangle este folosit undeva în aplicație. Extindem aplicația și adăugăm clasa Square. Clasa pătrată este returnată de un model din fabrică, pe baza unor condiții și nu știm exact ce tip de obiect va fi returnat. Dar știm că este un dreptunghi. Obținem obiectul dreptunghi, setăm lățimea la 5 și înălțimea la 10 și obținem aria. Pentru un dreptunghi cu lățimea 5 și înălțimea 10, aria ar trebui să fie 50. În schimb, rezultatul va fi 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.
}
}
Concluzie:
Acest principiu este doar o extensie a principiului Open Close și înseamnă că trebuie să ne asigurăm că noile clase derivate extind clasele de bază fără a le schimba comportamentul.
Vedeți și: Principiul Open Close
Câteva concepte similare pentru o structură mai bună: Convenție peste configurare
LSP în termeni simpli afirmă că obiectele aceluiași superclasa ar trebui să poată fi schimbată între ele fără a sparge nimic.
De exemplu, dacă avem o clasă Cat
și o clasă Dog
derivate dintr-o clasă Animal
, orice funcții care utilizează clasa Animal ar trebui să poată folosi Cat
sau Dog
și să se comporte normal.
Acest principiu a fost introdus de Barbara Liskov în 1987 și extinde Principiul Deschis-Închis concentrându-se asupra comportamentului unei superclase și a subtipurilor acesteia.
Importanța sa devine evidentă atunci când luăm în considerare consecințele încălcării acesteia. Luați în considerare o aplicație care utilizează următoarea clasă.
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;
}
}
}
Imaginați-vă că într-o zi, clientul cere capacitatea de a manipula pătrate pe lângă dreptunghiuri. Deoarece un pătrat este un dreptunghi, clasa pătratului ar trebui să fie derivată din clasa Rectangle.
public class Square : Rectangle
{
}
Totuși, făcând asta ne vom confrunta cu două probleme:
Un pătrat nu are nevoie atât de variabile de înălțime, cât și de lățime moștenite din dreptunghi și acest lucru ar putea crea o pierdere semnificativă de memorie dacă trebuie să creăm sute de mii de obiecte pătrate. Proprietățile de stabilire a lățimii și înălțimii moștenite din dreptunghi sunt nepotrivite pentru un pătrat, deoarece lățimea și înălțimea unui pătrat sunt identice. Pentru a seta atât înălțimea cât și lățimea la aceeași valoare, putem crea două proprietăți noi, după cum urmează:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
Acum, când cineva va stabili lățimea unui obiect pătrat, înălțimea acestuia se va schimba în consecință și invers.
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
Să mergem mai departe și să luăm în considerare această altă funcție:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
Dacă trecem o referință la un obiect pătrat în această funcție, am încălca LSP deoarece funcția nu funcționează pentru derivatele argumentelor sale. Proprietățile lățimea și înălțimea nu sunt polimorfe deoarece nu sunt declarate virtuale în dreptunghi (obiectul pătrat va fi corupt deoarece înălțimea nu va fi modificată).
Cu toate acestea, declarând proprietățile setterului ca fiind virtuale, ne vom confrunta cu o altă încălcare, OCP. De fapt, crearea unui pătrat de clasă derivată provoacă modificări dreptunghiului clasei de bază.
A()
cred că te-ai referit la r.Width = 32;
deoarece un Rectangle
nu are o metodă SetWidth()
.
- person wired_in; 13.10.2020
Câteva addendum:
Mă întreb de ce nu a scris nimeni despre Invariant , precondiții și postcondiții ale clasei de bază care trebuie respectate de clasele derivate. Pentru ca o clasă derivată D să fie complet înlocuibilă cu clasa de bază B, clasa D trebuie să respecte anumite condiții:
- Variantele din clasa de bază trebuie păstrate de către clasa derivată
- Condițiile preliminare ale clasei de bază nu trebuie să fie consolidate de clasa derivată
- Postcondițiile clasei de bază nu trebuie să fie slăbite de clasa derivată.
Deci, derivatul trebuie să fie conștient de cele trei condiții de mai sus impuse de clasa de bază. Prin urmare, regulile de subtipizare sunt prestabilite. Ceea ce înseamnă că relația „IS A” va fi respectată numai atunci când anumite reguli sunt respectate de subtip. Aceste reguli, sub formă de invariante, precodiții și postcondiții, ar trebui decise printr-un „contract de proiectare'.
Discuții suplimentare despre acest lucru sunt disponibile pe blogul meu: Principiul substituției Liskov
Un pătrat este un dreptunghi în care lățimea este egală cu înălțimea. Dacă pătratul stabilește două dimensiuni diferite pentru lățime și înălțime, acesta încalcă invariantul pătratului. Acest lucru este rezolvat prin introducerea de efecte secundare. Dar dacă dreptunghiul avea un setSize(height, width) cu precondiția 0 ‹ înălțime și 0 ‹ lățime. Metoda subtipului derivat necesită înălțime == lățime; o precondiție mai puternică (și care încalcă lsp). Acest lucru arată că, deși pătratul este un dreptunghi, nu este un subtip valid, deoarece condiția preliminară este întărită. Lucrul în jurul valorii de (în general un lucru rău) provoacă un efect secundar și acest lucru slăbește starea post (care încalcă lsp). setWidth de pe bază are condiția post 0 ‹ lățime. Derivata o slabeste cu inaltime == latime.
Prin urmare, un pătrat redimensionabil nu este un dreptunghi redimensionabil.
Ar fi atât de utilă implementarea ThreeDBoard în termeni de o serie de Board?
Poate că doriți să tratați felii de ThreeDBoard în diferite planuri ca o placă. În acest caz, este posibil să doriți să abstractizați o interfață (sau o clasă abstractă) pentru Board pentru a permite implementări multiple.
În ceea ce privește interfața externă, este posibil să doriți să luați în considerare o interfață Board atât pentru TwoDBoard, cât și pentru ThreeDBoard (deși nici una dintre metodele de mai sus nu se potrivește).
Cea mai clară explicație pentru LSP pe care am găsit-o până acum a fost „Principiul substituției Liskov spune că obiectul unei clase derivate ar trebui să poată înlocui un obiect al clasei de bază fără a aduce erori în sistem sau a modifica comportamentul clasei de bază. " de la aici. Articolul oferă exemplu de cod pentru încălcarea LSP și remedierea acestuia.
Să presupunem că folosim un dreptunghi în codul nostru
r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);
În clasa noastră de geometrie am învățat că un pătrat este un tip special de dreptunghi, deoarece lățimea lui este aceeași lungime cu înălțimea. Să facem și o clasă Square
pe baza acestor informații:
class Square extends Rectangle {
setDimensions(width, height){
assert(width == height);
super.setDimensions(width, height);
}
}
Dacă înlocuim Rectangle
cu Square
în primul nostru cod, atunci se va rupe:
r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);
Acest lucru se datorează faptului că Square
are o nouă precondiție pe care nu o aveam în clasa Rectangle
: width == height
. Conform LSP, Rectangle
instanțe ar trebui să fie înlocuibile cu Rectangle
instanțe de subclasă. Acest lucru se datorează faptului că aceste instanțe trec verificarea de tip pentru Rectangle
instanțe și astfel vor cauza erori neașteptate în codul dvs.
Acesta a fost un exemplu pentru partea „precondițiile nu pot fi consolidate într-un subtip” din articol wiki. Deci, pentru a rezuma, încălcarea LSP va cauza probabil erori în codul dvs. la un moment dat.
LSP spune că „Obiectele ar trebui să fie înlocuibile cu subtipurile lor”. Pe de altă parte, acest principiu indică
Clasele copil nu ar trebui să încalce niciodată definițiile tipului clasei părinte.
iar exemplul următor ajută la o mai bună înțelegere a LSP.
Fără 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();
}
Remediere prin 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();
}
Vă încurajez să citiți articolul: Violating Liskov Substitution Principle (LSP).
Puteți găsi acolo o explicație care este Principiul substituției Liskov, indicii generale care vă ajută să ghiciți dacă l-ați încălcat deja și un exemplu de abordare care vă va ajuta să vă faceți mai sigură ierarhia clasei.
PRINCIPIUL DE SUBSTITUȚIE LISKOV (Din cartea Mark Seemann) afirmă că ar trebui să putem înlocui o implementare a unei interfețe cu alta fără a întrerupe clientul sau implementarea. Acesta este principiul care ne permite să răspundem cerințelor care apar în viitor, chiar dacă putem. nu le prevăd astăzi.
Dacă deconectam computerul de la perete (Implementare), nici priza de perete (Interfață) și nici computerul (Client) nu se defectează (de fapt, dacă este un laptop, poate chiar să funcționeze cu bateriile sale pentru o perioadă de timp) . Cu toate acestea, cu software, un client se așteaptă adesea ca un serviciu să fie disponibil. Dacă serviciul a fost eliminat, obținem o NullReferenceException. Pentru a face față acestui tip de situație, putem crea o implementare a unei interfețe care nu face „nimic”. Acesta este un model de design cunoscut sub numele de Null Object,[4] și corespunde aproximativ cu deconectarea computerului de la perete. Deoarece folosim cuplaje libere, putem înlocui o implementare reală cu ceva care nu face nimic fără a cauza probleme.
Principiul substituției al lui Likov afirmă că dacă un modul de program utilizează o clasă de bază, atunci referința la clasa de bază poate fi înlocuită cu o clasă derivată fără a afecta funcționalitatea modulului de program.
Intenție - Tipurile derivate trebuie să fie complet substituibile pentru tipurile lor de bază.
Exemplu - Tipuri de returnare covariante în java.
Iată un extras din această postare care clarifică lucrurile frumos:
[..] pentru a înțelege unele principii, este important să ne dăm seama când a fost încălcat. Asta voi face acum.
Ce înseamnă încălcarea acestui principiu? Implică faptul că un obiect nu îndeplinește contractul impus de o abstracție exprimată cu o interfață. Cu alte cuvinte, înseamnă că ți-ai identificat greșit abstracțiile.
Luați în considerare următorul exemplu:
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);
}
}
Este aceasta o încălcare a LSP? Da. Acest lucru se datorează faptului că contractul contului ne spune că un cont ar fi retras, dar nu este întotdeauna cazul. Deci, ce ar trebui să fac pentru a o remedia? Doar modific contractul:
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à, acum contractul este îndeplinit.
Această încălcare subtilă impune adesea un client cu capacitatea de a face diferența dintre obiectele concrete folosite. De exemplu, având în vedere contractul primului cont, ar putea arăta astfel:
class Client
{
public function go(Account $account, Money $money)
{
if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
return;
}
$account->withdraw($money);
}
}
Și, acest lucru încalcă automat principiul deschis-închis [adică pentru cerința de retragere a banilor. Pentru că nu știi niciodată ce se întâmplă dacă un obiect care încalcă contractul nu are suficienți bani. Probabil că pur și simplu nu returnează nimic, probabil că va fi aruncată o excepție. Deci trebuie să verificați dacă hasEnoughMoney()
-- care nu face parte dintr-o interfață. Deci, această verificare forțată dependentă de clasă de beton este o încălcare OCP].
Acest punct abordează și o concepție greșită pe care o întâlnesc destul de des despre încălcarea LSP. Se spune că „dacă comportamentul unui părinte s-a schimbat la un copil, atunci încalcă LSP”. Cu toate acestea, nu - atâta timp cât un copil nu încalcă contractul părintelui său.
Se afirmă că dacă C este un subtip de E, atunci E poate fi înlocuit cu obiecte de tip C fără a modifica sau întrerupe comportamentul programului. Cu cuvinte simple, clasele derivate ar trebui să fie înlocuibile cu clasele lor părinte. De exemplu, dacă un fiul unui fermier este un fermier, atunci el poate lucra în locul tatălui său, dar dacă un fiul unui fermier este jucător de cricket, atunci nu poate lucra în locul tatălui său. Tată.
Exemplu de încălcare:
public class Plane{
public void startEngine(){}
}
public class FighterJet extends Plane{}
public class PaperPlane extends Plane{}
În exemplul dat, clasele FighterPlane
și PaperPlane
extind ambele clasa Plane
care conține metoda startEngine()
. Deci este clar că FighterPlane
poate porni motorul, dar PaperPlane
nu poate, așa că se rupe LSP
.
Clasa PaperPlane
deși extinde clasa Plane
și ar trebui să fie înlocuibilă în locul acesteia, dar nu este o entitate eligibilă cu care instanța lui Plane ar putea fi înlocuită, deoarece un avion de hârtie nu poate porni motorul deoarece nu are unul. Deci exemplul bun ar fi,
Exemplu respectat:
public class Plane{
}
public class RealPlane{
public void startEngine(){}
}
public class FighterJet extends RealPlane{}
public class PaperPlane extends Plane{}
Principiul de substituție Wiki Liskov (LSP)
Condițiile preliminare nu pot fi întărite într-un subtip.
Postcondițiile nu pot fi slăbite într-un subtip.
Invariantele supertipului trebuie păstrate într-un subtip.
*Precondiția și postcondiția sunt function (method) types
[Tipul funcției Swift. Funcție rapidă vs metoda]
//C1 <- C2 <- C3
class C1 {}
class C2: C1 {}
class C3: C2 {}
Condițiile preliminare (de exemplu, funcția
parameter type
) pot fi aceleași sau mai slabe. (se străduiește pentru -› C1)Postcondițiile (de exemplu, funcția
returned type
) pot fi aceleași sau mai puternice (se străduiește pentru -› C3)Variabila invariantă[Despre] de tip super ar trebui să rămână invariabilă
Rapid
class A {
func foo(a: C2) -> C2 {
return C2()
}
}
class B: A {
override func foo(a: C1) -> C3 {
return C3()
}
}
Java
class A {
public C2 foo(C2 a) {
return new C2();
}
}
class B extends A {
@Override
public C3 foo(C2 a) { //You are available pass only C2 as parameter
return new C3();
}
}
Subscriere
- Sybtype nu ar trebui să solicite de la apelant mai mult decât supertype
- Sybtype nu ar trebui să expună pentru apelant mai puțin decât supertype
Contravarianța tipurilor de argument și covarianța tipului returnat.
- Contravarianța argumentelor metodei în subtip.
- Covarianța tipurilor de returnare în subtip.
- Nicio excepție nouă nu ar trebui să fie aruncată de metodele subtipului, cu excepția cazului în care acele excepții sunt ele însele subtipuri de excepții aruncate de metodele supertipului.
[Varianță, Covarianță, Contravarianță]
Permiteți-mi să încerc, luați în considerare o interfață:
interface Planet{
}
Aceasta este implementată de clasă:
class Earth implements Planet {
public $radius;
public function construct($radius) {
$this->radius = $radius;
}
}
Veți folosi Pământul ca:
$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();
Acum luați în considerare încă o clasă care extinde Pământul:
class LiveablePlanet extends Earth{
public function color(){
}
}
Acum, conform LSP, ar trebui să puteți utiliza LiveablePlanet în locul Pământului și nu ar trebui să vă distrugă sistemul. Ca:
$planet = new LiveablePlanet(6371); // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();
Exemple preluate de pe aici
Earth
este o instanță a lui plant
, de ce ar fi derivat din el?
- person zar; 03.05.2019