Care este un exemplu al principiului substituției Liskov?

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?


person NotMyself    schedule 11.09.2008    source sursă
comment
Mai multe exemple de aderență și încălcare a LSP aici   -  person StuartLC    schedule 15.05.2015
comment
Această întrebare are infinit de răspunsuri bune și, prin urmare, este prea largă.   -  person Raedwald    schedule 16.12.2018
comment
Acum 12 ani mai târziu, ar fi grozav dacă ați putea să vă anulați propriul răspuns și să acceptați altul, deoarece, din păcate, propriul răspuns este complet greșit și nu descrie LSP, ci o nepotrivire a interfeței. Și, din moment ce este acceptat, induce în eroare mulți oameni (după cum este indicat de numărul de voturi pozitive).   -  person Konrad Rudolph    schedule 13.10.2020
comment
Asta funcționează pentru mine. Am trecut la cel mai popular răspuns.   -  person NotMyself    schedule 13.10.2020
comment
numărul mii de voturi pozitive!   -  person Jonas Grønbek    schedule 06.06.2021


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ă.

„introduceți

Ar trebui să verificați celelalte Afișe motivaționale SOLID Principles.

person m-sharp    schedule 25.02.2009
comment
@m-sharp Ce se întâmplă dacă este un dreptunghi imuabil astfel încât în ​​loc de SetWidth și SetHeight, avem metodele GetWidth și GetHeight? - person Pacerier; 26.04.2012
comment
Morala poveștii: modelați-vă cursurile pe baza comportamentelor, nu pe proprietăți; modelați-vă datele pe baza proprietăților și nu pe comportamente. Dacă se comportă ca o rață, cu siguranță este o pasăre. - person Sklivvz; 20.05.2012
comment
Ei bine, un pătrat ESTE în mod clar un tip de dreptunghi în lumea reală. Dacă putem modela acest lucru în codul nostru depinde de specificație. Ceea ce indică LSP este că comportamentul subtipului ar trebui să se potrivească cu comportamentul tipului de bază, așa cum este definit în specificația tipului de bază. Dacă specificația tipului de bază dreptunghiul spune că înălțimea și lățimea pot fi setate independent, atunci LSP spune că pătratul nu poate fi un subtip de dreptunghi. Dacă specificația dreptunghiului spune că un dreptunghi este imuabil, atunci un pătrat poate fi un subtip de dreptunghi. Este vorba despre subtipuri care mențin comportamentul specificat pentru tipul de bază. - person SteveT; 24.09.2012
comment
@Pacerier nu există nicio problemă dacă este imuabil. Adevărata problemă aici este că nu modelăm dreptunghiuri, ci mai degrabă dreptunghiuri remodelabile, adică dreptunghiuri a căror lățime sau înălțime poate fi modificată după creare (și considerăm în continuare că este același obiect). Dacă ne uităm la clasa dreptunghi în acest fel, este clar că un pătrat nu este un dreptunghi remodelabil, deoarece un pătrat nu poate fi remodelat și totuși poate fi un pătrat (în general). Din punct de vedere matematic, nu vedem problema pentru că mutabilitatea nici măcar nu are sens într-un context matematic. - person asmeurer; 20.01.2013
comment
Din prelegerea profesorului Barbara Liskov: Obiectele subtipurilor ar trebui să se comporte ca cele ale supertipurilor dacă sunt folosite prin metode de supertip. - person ruhong; 04.02.2015
comment
Dacă lățimea și înălțimea pot fi modificate de către setter, ar trebui să existe doar o clasă dreptunghi și nicio clasă specială de pătrat. În schimb, clasa dreptunghi ar trebui să aibă un getter numit IsSquare. Ori de câte ori lățimea și înălțimea au aceleași valori, IsSquare va returna true, în caz contrar false. Tipurile nu sunt întotdeauna statice, dar uneori - ca în acest caz - se pot schimba. - person brighty; 10.02.2015
comment
Deci, de ce chiar am nevoie de subtipărire dacă comportamentul clasei mele moștenite nu diferă de cel părinte (cu excepția cazurilor, când procedează la fel, dar diferit)? de ce ai nevoie de toate aceste metode de anulare dacă trebuie să se comporte absolut similar cu cele ale bazei? vorbind de figuri de geometrie: IDrawable are Draw(). cum ai putea insista ca Draw() lui Circle:IDrawable să dea același rezultat ca și Square:IDrawable? sau, să zicem, Rotated90DegreesSquare:Square desen la fel ca pătrat? - person jungle_mole; 11.09.2015
comment
Am o întrebare despre principiu. De ce ar fi problema dacă 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
comment
Acel poster motivațional nu are sens. Ar trebui să fie Dacă arată ca o rață, cărlatani ca o rață, dar are nevoie de baterii... de ce ești interesat de baterii, din nou? Pentru că până la urmă implică că singura substituție pentru o rață este o altă rață (cel mai bine exact aceeași). Un astfel de radicalism se autoînfrânge. - person David Tonhofer; 05.01.2016
comment
Problema Pătrat-Dreptunghi este cunoscută și ca Problema Circle-Elipse. - person mbx; 02.03.2016
comment
Ceea ce este corect sau greșit, bun sau rău, depinde de perspectiva cuiva - indiferent dacă este din partea consumatorului de cod sau din partea autorului codului. LSP, IMO, se aplică la partea consumatorului. Pune în perspectivă corectă, multe greșeli par corecte. - person Sudhir; 23.05.2016
comment
O altă soluție este de a defini dreptunghiurile ca păstrând raportul de aspect, mai degrabă decât ca având lățimi și înălțimi independente. - person Ed L; 16.07.2016
comment
Se pare că nu răspunde la întrebare. După ce l-am citit, încă nu știu ce este LSP (cu excepția cazului în care posterul conține definiția, deși dat fiind contextul unor astfel de postere, acest lucru nu este clar). - person iheanyi; 12.08.2016
comment
Nici exemplul pătrat nu înțeleg. Cum setWidth și setHeight nu au sens pe un pătrat. După cum ați explicat foarte ușor, a face una implică cealaltă - aceasta este doar definiția unui pătrat. Nu explică de ce este ceva greșit sau incompatibil cu înlocuirea unui dreptunghi cu un pătrat. - person iheanyi; 12.08.2016
comment
Nu ați putea pur și simplu să suprascrieți setWidth sau setHeight și, în definiție, să apelați cealaltă metodă cu același parametru și poof, totul funcționează și comportamentul este consecvent? - person Ungeheuer; 22.11.2016
comment
@MCEmperor dacă modificați implementarea 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
comment
Vă mulțumesc pentru răspuns - dar și dacă LSP-ul este stricat? - person BKSpurgeon; 02.01.2017
comment
Cu toate acestea, imaginea NU este un exemplu valid de LSP rupt (de fapt miroase a Reddit și nu este ceva ce vrem să simțim aici pe SO). Subclasele vor fi întotdeauna mai specifice decât superclasele. Cu toate acestea, specificul lor nu încalcă neapărat LSP. Se pune problema dacă aceste particularități afectează (și încalcă) contractul comun. Dacă o rață alimentată cu baterii rupe contractul comun al unei rațe abstracte depinde de detaliile specifice ale designului. - person AnT; 28.01.2017
comment
Toată lumea folosește acest pătrat vs dreptunghi, care este un exemplu atât de oribil. Supliniți proprietatea Width pentru a seta proprietatea Height, care pare mai degrabă un efect secundar. Toate acestea ar fi rezolvate dacă i-ați pune pe oameni să stabilească în mod explicit lățimea și înălțimea. - person johnny 5; 01.03.2017
comment
Atât imaginea, cât și linkul de la sfârșit sunt rupte. Te superi sa o repari? - person PaulB; 05.06.2017
comment
Mulțumesc @PaulB. Repara-l. - person m-sharp; 06.06.2017
comment
Tehnicienii lucrează pentru mine acum, @m-sharp. A fost doar o întrerupere temporară, sau poate chiar regională? Revizia 8 pare cel puțin inutilă, acum. - person Palec; 07.06.2017
comment
Rece. A anulat schimbarea. SO are un istoric de revizuiri foarte frumos! - person m-sharp; 07.06.2017
comment
La naiba, tot site-ul Lot Techies returnează acum aceeași pagină intitulată Database Error și conține doar un 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
comment
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
comment
Există o bună informație complementară despre înlocuirea metodei / contracte aici softwareengineering.stackexchange.com/a/244783/1451 - person BrunoLM; 21.10.2017
comment
Este un dreptunghi un tip de pătrat sau un pătrat este un tip de dreptunghi sau invers? Un dreptunghi poate fi definit ca un pătrat cu laturile inegale, iar un pătrat poate fi definit ca un dreptunghi cu laturile egale. Un pătrat cu laturile inegale nu este un pătrat, dar un dreptunghi cu laturile egale este totuși un dreptunghi și un pătrat. Deci un pătrat este un dreptunghi cu un contract suplimentar. Prin urmare, interfața pentru un pătrat ar trebui să fie aceeași cu cea pentru un dreptunghi. Setarea unei dimensiuni ar trebui să o stabilească pe cealaltă are sens perfect. - person ATL_DEV; 15.12.2017
comment
Nu aș spune că nu există nicio problemă dacă sunt imuabile. S-ar putea dezamăgi un cititor să vadă 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
comment
În unele cărți se numește regula %100? - person anilkay; 17.06.2018
comment
@AustinWBryan nu, acestea sunt cantități bine definite. Un pătrat are atât o înălțime, cât și o lățime; se întâmplă să fie egale. Dacă cititorul este dezamăgit de acest lucru, atunci fie învață ceva nou și important despre cod, fie nu îi aparține :) - person Ben Kushigian; 19.06.2018
comment
Cred că confundăm subtipul cu cazul particular. Un pătrat nu are nimic diferit în comportament în comparație cu un dreptunghi. Perimetrul, aria pot fi calculate folosind aceleași formule. Pătratul nu este o îmbunătățire sau o rafinare a unui dreptunghi, este doar un caz particular. Square nu ar trebui să fie o subclasă a unui dreptunghi, nici măcar nu ar trebui să fie o clasă separată. - person Mircea Ion; 05.02.2019
comment
@MirceaIon, Square ar putea fi o subclasă a dreptunghiului pentru a simplifica interfața, de exemplu cu SetLength care ar putea apela intern SetWidth și SetHeight - person alancalvitti; 29.03.2019
comment
Este OK să derivați Square din Rectangle, dar nu ar trebui să aveți setWidth și setHeight în caz. Dar nimeni nu te-a putut împiedica să ai setari pentru o zonă acolo unde pui dreptunghiurile. În acest caz, Square va umple doar o parte a zonei, în timp ce Rectangle o va umple complet. :) - person Alex; 17.04.2019
comment
Poate cineva, te rog, din dragoste pentru cod, să-mi spună cum altfel să modelez același exemplu, astfel încât să urmeze LSP? - person Saurabh Goyal; 11.06.2019
comment
Odată ce am eșuat la un interviu pe această întrebare exactă despre pătrate. Intervievatorul a respins întrebarea mea imediată: ce vom face cu acele dreptunghiuri și pătrate? Astăzi sunt fericit că nu am avut acel job. :) - person Aleksei Guzev; 15.12.2019
comment
Care unchi Bob? :-? - person Yousha Aleayoub; 05.02.2020
comment
@sdlins Nu sunt de acord. Puteți numi o situație în care dacă aveți comportamentul setHeight și setWidth suprascris în clasa Square, cum ar fi MCEmperor, a semnat că codul nu va mai funcționa pentru Reactangle? - person Murilo; 09.04.2020
comment
Ar fi fost mult mai bine să oferiți un caz de utilizare exemplificator și un fragment, care ar fi demonstrat codul. - person Giorgi Tsiklauri; 06.08.2020
comment
@Murilo Trebuie să ne umple caseta de 640x480, deci r.SetWidth (640); r.SetHeight(480), sau invers, nu face nicio diferență. Orice subclasă care oferă posibilitatea de a seta lățimea și înălțimea separat, dar de fapt nu face acest lucru, va distruge lucrurile. - person prosfilaes; 13.01.2021
comment
Problema pe care o găsesc cu metodele SetWidth și SetHeight este că nu numai că rupe LSP, ci și ISP (Principiul de segregare a interfeței), deci ar trebui să fie separate în două interfețe diferite, poate SetSize(int, int) pentru Rectangle și SetSize(int) pentru Square, pentru că sunt diferiți, Getters ar putea rămâne acolo unde sunt... deschiși la discuții - person Eugenio Miró; 28.01.2021
comment
Referitor la rață, baterii și de ce sparge LSV -› pentru că rațele, prin definiție, nu au nevoie de baterii. Pentru mine este destul de evident și este o metaforă sub formă de glumă, care nu trebuie luată mai în serios decât atât. - person Ric Jafe; 17.03.2021
comment
@SaurabhGoyal - ideea este că implementarea corectă nu ar trebui să aibă efecte secundare ale unui transfer la o altă proprietate, deoarece părintele nu le-a avut în primul rând. De exemplu, nu ați putea avea getters pentru înălțimea și lățimea individuale și apoi să aveți ca o pereche și un singur setDimentions(x,y) și astfel puteți evita acest efect secundar specific de modificare a înălțimii atunci când setați Width în exemplul pătrat . Cu siguranță există mai multe modalități de a realiza acest lucru. Principalul aspect este că comportamentul copilului ar diferi de cel al părintelui. Asta este ceea ce rupe LSP-ul. - person Ric Jafe; 17.03.2021
comment
Acest lucru m-a ajutat să înțeleg mai mult decât orice alt exemplu - person Tom; 24.05.2021

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:

Diagrama de clasă

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.

person NotMyself    schedule 11.09.2008
comment
Consultați și Problema Circle-Elipse pe Wikipedia pentru un exemplu similar, dar mai simplu. - person Brian; 21.10.2011
comment
Recit de la @NotMySelf: Cred că exemplul este pur și simplu pentru a demonstra că moștenirea de la bord nu are sens în contextul ThreeDBoard și toate semnăturile metodei sunt lipsite de sens cu o axa Z.. - person Contango; 05.06.2013
comment
Recit de la @Chris Ammerman: 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. - person Contango; 05.06.2013
comment
Deci, dacă adăugăm o altă metodă la o clasă Child, dar toată funcționalitatea lui Parent mai are sens în clasa Child, ar fi ruperea LSP? Deoarece, pe de o parte, am modificat puțin interfața pentru utilizarea Copilului, pe de altă parte, dacă am transformat Copilul să fie un Părinte, codul care se așteaptă ca un Părinte să funcționeze bine. - person Nickolay Kondratyev; 18.06.2013
comment
Acesta este un exemplu anti-Liskov. Liskov ne face să derivăm dreptunghiul din pătrat. Clasa mai mulți parametri de la clasa mai puțini parametri. Și ai arătat frumos că este rău. Este într-adevăr o glumă bună să fi marcat ca răspuns și să fi fost votat pozitiv de 200 de ori un răspuns anti-liskov la întrebarea liskov. Este principiul Liskov o eroare cu adevărat? - person Gangnus; 18.10.2015
comment
@Contango Și poți să aduci un exemplu în care moștenirea este bună, conform citației din răspunsul de aici? - person Gangnus; 18.10.2015
comment
Am văzut că moștenirea funcționează greșit. Iată un exemplu. Clasa de bază ar trebui să fie 3DBoard și clasa derivată Board. Placa are încă o axa Z de Max(Z) = Min(Z) = 1 - person Paulustrious; 05.08.2017
comment
@Nickolay Kondratyev, da, nu rupe LSP, ai rezolvat-o singur. - person brgs; 12.07.2018
comment
Putem rezolva problema fără a folosi compoziția? Cred că se poate face prin schimbarea semnăturii lui 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
comment
@Paulustrious asta frânează principiul deschis-închis. Dacă vrei un FourDBoard? Ar trebui să faceți din aceasta o clasă de bază a ThreeDBoard, prin schimbarea implementării ThreeDBoard (s-ar putea să nu aveți acces). - person Jupiter; 19.07.2020
comment
Numpy a rezolvat exact această problemă cu ndarray. O matrice tridimensională, bidimensională și unidimensională sunt toate specializări ale matricei n-dimensionale generalizate. Matematicienii și-au dat seama asta cu secole în urmă. Programatorii reinventează treptat algebra. - person Adam Acosta; 22.12.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{} 
person Maysara Alhindi    schedule 04.07.2017
comment
Bun exemplu, dar ce ai face dacă clientul are Bird bird. Trebuie să aruncați obiectul la FlyingBirds pentru a folosi fly, ceea ce nu este frumos, nu? - person Moody; 20.11.2017
comment
Nu. Dacă clientul are 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
comment
Nu ar servi și ca un exemplu bun pentru segregarea interfeței? - person Saharsh; 05.03.2019
comment
Excelent exemplu Multumesc Omule - person Abdelhadi Abdo; 28.05.2019
comment
Ce zici de folosirea interfeței „Flyable” (nu mă pot gândi la un nume mai bun). În acest fel, nu ne angajăm în această ierarhie rigidă.. Dacă nu știm că avem nevoie de ea. - person Thirdy; 28.05.2019
comment
Veterinarul care vindecă aripile rupte va vedea un exemplu rău ca unul mai bun. Unele specii de păsări care nu zboară pot zbura în propriul lor drum (puiul este un hibrid aici). De asemenea, capacitatea de zbor se schimbă în evoluție. O metodă implicită de zbor cu NULL este o decizie bună, care urmează să fie înlocuită mai târziu. - person Sławomir Lenart; 31.01.2020
comment
Deci, clasa părinte ar trebui să includă doar comportamentele pe care le au întregi copii ai săi sau, cu alte cuvinte, să nu limiteze comportamentele copiilor săi. - person Ari; 25.02.2020
comment
În sfârșit s-a înțeles după o oră de căutare! - person Boshra N; 17.11.2020

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.

person Konrad Rudolph    schedule 12.09.2008
comment
Și de aici și dificultatea de a folosi OO pentru a modela orice am dori să modelăm de fapt. - person DrPizza; 30.11.2009
comment
@DrPizza: Absolut. Cu toate acestea, două lucruri. În primul rând, astfel de relații pot fi încă modelate în POO, deși incomplet sau folosind ocoluri mai complexe (alegeți oricare se potrivește problemei dvs.). În al doilea rând, nu există o alternativă mai bună. Alte mapări/modelări au aceleași probleme sau probleme similare. ;-) - person Konrad Rudolph; 30.11.2009
comment
@KonradRudolph, Este un bun exemplu. Atunci care este modul corect de a declara Square? - person ca9163d9; 24.01.2012
comment
@NickW În unele cazuri (dar nu în cele de mai sus) puteți inversa pur și simplu lanțul de moștenire - logic vorbind, un punct 2D este un punct 3D, unde a treia dimensiune este ignorată (sau 0 - toate punctele se află pe același plan în spațiu 3D). Dar, desigur, acest lucru nu este chiar practic. În general, acesta este unul dintre cazurile în care moștenirea nu ajută cu adevărat și nu există nicio relație naturală între entități. Modelați-le separat (cel puțin eu nu cunosc o modalitate mai bună). - person Konrad Rudolph; 24.01.2012
comment
OOP este menit să modeleze comportamente și nu date. Clasele tale încalcă încapsularea chiar înainte de a încălca LSP. - person Sklivvz; 20.05.2012
comment
@Sklivvz Sunt de acord. Totuși, a fost greu să vin cu un exemplu concis, iar acesta este exemplul stoc LSP ( îl veți găsi în mai multe cărți). Dar da, deși obișnuit, acest lucru este departe de a fi bun OOP. - person Konrad Rudolph; 20.05.2012
comment
Metodele 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
comment
@Leonid Într-un fel, asta rupe și contractul, deoarece schimbă „semnătura” (adică posibilele moduri în care funcția poate ieși). Acum, unele cadre (ahem .NET …) folosesc în mod obișnuit această rută, dar consider că designul interfeței este foarte prost. - person Konrad Rudolph; 03.07.2012
comment
Nu înțeleg cum a fost încălcat LSP aici. Invariantul afirmă că getheight și getwidth ar trebui să returneze înălțimea și lățimea setate, care este întotdeauna adevărată, indiferent de dreptunghiul meteorologic este pătrat sau dreptunghi. Dacă clientul stabilește diferite înălțimi și lățimi, cu siguranță știe că obiectul nu este un pătrat. În mod similar, dacă clientul știa că obiectul este pătrat, el va stabili înălțimea și lățimea egale. Presupunând că clasa dreptunghi are două funcții aria și perimetrul, funcțiile ar funcționa corect indiferent dacă dreptunghiul este un pătrat sau nu. Atunci cum încalcă acest LSP? - person nurabha; 05.06.2013
comment
@nurabha „care este întotdeauna adevărat, indiferent de vreme, dreptunghiul este pătrat sau dreptunghi” – fals. Cu un 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
comment
@nurabha Presupunând că dreptunghiul are două funcții, aria și perimetrul ar duce la aceeași problemă. 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
comment
Dacă nu există invarianți reale, cum ai putut să le fi încălcat? De unde știi ce variante există pentru a fi încălcate (sau conformate)? - person iheanyi; 12.08.2016
comment
@iheanyi Fie trebuie să expliciți invarianții (alias aserțiuni sau folosind sistemul de tip), fie trebuie să cunoașteți domeniul în care lucrați. Știm cum sunt definite dreptunghiurile și pătratele, așa că știm ce invariante impun. - person Konrad Rudolph; 13.08.2016
comment
Sunt de acord - fie constrângerile sau invarianții ar trebui să fie explicite, fie ar trebui să fie impuse de sistem. Dar, pe baza exemplului tău, nimic nu mă obligă să scriu o clasă pătrată cu invariantul implicit pe care l-ai menționat. Presupun că aș sugera să vă actualizați răspunsul pentru a elimina implicit atunci când discutați invarianții, acestea sunt constrângeri reale care dau naștere problemei. - person iheanyi; 14.08.2016
comment
LSP nu ar trebui să știe ce face obiectul derivat, doar că obiectele derivate onorează contractul clasei de bază. De exemplu, Square poate avea 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
comment
Alternativ...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
comment
Ar fi mai logic ca un dreptunghi să moștenească dintr-un tip de pătrat sau ca Square și Rectangle să fie doar clase de frați? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Ați evita această problemă specială, dar relația nu are cu adevărat sens: modelele de moștenire relațiile „este-o” și „fiecare dreptunghi este-un pătrat” este greșită (și, în consecință, ați întâlni alte probleme în care nu puteți pur și simplu înlocui unul cu celălalt). Este corect invers („fiecare pătrat este un dreptunghi”), motiv pentru care este atât de tentant să scrieți cod care încalcă LSP. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Deci, atunci ar fi mai bine să le păstrăm ca clase de frați care moștenesc de la Shape sau Parallelogram sau ceva de genul ăsta? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Da; cu cât lucrez mai mult în acest domeniu, cu atât tind să folosesc moștenirea doar pentru interfețe și clase de bază abstracte și compoziția pentru restul. Uneori este un pic mai mult de lucru (dactilografia), dar evită o grămadă de probleme și este larg repetată de sfaturi de către alți programatori experimentați. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Wow, habar n-aveam, deși, la ultimul meu proiect, m-am confruntat cu problemele de a folosi prea multă moștenire. Mi-am dat seama că vreau să dau unor clase derivate proprietățile altor clase derivate și nu exista nicio modalitate de a face asta. Crezi că numai clasele abstracte ar trebui să aibă copii, în cea mai mare parte? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan În cea mai mare parte, da. Este faimoasă una dintre regulile cărților influente Effective C++, iar o regulă mai generală, Composition over Inheritance, are propriul său Pagina Wikipedia. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Wow, mulțumesc, voi verifica asta. Mai merită acea carte citită pentru dezvoltarea C#, pentru a învăța structuri și modele de design? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Probabil că nu, din păcate; este foarte specific pentru C++. Totuși, Effective C# al lui Bill Wagner urmează un model similar. - person Konrad Rudolph; 10.05.2018
comment
@ca9163d9 Pentru modelarea 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
comment
@ReuelRibeiro Acest lucru merge în direcția supraingineriei; de cele mai multe ori ar fi mai bine să furnizați pur și simplu clase ortogonale care ar putea moșteni ambele de la un 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
comment
Nu sunt convins că acesta este un exemplu bun de încălcare a LSP. Înțeleg că este oarecum exemplul canonic folosit, dar afirmația folosită pentru a dovedi încălcarea nu este deschisă și închisă. Da, când citești codul pare evident. Am stabilit doar lățimea și înălțimea, de ce nu este înălțimea la care am setat-o? Dar setarea lățimii și înălțimii sunt două operații separate și, la urma urmei, motivul pentru care avem metode pentru a seta proprietăți, mai degrabă decât să expunem proprietățile în sine, este astfel încât să ne putem menține invarianții, ceea ce poate duce la efecte secundare. - person wired_in; 13.10.2020
comment
În esență, spun că, deși aceasta poate fi o încălcare, este departe de a fi evident că este și, prin urmare, nu ar trebui să fie folosit ca exemplu pentru oamenii care încearcă să învețe acest concept. - person wired_in; 13.10.2020
comment
@wired_in Văd de unde vii, dar comentariul tău ignoră contextul specific al exemplului. Și anume, nu numim doar orice setter vechi pe orice tip de obiect vechi. Atribuim în mod special lățimea și înălțimea unui dreptunghi. Și asta vine cu așteptări implicite (un contract implicit). Așteptarea (implicita) este că modificarea lățimii nu modifică înălțimea și viceversa. Există motive pentru a schimba acest contract (la urma urmei, asta face un pătrat!). Dar în general acestea sunt așteptările naturale. Înlocuirea (cu un pătrat) încalcă această generalitate. - person Konrad Rudolph; 13.10.2020
comment
@KonradRudolph Nu am ignorat faptul că afirmația a fost în contextul unui Dreptunghi și nu am spus niciodată că nu a fost o încălcare a LSP. Nu este deloc evident că există un contract implicit care spune că schimbarea lățimii dreptunghiului nu poate modifica înălțimea și invers. Deoarece schimbarea lățimii este o operație separată de modificarea înălțimii, nu văd un contract implicit care să spună că un pătrat nu își poate impune invarianții atunci când lățimea sau înălțimea este schimbată izolat, ca dreptunghi. - person wired_in; 13.10.2020
comment
@KonradRudolph Exemplul dvs. de afirmație a forțat lățimea și înălțimea să fie setate în același timp prin combinarea a două operații separate, dar contractul real al unui dreptunghi are aceste operații ca separate, ceea ce face ca întregul exemplu să fie un exercițiu de interpretare, mai degrabă decât un încălcarea concretă a LSP - person wired_in; 13.10.2020
comment
@wired_in „Nu văd un contract implicit” — Nu îl vedeți pentru că este implicit. Ați putea face acest lucru explicit prin adăugarea de aserțiuni la setatorii clasei de bază 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
comment
@wired_in Am adăugat postcondiții explicite la declarația mea a clasei 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
comment
@KonradRudolph Problema este că invarianții impliciti sunt discutabile. Ele nu sunt neapărat puse în piatră. Interpretarea dvs. a ceea ce constituie un invariant implicit în acest caz este foarte mult opinie și se bazează pe contextul cererii. - person wired_in; 19.10.2020
comment
@KonradRudolph Pot argumenta că, deoarece 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
comment
@wired_in „Este doar atunci când încercați să forțați operațiunile independente să fie o singură operație” – Nu, nu le forțăm să fie o singură operație – în mod clar, nu sunt o singură operație. Ceea ce forțăm este un invariant, adică afirmații despre starea obiectului. Și, din nou, acesta este scopul LSP: invarianți. Este vorba despre invarianți, nu despre operații. - person Konrad Rudolph; 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 clasei Shape și trebuie schimbată ori de câte ori sunt create noi derivate ale Shape. Î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ă din Rectangle. [...]

Square va moșteni funcțiile SetWidth și SetHeight. Aceste funcții sunt complet nepotrivite pentru un Square, 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 suprascrie SetWidth și SetHeight [...]

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, obiectul Square 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.

[...]

person Phillip Wells    schedule 12.09.2008
comment
Mult târziu, dar am crezut că acesta este un citat interesant în lucrarea respectivă: 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
comment
@user2023861 Ai perfectă dreptate. Voi scrie un răspuns pe baza asta. - person inf3rno; 09.10.2017

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.
person Shelby Moore III    schedule 26.11.2011
comment
@Shelyby: Ai amestecat prea multe lucruri. Lucrurile nu sunt atât de confuze pe cât le spui. Multe dintre afirmațiile tale teoretice se bazează pe temeiuri slabe, cum ar fi „Pentru ca cunoașterea să existe, există multe posibilități neașteptate, .........” ȘI „în general, este o problemă de nedecidit dacă vreo mulțime este submulțimea altuia, de exemplu. moştenirea este în general indecidabilă' . Puteți porni un blog separat pentru fiecare dintre aceste puncte. Oricum, afirmațiile și presupunerile tale sunt foarte discutabile. Nu trebuie să folosiți lucruri de care nu știți! - person aknon; 27.12.2013
comment
@aknon Am un blog care explică aceste chestiuni mai în profunzime. Modelul meu TOE al spațiu-timpului infinit este frecvențele nelimitate. Nu este confuz pentru mine că o funcție inductivă recursivă are o valoare de început cunoscută cu o limită de sfârșit infinită, sau o funcție coinductivă are o valoare finală necunoscută și o limită de început cunoscută. Relativitatea este problema odată cu introducerea recursiunii. Acesta este motivul pentru care Turing complete este echivalent cu recursiunea nelimitată. - person Shelby Moore III; 16.03.2014
comment
@ShelbyMooreIII Mergi în prea multe direcții. Acesta nu este un răspuns. - person Soldalma; 09.12.2016
comment
@Soldalma este un răspuns. Nu îl vedeți în secțiunea Răspuns. Al tău este un comentariu pentru că se află în secțiunea de comentarii. - person Shelby Moore III; 23.12.2016
comment
@aknon în ceea ce privește acuzațiile tale de subțire, am blogging și scris de la răspunsul meu în 2014. - person Shelby Moore III; 23.12.2016
comment
Ca amestecul tău cu scala world! - person Ehsan M. Kermani; 28.06.2017

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.

person Steve Chamaillard    schedule 18.02.2018
comment
Exemplul nu încalcă LSP doar atâta timp cât constrângem semantica lui 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
comment
Mi s-a părut că acest răspuns este cel mai ușor de înțeles dintre restul. - person Malcolm Salvador; 11.02.2019
comment
este practic aplicarea LSP pe baze de date? Văd că majoritatea, dacă nu toate, operațiunile db vor trebui să fie împachetate și sunt vulnerabile la greșeli. Deși partea bună este că API-ul rămâne același, chiar dacă este SQL vs NoSQL. - person Sean W; 23.08.2020

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.

person Cù Đức Hiếu    schedule 13.08.2016
comment
De asemenea, sunt dezvoltator C# și voi spune că ultima dvs. afirmație nu este adevărată din Visual Studio 2010, cu framework-ul .Net 4.0. Covarianța tipurilor de returnare permite un tip de returnare mai derivat decât ceea ce a fost definit de interfață. Exemplu: Exemplu: IEnumerable‹T› (T este covariantă) IEnumerator‹T› (T este covariantă) IQueryable‹T› (T este covariantă) IGrouping‹TKey, TElement› (TKey și TElement sunt covariante) IComparer‹T› (T este contravariant) IEqualityComparer‹T› (T este contravariant) IComparable‹T› (T este contravariant) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx - person LCarter; 05.09.2017
comment
Răspuns excelent și concentrat (deși întrebările originale au fost mai mult despre exemple decât despre reguli). - person Mike; 12.06.2018

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/

person Lukas Lukac    schedule 28.10.2017
comment
Acesta este un exemplu mult mai bun. Mulțumesc! - person Kurt Campher; 14.02.2021

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.

person Charlie Martin    schedule 08.11.2008
comment
Dar... dacă aveți întotdeauna același comportament, atunci ce rost are să aveți clasa derivată? - person Leonid; 03.07.2012
comment

Este mult mai simplu decât crezi:

usr->next = group->users;
group->users = usr;
- person Charlie Martin; 04.07.2012
comment
@Charlie Martin, codificarea mai degrabă la o interfață decât la o implementare - am săpat asta. Acest lucru nu este unic pentru OOP; limbaje funcționale precum Clojure promovează și asta. Chiar și în ceea ce privește Java sau C#, cred că utilizarea unei interfețe mai degrabă decât utilizarea unei clase abstracte plus ierarhii de clasă ar fi naturală pentru exemplele pe care le oferiți. Python nu este puternic tipat și nu are interfețe, cel puțin nu în mod explicit. Dificultatea mea este că fac OOP de câțiva ani fără a adera la SOLID. Acum că am dat peste el, mi se pare limitativ și aproape auto-contradictiv. - person Hamish Grubijan; 06.07.2012
comment
Ei bine, trebuie să te întorci și să verifici lucrarea originală a Barbara. reports-archive.adm.cs. cmu.edu/anon/1999/CMU-CS-99-156.ps Nu este chiar menționat în termeni de interfețe și este o relație logică care se menține (sau nu) în orice limbaj de programare care are vreo formă de moștenire. - person Charlie Martin; 07.07.2012
comment
@HamishGrubijan Nu știu cine ți-a spus că Python nu este tastat puternic, dar te-au mințit (și dacă nu mă crezi, pornește un interpret Python și încearcă 2 + "2"). Poate confundați tastarea puternică cu tastarea static? - person asmeurer; 20.01.2013
comment
@asmeurer Hamish nu greșește. Tastat puternic nu este un termen bine definit; uneori este folosit pentru a însemna are un fel de verificare statică de tip (sensul lui Hamish), uneori a însemna nu efectuează constrângere de tip (înțelesul dvs. și, ca de obicei, numai în ceea ce privește cazul specific de „coerciție de tip șir la număr ' - N-am văzut niciodată pe cineva argumentând că C este scris slab pentru că puteți înmulți inții cu floats, sau că Python este pentru că puteți concatena 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
comment
Ei bine, din punctul de vedere al lui Liskov, acestea sunt diferite, pentru că funcționează întotdeauna. Dacă o limbă acceptă 2 + '2', tot nu va accepta 2 + 'two'. - person asmeurer; 27.12.2013
comment
În mod ciudat, @MarkAmery, articolul wiki pe care îl legați susține folosirea de către mine a „tactil puternic”. Doar pentru că oamenii l-au folosit greșit nu face definiția greșită. În 1974, Liskov și Zilles au descris un limbaj tip puternic ca fiind unul în care ori de câte ori un obiect este trecut de la o funcție de apelare la o funcție apelată, tipul său trebuie să fie compatibil cu tipul declarat în funcția apelată. - person Charlie Martin; 27.12.2013
comment
@CharlieMartin, dacă înțeleg corect răspunsul tău, atunci LSP este capacitatea de a înlocui orice instanță a unei clase părinte cu o instanță a uneia dintre clasele sale copil fără efecte secundare negative, corect? - person Daniel; 27.10.2018

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ă:

  1. 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.

  2. 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.
  3. 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

person snagpaul    schedule 18.12.2016

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() { ... }
}
person Khaled Qasem    schedule 10.02.2019

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.

person Chris Ammerman    schedule 11.09.2008

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?"

person avandeursen    schedule 25.03.2013

Î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.

person Alireza Rahmani khalili    schedule 16.08.2017

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.
person Rahamath    schedule 19.08.2019

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.

person Damien Pollet    schedule 03.04.2009
comment
Em, formularea aia este a Barbara Liskov. Barbara Liskov, „Data Abstraction and Hierarchy”, SIGPLAN Notices, 23,5 (mai, 1988). Nu este prea puternic, este exact corect și nu are implicația pe care crezi că o are. Este puternic, dar are exact cantitatea potrivită de putere. - person DrPizza; 30.11.2009
comment
Apoi, există foarte puține subtipuri în viața reală :) - person Damien Pollet; 09.12.2009
comment
Comportamentul este neschimbat nu înseamnă că un subtip vă va oferi exact aceleași valori concrete de rezultat. Înseamnă că comportamentul subtipului se potrivește cu ceea ce se așteaptă în tipul de bază. Exemplu: tipul de bază Shape ar putea avea o metodă draw() și să stipuleze că această metodă ar trebui să redea forma. Două subtipuri de Formă (de exemplu, Pătrat și Cerc) ar implementa ambele metoda draw(), iar rezultatele ar arăta diferit. Dar atâta timp cât comportamentul (redarea formei) se potrivea cu comportamentul specificat al Formei, atunci Pătrat și Cerc ar fi subtipuri de Formă în conformitate cu LSP. - person SteveT; 11.10.2012

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

person GauRang Omar    schedule 21.05.2018

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.

person johannesMatevosyan    schedule 24.01.2020

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ă.

person Ivan Porta    schedule 02.03.2020
comment
În metoda 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

person aknon    schedule 27.12.2013

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.

person Wouter    schedule 27.07.2015

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).

person Tom Hawtin - tackline    schedule 11.09.2008
comment
Cred că exemplul este pur și simplu pentru a demonstra că moștenirea de la bord nu are sens în contextul ThreeDBoard și toate semnăturile metodei sunt lipsite de sens cu o axa Z. - person NotMyself; 11.09.2008

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.

person Prasa    schedule 03.05.2016
comment
Vă rugăm să furnizați exemple de cod pe stackoverflow. - person sebenalern; 03.05.2016

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.

person inf3rno    schedule 09.10.2017

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();
}
person Zahra.HY    schedule 06.04.2019

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.

person Ryszard Dżegan    schedule 09.09.2013

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.

person Raghu Reddy Muttana    schedule 12.08.2016

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.

person Ishan Aggarwal    schedule 23.09.2017

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.

person Vadim Samokhin    schedule 12.08.2018

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{}
person Sarmad Sohail    schedule 22.11.2020

[SOLID]

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.

  1. Contravarianța argumentelor metodei în subtip.
  2. Covarianța tipurilor de returnare în subtip.
  3. 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ță]

person yoAlex5    schedule 11.11.2020

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

person prady00    schedule 26.04.2019
comment
Slab exemplu. Earth este o instanță a lui plant, de ce ar fi derivat din el? - person zar; 03.05.2019