Responsabilitatea unică (SRP), deschisă / închisă (OCP), Înlocuirea lui Liskov, segregarea interfeței, și inversarea dependenței. Cinci principii agile care ar trebui să vă ghideze de fiecare dată când scrieți cod.
Deoarece atât principiul de substituție Liskov (LSP), cât și principiul de separare a interfeței (ISP) sunt destul de ușor de definit și de exemplificat, în această lecție vom vorbi despre ambele.
Clasele de copii nu ar trebui să încalce definițiile tipului de clasă părinte.
Conceptul acestui principiu a fost introdus de Barbara Liskov într-o conferință din 1987 și ulterior publicată într-o lucrare împreună cu Jannette Wing în 1994. Definiția lor inițială este următoarea:
Fie q (x) o proprietate probabilă pentru obiectele x de tip T. Atunci q (y) ar trebui să fie probabilă pentru obiectele y de tip S unde S este un subtip de T.
Ulterior, prin publicarea principiilor SOLID de către Robert C. Martin în cartea sa Agile Software Development, principii, modele și practici și apoi republicată în versiunea C # a cărții Agile Principles, Patterns and Practices în C #, definiția a devenit cunoscut sub numele de principiul de substituție Liskov.
Aceasta ne conduce la definiția dată de Robert C. Martin:
Subtipurile trebuie să fie substituibile pentru tipurile de bază ale acestora.
La fel de simplu, o subclasă ar trebui să suprascrie metodele clasei părinte într-un mod care să nu întrerupă funcționalitatea din punctul de vedere al clientului. Iată un exemplu simplu pentru a demonstra conceptul.
clasa Vehicle funcția startEngine () // Funcția inițială a funcției de pornire a motorului accelera () // Funcția implicită de accelerare
Având o clasă Vehicul
- poate fi abstractă și două implementări:
clasa Masina extinde Vehicul function startEngine () $ this-> engageIgnition (); părinte :: startEngine (); funcția privată engageIgnition () // Procedură de aprindere Clasa ElectricBus extinde Vehicle function accelerate () $ this-> increaseVoltage (); $ This-> connectIndividualEngines (); funcția privată increaseVoltage () // Electric logic funcția privată connectIndividualEngines () // Logic conexiune
O clasă de clienți ar trebui să poată utiliza oricare dintre ele, dacă poate folosi Vehicul
.
driver de clasă funcție merge (Vehicle $ v) $ v-> startEngine (); $ V-> accelera ();
Ceea ce ne conduce la o simplă implementare a modelului de proiectare a metodei șablonului așa cum l-am folosit în tutorialul OCP.
Pe baza experienței noastre anterioare cu principiul Open / Closed, putem concluziona că principiul de substituție al lui Liskov este în strânsă legătură cu OCP. De fapt, "o încălcare a LSP este o încălcare latentă a OCP" (Robert C. Martin), iar modelul de model de proiectare este un exemplu clasic de respectare și implementare a LSP, care la rândul său este una dintre soluțiile care respectă OCP.
Pentru a ilustra acest lucru complet, vom merge cu un exemplu clasic, deoarece este foarte semnificativ și ușor de înțeles.
clasa dreptunghiulară private $ topLeft; lățime privată $; suma înălțimii private; funcția publică setHeight ($ înălțime) $ this-> height = $ height; funcția publică getHeight () return $ this-> height; funcția publică setWidth ($ width) $ this-> width = $ width; funcția publică getWidth () return $ this-> width;
Începem cu o formă geometrică de bază, a Dreptunghi
. Este doar un obiect de date simplu cu setters și getters pentru lăţime
și înălţime
. Imaginați-vă că aplicația noastră funcționează și este deja implementată pentru mai mulți clienți. Acum au nevoie de o nouă caracteristică. Trebuie să poată manipula pătrate.
În realitate, în geometrie, un pătrat este o formă particulară de dreptunghi. Așadar, am putea încerca să punem în aplicare a Pătrat
clasa care extinde a Dreptunghi
clasă. Se spune frecvent că o clasă pentru copii este a clasa părintească și această expresie este, de asemenea, conformă cu LSP, cel puțin la prima vedere.
Dar este a Pătrat
într-adevăr Dreptunghi
în programare?
clasa Square extinde Rectangle public function setHeight (valoarea $) $ this-> width = $ value; $ this-> height = value value; funcția publică setWidth (valoare $) $ this-> width = $ value; $ this-> height = value value;
Un pătrat este un dreptunghi cu lățimea și înălțimea egală și am putea face o implementare ciudată ca în exemplul de mai sus. Am putea suprascrie ambii setteri pentru a seta atât înălțimea, cât și lățimea. Dar cum ar afecta codul clientului?
clasa Client zona funcțieiVerificator (dreptunghi $ r) $ r-> setWidth (5); $ R-> setHeight (4); dacă ($ r-> area ()! = 20) aruncați o nouă Excepție ("Zonă rea!"); return true;
Este posibil să existe o clasă de clienți care verifică zona dreptunghiului și aruncă o excepție dacă este greșită.
zonă de funcții () return $ this-> width * $ this-> height;
Bineînțeles că am adăugat metoda de mai sus pentru a noastră Dreptunghi
clasa pentru a furniza zona.
clasa LspTest extinde PHPUnit_Framework_TestCase function testRectangleArea () $ r = new Rectangle (); $ c = Client nou (); $ This-> assertTrue ($ c-> areaVerifier ($ r));
Și am creat un test simplu prin trimiterea unui obiect dreptunghiular gol către verificatorul zonei și testul trece. Dacă noi Pătrat
clasa este corect definită, trimițând-o la client areaVerifier ()
nu ar trebui să-și întrerupă funcționalitatea. La urma urmei, a Pătrat
este a Dreptunghi
în toate sensurile matematice. Dar este clasa noastră?
funcția testSquareArea () $ r = new Square (); $ c = Client nou (); $ This-> assertTrue ($ c-> areaVerifier ($ r));
Testarea este foarte ușoară și sparge timpul mare. O excepție ne este aruncată atunci când conducem testul de mai sus.
PHPUnit 3.7.28 de Sebastian Bergmann. Excepție: zonă rea! # 1 / paht /: / ... / ... /LspTest.php(18): Client-> areaVerifier (obiect (pătrat)) # 1 [funcția internă]: LspTest-> testSquareArea
Deci, ale noastre Pătrat
clasa nu este a Dreptunghi
dupa toate acestea. Se încalcă legile geometriei. Eșuează și încalcă principiul de substituție Liskov.
Îmi place mai ales acest exemplu, deoarece nu numai că încalcă LSP, dar demonstrează că programarea orientată obiect nu vizează cartografierea reală a obiectelor. Fiecare obiect din programul nostru trebuie să fie o abstractizare asupra unui concept. Dacă încercăm să cartografice obiecte reale la obiecte programate, vom greși aproape întotdeauna.
Principiul responsabilității unice se referă la actori și arhitecturi de nivel înalt. Principiul Open / Closed se referă la designul de clasă și extensiile de caracteristici. Principiul de substituție Liskov este despre subtipare și moștenire. Principiul de Segregare a Interfeței (ISP) este o logică de afaceri pentru comunicarea clienților.
În toate aplicațiile modulare trebuie să existe un fel de interfață pe care să se poată baza clientul. Acestea pot fi entități reale tipizate de interfață sau alte obiecte clasice care implementează modele de proiectare precum fațadele. Nu contează ce soluție este utilizată. Are întotdeauna același domeniu de aplicare: să comunice codul clientului cu privire la modul de utilizare a modulului. Aceste interfețe se pot afla între diferite module din aceeași aplicație sau proiect sau între un proiect ca o bibliotecă terță parte care deservește alt proiect. Din nou, nu contează. Comunicarea este comunicare, iar clienții sunt clienți, indiferent de persoanele care scriu codul.
Deci, cum ar trebui să definim aceste interfețe? Am putea să ne gândim la modulul nostru și să expunem toate funcționalitățile pe care vrem să le oferim.
Acest lucru pare a fi un început bun, o modalitate foarte bună de a defini ce vrem să implementăm în modulul nostru. Sau este? Un astfel de început va duce la una din cele două posibile implementări:
Mașină
sau Autobuz
clasa de punere în aplicare a tuturor metodelor de pe Vehicul
interfață. Doar dimensiunile pură ale acestor clase ar trebui să ne spună să le evităm cu orice preț.LightsControl
, Control de viteza
, sau RadioCD
care toate pun în aplicare întreaga interfață, dar de fapt oferă ceva util numai pentru părțile pe care le implementează.Este evident că nici o soluție nu este acceptabilă pentru implementarea logicii noastre de afaceri.
Am putea lua o altă abordare. Sparge interfața în bucăți, specializată pentru fiecare implementare. Acest lucru ar ajuta la utilizarea unor clase mici care să aibă grijă de interfața proprie. Obiectele care implementează interfețele vor fi utilizate de diferitele tipuri de vehicule, cum ar fi mașina din imaginea de mai sus. Automobilul va utiliza implementările, dar va depinde de interfețe. Deci o schemă ca cea de mai jos poate fi și mai expresivă.
Dar acest lucru ne schimbă fundamental percepția asupra arhitecturii. Mașină
devine clientul în locul implementării. Încă mai dorim să oferim clienților noștri modalități de a utiliza întregul nostru modul, care este un tip de vehicul.
Să presupunem că am rezolvat problema implementării și că avem o logică de afaceri stabilă. Cel mai simplu lucru este să oferiți o singură interfață cu toate implementările și să lăsați clienții, în cazul nostru Stație de autobuz
, șosea
, Conducător auto
și așa mai departe, să folosești orice vrei de la implementarea interfeței. Practic, aceasta schimbă responsabilitatea de selecție a comportamentului către clienți. Puteți găsi acest tip de soluție în multe aplicații mai vechi.
Principiul de segregare a interfeței (ISP) prevede că niciun client nu ar trebui să fie obligat să depindă de metodele pe care nu le utilizează.
Cu toate acestea, această soluție are problemele sale. Acum, toți clienții depind de toate metodele. De ce ar trebui să Stație de autobuz
depinde de starea luminilor autobuzului sau de canalele radio selectate de șofer? Nu ar trebui. Dar dacă se întâmplă? Conteaza? Ei bine, dacă ne gândim la principiul unic responsabilitate, acesta este un concept sora pentru acest lucru. Dacă Stație de autobuz
depinde de multe implementări individuale, nici măcar utilizate de acesta, poate necesita modificări dacă se schimbă oricare dintre implementările mici individuale. Acest lucru este valabil mai ales pentru limbile compilate, dar putem vedea încă efectul Control de lumini
schimbare impact Stație de autobuz
. Aceste lucruri nu ar trebui să se întâmple niciodată.
Interfețele aparțin clienților lor și nu implementărilor. Astfel, ar trebui să le proiectăm întotdeauna într-un mod cât mai potrivit pentru clienții noștri. Uneori putem, uneori nu ne putem cunoaște exact clienții noștri. Dar când putem, ar trebui să ne rupem interfețele în mai multe, astfel încât acestea să satisfacă mai bine nevoile exacte ale clienților noștri.
Desigur, acest lucru va duce la un anumit grad de duplicare. Dar amintește-ți! Interfețele sunt doar definiții de nume de funcții simple. Nu există nici o implementare a vreunui tip de logică în ele. Deci, duplicarea este mică și ușor de gestionat.
Apoi, avem marele avantaj al clienților care depinde numai de ceea ce au de fapt nevoie și de utilizat. În unele cazuri, clienții pot folosi și necesită mai multe interfețe, adică OK, atâta timp cât folosesc toate metodele din toate interfețele de care depind.
Un alt truc frumos este că, în logica noastră de afaceri, o singură clasă poate implementa mai multe interfețe dacă este necesar. Așadar, putem oferi o singură implementare pentru toate metodele comune între interfețe. Interfețele segregate ne vor forța să ne gândim mai mult la codul nostru din punctul de vedere al clientului, ceea ce va conduce, la rândul său, la o cuplare slabă și o testare ușoară. Deci, nu numai că am făcut codul nostru mai bun pentru clienții noștri, dar și noi am reușit să înțelegem, să testăm și să le implementăm mai ușor.
LSP ne-a învățat de ce realitatea nu poate fi reprezentată ca o relație unu-la-unu cu obiecte programate și cum subtipurile trebuie să-și respecte părinții. De asemenea, am pus-o în lumina celorlalte principii pe care le-am cunoscut deja.
ISP ne învață să respectăm clienții noștri mai mult decât am considerat necesar. Respectarea nevoilor lor va face codul nostru mai bun și viața noastră ca programatori mai ușoară.
Multumesc pentru timpul acordat.