Sunteți pe pagina 1din 12

Principiul Liskov de subtitutie

Functiile care utilizeaza pointeri sau trimiteri la clasele de baza trebuie s fie capabil s utilizeze obiecte ale claselor derivate fr s tie. Cele ce urmeaza sunt o parafrazare a Principiul Liskov al Substitutiei (LSP). Barbara Liskov a scris prima, dup cum urmeaz, in urma cu aproape 8 ani: Ceea ce este dorit aici este ceva de genul urmatoarei proprietati de substitutie: n cazul n care pentru fiecare obiect de tip S exista un obiect de tip T astfel c, pentru toate programele P definite n termeni de T, comportamentul P este neschimbat atunci cnd se substituie pentru , atunci S este un subtip de T.

Importana acestui principiu devine evident atunci cnd iau n considerare consecinele ncalcrii lui. Dac exist o funcie care nu sunt in conformitate cu LSP, atunci acea funcie folosete un pointer sau referin la o clasa de baza, dar trebuie s tie despre toate derivatele clasei de baza. O astfel de funcie ncalc principiul deschis-nchis, deoarece aceasta trebuie s fie modificat ori de cte ori un derivat din noua clasa de baza este creat.

Un exemplu simplu de o nclcare a LSP


Una dintre cele mai evidente nclcri ale acestui principiu este utilizarea de C + + Run-Time Type Information (RTTI) de a selecta o funcie pe baza tipului unui obiect : 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)); } [Not: static_cast este unul din noii operatori de distributie. n acest exemplu, funcioneaz exact ca o distributie regulata; adic DrawPatrat ((Patrat &) s);. Cu toate acestea, noua sintax are reguli mai stricte, care fac totul mai sigur de folositi, i este mai uor de a fi localizat cu instrumente, cum ar fi grep. Prin urmare, este preferat.] n mod clar funcia DrawShape este prost formata. Ea trebuie s tie despre fiecare derivata posibila ale clasei Shape, i trebuie s fie schimbata ori de cte ori noi derivate ale functiei Shape sunt create. ntr-adevr, muli consider structura acestei funcii ca anatema la Design Obiect Orientat.

Ptrat i Dreptunghi, o inclcare mai subtila

Cu toate acestea, exist i alte, mult mai subtile, modaliti de nclcarea LSP. Se ia n considerare o aplicaie care utilizeaz clasa Dreptunghi aa cum este descris mai jos: class Dreptunghi { 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; };

Imaginai-v c aceast aplicaie funcioneaz bine. Cum este cazul cu toate aplicaiile software de succes, atunci cand utilizatorii si au nevoie de schimbare, noi funcii sunt necesare. Imaginai-v c ntr-o zi utilizatorii cer sa aiba abilitatea de a manipula ptrate n loc de dreptunghiuri. Se spune adesea c, n C + +, motenirea este relaia ISA. Cu alte cuvinte, daca un nou tip de obiect poate inlocui relaia ISA cu un fel de obiect vechi, atunci clasa noului obiect ar trebui s fie derivat din clasa de obiecte vechi. n mod evident, un ptrat este un dreptunghi din toate punctele de vedere. Avnd n vedere c relaia ISA se mentine, este logic sspunem de clasa Square ca este derivata din Dreptunghi. (A se vedea Figura 1.)

Figura 1.

Dreptunghi

Patrat

Aceast utilizare a relaiei ISA este considerat de muli a fi una dintre tehnicile fundamentale de Analiza Obiect Orientata. Un ptrat este un dreptunghi, i atunci clasa

Patrat ar trebui s fie derivat din clasa Dreptunghi. Cu toate acestea, acest tip de gndire poate duce la unele probleme subtile, dar semnificative. n general, aceste probleme nu sunt prevzute pn cand nu incercam sa codam aplicatia. Primul nostru indiciu ar putea fi faptul c un Patrat nu are nevoie att de variabila itsHeight (inaltime) i de itsWidth (latime). Cu toate acestea, le va moteni oricum. n mod evident acest lucru este risipitor. Mai mult dect att, dac vom crea sute de mii de obiecte Patrat (de exemplu, un program CAD / CAE n care fiecare pin a fiecarei componente a unui circuit complex este trasat ca un ptrat), aceste deeuri ar putea fi extrem de semnificative. Cu toate acestea, s presupunem c nu suntem foarte preocupati de eficiena memoriei. Exist alte probleme? ntr-adevr! Patrat va moteni funciile SetWidth i SetHeight. Aceste funcii sunt extrem de nepotrivite pentru un Patrat, deoarece limea i nlimea unui ptrat sunt identice. Acest lucru ar trebui s fie un indiciu important c exist o problem cu designul. Cu toate acestea, exist o modalitate de a ocoli problema. Am putea trece peste SetWidth i SetHeight, dup cum urmeaz: void Patrat::SetWidth(double w) { Dreptunghi::SetWidth(w); Dreptunghi::SetHeight(w); } void Patrat::SetHeight(double h) { Dreptunghi::SetHeight(h); Dreptunghi::SetWidth(h); } Acum, cnd cineva stabilete limea unui obiect Patrat, inaltimea sa se va schimba corespunztor. i cnd cineva seteaza nlimea, lime se va schimba cu ea. Astfel, invarianiile Patrat rmn intacte. Obiectul Patrat va rmne un ptrat matematic adecvat. Patrat s; s.SetWidth(1); // Din fericire seteaza inaltimea si la 1. s,SetHeight(2); // seteaza latimea si inaltimea la 2, lucru bun. Dar sa luam in considerare urmatoarea functie: void f(Dreptunghi& r) { r.SetWidth(32); // apeleaza Dreptunghi::SetWidth } Dac trecem o referin la un obiect Patrat n aceast funcie, obiectul Patrat va fi corupt, deoarece nlimea nu va fi schimbata. Aceasta este o nclcare clar a LSP. Funcia f nu funcioneaz pentru derivaiile argumentelor sale. Motivul eecului este faptul c SetWidth i SetHeight nu au fost declarate virtual n Dreptunghi. Putem rezolva aceast problem cu uurin. Cu toate acestea, n cazul n care crearea unei clase derivate ne determin s facem modificri la clasa de baza,implica de

multe ori faptul ca designul este defect. ntr-adevr, aceasta ncalc principiul deschis-nchis. Am putea contracara acest lucru cu argumentul c uitarea de a face SetWidth i SetHeight virtuale a fost adevaratul defect al designului, iar noi doar fixam acest lucru acum. Cu toate acestea, acest lucru este greu de justificat, deoarece stabilirea nlimii i limii unui dreptunghi sunt operaiuni extrem de primitive. Prin ce raionament le-am face virtual dac nu am anticipa existena Patrat. Cu toate acestea, s presupunem c am accepta argumentul, i repara clasele. Am avea urmtorul cod: class Dreptunghi { public: virtual void SetWidth(double w) {itsWidth=w;} virtual void SetHeight(double h) {itsHeight=h;} double GetHeight() const {return itsHeight;} double GetWidth() const {return itsWidth;} private: double itsHeight; double itsWidth; }; class Patrat : public Dreptunghi { public: virtual void SetWidth(double w); virtual void SetHeight(double h); }; void Patrat::SetWidth(double w) { Dreptunghi::SetWidth(w); Dreptunghi::SetHeight(w); } void Patrat::SetHeight(double h) { Dreptunghi::SetHeight(h); Dreptunghi::SetWidth(h); }

Adevarata problema
n acest moment avem dou clase, Patrat si Dreptunghi, care par sa mearga. Indiferent de ceea ce facem unui obiect Patrat, acesta va rmne n concordan cu un ptrat matematic. i indiferent de ceea ce faci unui obiect Dreptunghi, acesta va rmne un dreptunghi matematic. Mai mult dect att, putei trece un ptrat ntr-o funcie care accept un pointer sau referin la un dreptunghi, i Patratul va aciona n continuare ca un ptrat i va rmne consecvent. Astfel, am putea concluziona c modelul este acum de sine consistent, i corect. Cu toate acestea, aceast concluzie ar fi rea. Un model care este de la sine consistent, nu este neaprat n concordan cu toi utilizatorii si! Luai n considerare funcia g de mai jos.

void g(Dreptunghi& r) { r.SetWidth(5); r.SetHeight(4); assert(r.GetWidth() * r.GetHeight()) == 20); } Aceast funcie invoc membrii SetWidth i SetHeight a ceea ce se consider a fi un Dreptunghi. Funcia functioneaza foarte bine pentru un Dreptunghi, dar declar o eroare, dac a trecut la ptrat. Deci, aici este problema reala: A avut justificare programatorul care a scris c n funcie schimbarea limii unui Dreptunghi lasa nlimea neschimbata? n mod clar, programatorul de g a facut aceasta presupunere foarte rezonabila. Trecerea unui Patrat la funciile ale cror programatori au fcut aceast presupunere va duce la probleme. Prin urmare, exist funcii care au pointeri sau trimiteri la obiecte Dreptunghi, dar nu poate funciona corect la obiecte Patrat. Aceste funcii expun o nclcare a LSP. Adugarea derivatei Patrat la Dreptunghi a rupt aceste funcii; i aa a principiului deschisnchis a fost nclcat.

Valabilitatea nu este intrinsec


Acest lucru ne conduce la o concluzie foarte important. Un model, privit n mod izolat, nu poate fi validat in mod semnificativ. Perioada de valabilitate a unui model poate fi exprimat doar n termeni de clienii si. De exemplu, atunci cnd am examinat versiunea final a claselor Patrat i Dreptunghi n mod izolat, am constatat c au fost de sine consistente i valide. Totui, cnd ne-am uitat la ele din punctul de vedere al unui programator care a fcut presupuneri rezonabile cu privire la clasa de baza, modelul s-a stricat. Astfel, atunci cnd se analizeaz dac un anumit design este potrivit sau nu , nu trebuie s vedem soluiile n mod izolat. Trebuie sa vedem totul in termenii unei presupuneri rezonabile care va fi facuta de utilizatorii designului.

Ce nu a mers?
Deci, ce s-a ntmplat? De ce modelul aparent rezonabil ale Patrat i Dreptunghi nu merg . Cu toate acestea, nu este un Patrat un Dreptunghi? Nu ine relaia ISA? Nu! Un ptrat ar putea fi un dreptunghi, dar un obiect Patrat cu siguranta nu este un obiect Dreptunghi. De ce? Deoarece comportamentul unui obiect Patrat nu este in conformitate cu comportamentul unui obiect Dreptunghi. Comportamental, un Patrat nu este un Dreptunghi! Oar softwareul este totul despre comportament de fapt. LSP face clar faptul c n OOD relaia ISA se refer la comportament. Nu comportamentul intrinsec privat, dar comportamentul extrinsec public; comportament de care clientii depind. De exemplu, autorul funciei g a depins de faptul c dreptunghiurile se comporta astfel nct nlimea i limea lor variaz independent unul de altul. Acea independena intre dou variabile este un comportament extrinsec public de care ali programatori tind sa depinda. Pentru ca LSP s se menin, i cu el principiului deschis-

nchis, toate derivatele trebuie sa fie in conformitate cu comportamentul pe care clienii il ateapt de la clasele de baz pe care le utilizeaz.

Contract de proiectare
Exist o relaie puternic ntre LSP i conceptul de design prin contract dupa cum este aratat de Bertrand Meyer . Folosind acest sistem, metodele de clase declara precondiii i postconditii. Preconditiile trebuie s fie valabile pentru ca metoda sa execute. Dup finalizarea, garaniile metoda garanteaza ca postconditia va fi adevrat. Putem vedea postconditia a Dreptunghi:: SetWidth (double w) ca: assert((itsWidth == w) && (itsHeight == old.itsHeight)); Acum regula pentru precondiiile i postconditiile pentru derivate, astfel cum a subliniat Meyer, este: ... Atunci cnd se redefineste o rutina [ntr-o derivata], putei nlocui doar condiie sale de ctre unele mai slabe, i postconditia de ctre una mai puternica. Cu alte cuvinte, atunci cnd se utilizeaz un obiect prin intermediul interfaei clasei de baz, utilizatorul tie doar precondiiile i postconditiile din clasa de baza. Astfel, obiectele derivate nu trebuie s atepte ca utilizatorii s ia in considerare astfel de precondiii care sunt mai puternice decat cele cerute de ctre clasa de baza. Ele trebuie s accepte orice clasa de baza ar putea accepta. De asemenea, clasele derivate trebuie s fie in conformitate cu toate postconditiile bazei. Comportamentele lor i ieirile nu trebuie s ncalce nicio constrngere stabilita pentru clasa de baza. Utilizatorii din clasa de baza nu trebuie s fie confundati de ieirea din clasa derivata. n mod clar, postconditia de Patrat:: SetWidth (double w) este mai slab dect postconditia de Dreptunghi:: SetWidth (double w), deoarece aceasta nu este in conformitate cu clauza clasei de baza (itsHeight == old.itsHeight). Astfel, Patrat:: SetWidth (double w ) ncalc contractul clasei de baza. Anumite limbi, cum ar fi Eiffel, au suport direct pentru precondiii i postconditii. Le poti chiar declara , i au sistemul runtime care le verifica pentru tine. C + + nu are o astfel de facilitate. Cu toate acestea, chiar i n C + + putem considera manual precondiiile i postconditiile fiecarei metode, i trebuie sa ne asiguram c regula Meyer nu este nclcata. Mai mult dect att, poate fi foarte util sa documentam aceste condiii prealabile i postconditii in comentarii pentru fiecare metod.

Un exemplu real
Destul cu ptrate i dreptunghiuri! Are LSP o inciden asupra software-ul real? S ne uitm asupra unui studiu de caz.

Motivarea
Barbara Liskov nu era multumita de interfata claselor container care au fost disponibile prin intermediul prilor tere. Nu a vrut ca codul aplicatiei s fie dependent de

aceste containere, deoarece a dorit s le nlocuiasc cu clase mai bune mai tarziu. Astfel, a nfurat containerele tere n propria ei interfa abstract. (A se vedea figura 2)

Figura 2. Ierarhia container

Set

ThirdParty BoundedSet BoundedSet

UnboundedSet

ThirdParty UnboundedSet

A avut o clas abstract numit Set care a prezentat pur virtual Add , Delete, i funcii IsMember. template <class T> class Set { public: virtual void Add(const T&) = 0; virtual void Delete(const T&) = 0; virtual bool IsMember(const T&) const = 0; };

Aceast structur a unificat varietatile nelimitate i mrginite ale dou seturi tere i le-a permis s fi accesate printr-o interfa comun. Astfel, unii clienti ar putea accepta un argument de tip Set <T> & si nu il va interesa daca adevaratul Set cu care a lucrat a fost din varietatea nelimitata sau cea limitata. (A se vedea lista funcia PrintSet.)
template <class T> void PrintSet(const Set<T>& s) { for (Iterator<T>i(s); i; i++) cout << (*i) << endl; }

Acest abilitatea de a nu ti i a nu te interesa tipul de Set pe care operam este un mare avantaj. Aceasta nseamn c programatorul poate decide ce fel de Set este necesar n fiecare caz particular. Nici una din funciile de client nu vor fi afectate de aceast decizie. Programatorul poate alege un BoundedSet ( Set Limitat) atunci cnd memoria este mica i viteza nu este critic, sau programatorul poate alege o UnboundedSet(SetNelimitat) atunci cnd memoria este abundenta si viteza este critic. Funciile clientului vor manipula aceste obiecte prin intermediul interfeei clasei de baz Set, i nu va ti, prin urmare,ce fel de Set foloseste.

Problema
A vrut s adauge un Persistent-Set pentru aceast ierarhie. Un set persistent este un set care poate fi scris intr-un siroi, i apoi poate fi citit din nou mai trziu, eventual, de o aplicaie diferit. Din pcate, singurul container parti tere la care a avut acces , care a oferit, de asemenea, persisten, nu a fost o clas ablon. n schimb, a acceptat obiecte care au fost derivate din clasa de baza abstracta PersistentObject. S-a creat ierarhia prezentat n Figura 3.

Figura 3. Persistent Set

Set
PersistentObject

PersistentSet

ThirdParty PersistentSet

La suprafata, acesta ar putea arata corect. Cu toate acestea exist o implicaie care este destul de urta. Atunci cnd un client adauga membrii la clasa de baz Set, cum poate acel client sa asigure faptul ca adauga numai derivate ale PersistentObject daca Set se intampla sa fie PersistentSet? Luai n considerare codul pentru PersistentSet::Add: template <class T> void PersistentSet::Add(const T& t) { PersistentObject& p = dynamic_cast<PersistentObject&>(t); // throw bad_cast itsThirdPartyPersistentSet.Add(p); } Acest cod face clar c n cazul n care orice client ncearc sa adauge un obiect care nu este derivat din clasa PersistentObject la PersistentSet al meu, o eroare de runtime va rezulta. Dynamic_cast va pune bad_cast (unul dintre obiectele standard de excepie). Nici unul dintre clienii existeni ai clasei de baza abstracte Set nu ateapt excepiile s fie pus pe Add. Din moment ce aceste funcii vor fi confundate cu o derivata a lui Set, aceast schimbare a ierarhiei ncalc LSP. Este aceasta o problem? Cu siguran. Funcii care niciodat nu au esuat inainte cand au trecut o derivata a lui Set,acum vor provoca erori de rulare atunci cnd trec de un PersistentSet. Depanarea acestui tip de problem este destul de dificila, deoarece runtime error apare foarte departe de adevaratul defect logic. Defectul logic, este fie decizia de a trece o PersistentSet ntr-o funcie euata, sau aceasta este decizia de a aduga un obiect la PersistentSet, care nu este derivat din PersistentObject. n ambele cazuri, decizia ar putea fi la milioane de instructiuni departare de invocatia actuala a metodei Add. Gsirea acesteia poate fi un chin. Fixarea poate fi mai ru.

O soluie care nu este in conformitate cu LSP.


Cum putem rezolva aceasta problema? Cu civa ani n urm, Barbara Liskov a rezolvat-o prin convenie. Ceea ce inseamna ca nu a rezolvat-o prin codul sursa. Mai degrab a instaurat o convenie prin care PersistentSet i PersistentObject nu au fost cunoscute aplicatiei ca un ntreg. Erau cunoscute doar unui singur modul. Acest modul a fost responsabil pentru citirea i scrierea tuturor containerelor. Atunci cnd un container este necesar s fie scris, coninutul acestuia au fost copiate n PersistentObjects i apoi adaugate la PersistentSets, care au fost apoi salvate pe un flux. Atunci cnd un container a avut nevoie sa fie citit de la un flux, procesul a fost inversat. Un PersistentSet a fost citit de flux, iar apoi PersistentObjects au fost scoase din PersistentSet i copiat n obiecte normale (non-persistente), obiecte care au fost apoi adugate la un Set normal. Aceast soluie poate prea prea restrictiva, dar a fost singura cale prin care a putut preveni aparitia obiectelor PersistentSet la interfaa funciilor care ar dori s adauge obiecte non-persistente. Mai mult, a rupt dependena de restul aplicatiei asupra ntregii noiuni de persisten.

A mers aceasta solutie? Nu chiar. Convenia a fost nclcata n mai multe pri ale cererii de ctre inginerii care nu au neles necesitatea ei. Asta este problema cu conveniile, acestea trebuie s fie continuu re-vndute la fiecare inginer. n cazul n care inginerul nu este de acord, atunci conveniile vor fi nclcate. i o nclcare ruineaza ntreaga structur.

O solutie LSP
Cum a rezolvat acest lucru acum? A recunoscut faptul ca PersistentSet nu are o relatie ISA cu Set, c nu este o derivata adecvat pentru Set. Astfel, a separa ierarhiile. Dar nu complet. Exist caracteristici pe care Set i PersistentSet le au n comun. De fapt, doar metoda Add este cea care creaza dificcultati cu LSP. Astfel, a creat o ierarhie n care att Set cat i PersistentSet sunt in legatura sub o interfata care a permis testarea membrilor,iteratiilor etc(a se vedea figura 4.) Figura4. Ierarhie separata

Iterable container

Member Container

Persistent Object

ThirdParty Set
Abstract

PersistentSet

PersistentSet

Acest lucru ar permite obiectelor PersistentSet s fie iterate i testate pentru calitatea de membru, etc. Dar nu ar permite posibilitatea de a aduga obiecte care nu au fost derivate din PersistentObject la un PersistentSet. template <class T> void PersistentSet::Add(const T& t) { itsThirdPartyPersistentSet.Add(t); // This will generate a compiler error if t is // not derived from PersistentObject. }

Cum arata lista de mai sus, orice ncercare de a aduga un obiect care nu este derivat din PersistentObject la o PersistentSet va duce la o eroare de compilare. (Interfata celui de-al treilea set persistent se ateapt la o PersistentObject &).

Concluzii
Principiul deschis-nchis se afl n centrul multora dintre afirmaiile fcute de OOD. Este atunci cnd acest principiu este n vigoare faptul c aplicatiile sunt mai usor de ntreinut, reutilizabile i robuste. Principiul Liskov Substitutie este o caracteristic important a tuturor programelor care sunt in conformitate cu principiul deschis-nchis. Este doar atunci cnd tipurile de derivate sunt complet substituibile pentru tipurile de baza, c funciile care utilizeaz aceste tipuri de baz pot fi refolosite cu impunitate, i tipuri de derivate pot fi schimbate cu impunitate.

Bibliografie

http://ro.wikipedia.org/wiki/Barbara_Liskov http://www.objectmentor.com/resources/articles/lsp.pdf http://en.wikipedia.org/wiki/Liskov_substitution_principle http://www.oodesign.com/liskov-s-substitution-principle.html

S-ar putea să vă placă și