SOLID Partea 2 - Principiul deschis / închis

Responsabilitatea unică (SRP), Open / Closed (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 aveți nevoie să scrieți cod.

Definiție

Entitățile de software (clase, module, funcții etc.) ar trebui să fie deschise pentru extindere, dar închise pentru modificare.

Principiul Open / Closed, OCP pe scurt, este creditat lui Bertrand Mayer, programator francez, care la publicat pentru prima data in cartea sa "Object-Oriented Software Construction" in 1988.

Principiul a crescut în popularitate la începutul anilor 2000, când a devenit unul dintre principiile SOLID definite de Robert C. Martin în cartea sa Agile Software Development, Principles, Patterns and Practices și ulterior republicată în versiunea C # a cărții Agile Principles, Patterns , și Practici în C #.

Ceea ce vorbim aici este de a proiecta modulele, clasele și funcțiile noastre într-un mod care, atunci când este nevoie de o nouă funcționalitate, nu ar trebui să modificăm codul existent, ci să scriem un nou cod care va fi folosit de codul existent. Sună puțin cam ciudat, mai ales dacă lucrăm în limbi precum Java, C, C ++ sau C # unde se aplică nu numai codului sursă, ci și cel binar. Vrem să creăm noi funcții în moduri care nu vor necesita redistribuirea binarelor, executabilelor sau DLL-urilor existente.

OCP în contextul SOLID

Pe măsură ce progresăm cu aceste tutoriale, putem pune fiecare nou principiu în contextul celor deja discutate. Am discutat deja responsabilitatea unică (SRP) care a declarat că un modul ar trebui să aibă doar un singur motiv pentru a se schimba. Dacă ne gândim la OCP și SRP, putem observa că ele sunt complementare. Codul special conceput cu SRP în minte va fi aproape de principiile OCP sau ușor de respectat de aceste principii. Când avem un cod care are un singur motiv de schimbare, introducerea unei noi caracteristici va crea un motiv secundar pentru această modificare. Astfel, atât SRP cât și OCP ar fi încălcate. În același mod, dacă avem un cod care ar trebui să se schimbe doar atunci când funcția sa principală se schimbă și ar trebui să rămână neschimbată atunci când o nouă caracteristică este adăugată, respectând astfel OCP, va respecta în cea mai mare parte SRP și.

Acest lucru nu înseamnă că SRP duce întotdeauna la OCP sau invers, dar în majoritatea cazurilor dacă unul dintre ele este respectat, realizarea celui de-al doilea este destul de simplă.

Exemplul evident al încălcării OCP

Din punct de vedere pur tehnic, principiul Open / Closed este foarte simplu. O relație simplă între două clase, cum este cea de mai jos, încalcă OCP.


Utilizator clasa utilizează Logică clasa direct. Dacă trebuie să implementăm un al doilea Logică clasa într-un mod care ne va permite să folosim atât cea actuală, cât și cea actuală, cea existentă Logică clasa va trebui să fie schimbată. Utilizator este direct legat de punerea în aplicare a Logică, nu există nici o modalitate pentru noi să oferim un nou Logică fără a afecta cel actual. Și când vorbim despre limbi statice, este foarte posibil ca Utilizator clasa va necesita, de asemenea, modificări. Dacă vorbim despre limbi compilate, cu siguranță atât Utilizator executabil și Logică biblioteca executabilă sau dinamică va necesita recompilarea și redistribuirea către clienții noștri, proces pe care dorim să-l evităm ori de câte ori este posibil.

Arată-mi codul

Bazându-se numai pe schema de mai sus, se poate deduce că orice clasă care utilizează direct o altă clasă ar încălca, de fapt, principiul Open / Closed. Și asta este drept, strict vorbind. Mi sa părut destul de interesant să găsesc limitele, momentul în care trageți linia și să decideți că este mai dificil să respectați OCP decât să modificați codul existent sau costul arhitectural nu justifică costul schimbării codului existent.

Să presupunem că vrem să scriem o clasă care să poată furniza progresul ca procentaj pentru un fișier care este descărcat prin aplicația noastră. Vom avea două clase principale, a progres și a Fişier, și cred că vom dori să le folosim ca în testul de mai jos.

funcția testItCanGetTheProgressOfAFileAsAPercent () $ file = fișier nou (); $ file-> length = 200; $ file-> trimis = 100; $ progress = Progress nou (fișier $); $ this-> assertEquals (50, $ progres-> getAsPercent ()); 

În acest test suntem un utilizator de progres. Vrem să obținem o valoare ca procent, indiferent de dimensiunea reală a fișierului. Folosim Fişier ca sursă de informație pentru noi progres. Un fișier are o lungime în octeți și un câmp numit trimis reprezentând cantitatea de date trimisă celui care efectuează descărcarea. Nu ne interesează modul în care aceste valori sunt actualizate în aplicație. Putem presupune că există o logică magică care o face pentru noi, deci într-un test le putem stabili în mod explicit.

fișier clasă public $ length; public $ trimis; 

Fişier clasa este doar un obiect de date simplu care conține cele două câmpuri. Desigur, în viața reală, ar conține probabil și alte informații și comportamente, cum ar fi numele fișierului, calea, calea relativă, directorul curent, tipul, permisiunile și așa mai departe.

Progress class fișier privat $; funcția __construct (fișier $ file) $ this-> file = $ file;  funcția getAsPercent () return $ this-> file-> sent * 100 / $ this-> file-> length; 

progres este pur și simplu o clasă luând a Fişier în constructorul său. Pentru claritate, am specificat tipul variabilei în parametrii constructorului. Există o singură metodă utilă progres, getAsPercent (), care va lua valorile expediate și lungimea de la Fişier și transformă-le într-un procent. Simplu și funcționează.

Testarea a început la ora 17:39 ... PHPUnit 3.7.28 de Sebastian Bergmann ... Timp: 15 ms, Memorie: 2.50Mb OK (1 test, 1 afirmație)

Acest cod pare să aibă dreptate, dar încalcă Principiul Deschis / Închis. Dar de ce? Si cum?

Modificarea cerințelor

Fiecare aplicație care se așteaptă să evolueze în timp va avea nevoie de noi caracteristici. O caracteristică nouă pentru aplicația noastră ar putea fi să permitem streaming-ul muzicii, în loc să descărcăm doar fișiere. Fişierlungimea este reprezentată în octeți, durata muzicii în câteva secunde. Vrem să oferim un bara de progres frumos ascultătorilor noștri, dar putem reutiliza pe cea pe care o avem deja?

Nu, nu putem. Progresul nostru este obligat Fişier. Înțelege numai fișierele, deși ar putea fi aplicată și conținutului muzical. Dar pentru a face acest lucru trebuie să îl modificăm, trebuie să facem progres Știi despre Muzică și Fişier. Dacă designul nostru ar respecta OCP, nu ar trebui să atingem Fişier sau progres. Am putea refolosi pur și simplu pe cele existente progres și aplicați-o Muzică.

Soluția 1: Profitați de natura dinamică a PHP

Limbile dinamice tipărite au avantajul de a ghici tipurile de obiecte în timpul rulării. Acest lucru ne permite să eliminăm tipul de pisică progresconstructor și codul va funcționa în continuare.

Progress class fișier privat $; funcția __construct ($ file) $ this-> file = $ file;  funcția getAsPercent () return $ this-> file-> sent * 100 / $ this-> file-> length; 

Acum putem arunca ceva progres. Și prin orice, vreau să spun ceva:

Muzică de clasă public $ length; public $ trimis; public artist $; album public $; public $ releaseDate; funcția getAlbumCoverFile () returnează "Imagini / Coperți /". $ this-> artist. '/'. $ this-> album. '.Png'; 

Și a Muzică clasa ca cea de mai sus va funcționa foarte bine. Putem testa ușor cu un test foarte asemănător Fişier.

funcția testItCanGetTheProgressOfAMusicStreamAsAPercent () $ music = new Music (); $ music-> length = 200; $ music-> sent = 100; $ progress = new Progress ($ muzică); $ this-> assertEquals (50, $ progres-> getAsPercent ()); 

Deci, practic, orice conținut măsurabil poate fi folosit cu progres clasă. Poate ar trebui să exprimăm acest lucru în cod prin schimbarea numelui variabilei:

Progress de clasă private $ measurableContent; funcția __construct ($ measurableContent) $ this-> measurableContent = $ measurableContent;  funcția getAsPercent () return $ this-> measurableContent-> trimis * 100 / $ this-> measurableContent-> length; 

Bine, dar avem o mare problemă cu această abordare. Când am avut Fişier specificat ca un tip de tip, am fost pozitivi cu privire la ceea ce clasa noastră se poate ocupa. A fost explicită și dacă altceva a venit, o eroare frumoasă ne-a spus așa.

Argumentul 1 a trecut la Progress :: __ construct () trebuie să fie o instanță a fișierului, instanță a Muzicii date.

Dar fără tipul de tip, trebuie să ne bazăm pe faptul că orice va veni va avea două variabile publice ale unor nume exacte precum "lungime" și "trimis"În caz contrar, vom avea o moștenire refuzată.

Refuzat cu moștenire: o clasă care înlocuiește o metodă de clasă de bază astfel încât contractul clasei de bază să nu fie onorat de clasa derivată. ~ Sursa Wikipedia.

Acesta este unul dintre cod mirosuri prezentat cu mult mai multe detalii în Codul de detecție Smells curs de primă. Pe scurt, nu vrem să ajungem să încercăm să apelam metode sau să accesăm câmpuri pe obiecte care nu se conformează contractului nostru. Când am avut un tip de semn, contractul a fost specificat de acesta. Câmpurile și metodele Fişier clasă. Acum că nu avem nimic, putem trimite ceva, chiar și un șir și ar duce la o eroare urâtă.

funcția testItFailsWithAParameterThatDoesNotRespectTheImplicitContract () $ progress = new Progress ("un șir"); $ this-> assertEquals (50, $ progres-> getAsPercent ()); 

Un test ca acesta, în cazul în care trimitem într-un șir simplu, va produce o moștenire refuzată:

Încercarea de a obține proprietatea non-obiectului.

În timp ce rezultatul final este același în ambele cazuri, adică codul se rupe, primul a produs un mesaj frumos. Aceasta, însă, este foarte obscură. Nu există nici o modalitate de a ști care este variabila - un șir în cazul nostru - și ce proprietăți au fost căutate și nu au fost găsite. Este dificil să se depaneze și să se rezolve problema. Un programator trebuie să deschidă progres clasa și citiți-o și înțelegeți-o. Contractul, în acest caz, când nu specificăm în mod explicit tipul de tip, este definit de comportamentul lui progres. Este un contract implicit, cunoscut numai de către acesta progres. În exemplul nostru, este definit prin accesul la cele două câmpuri, trimis și lungime, în getAsPercent () metodă. În viața reală contractul implicit poate fi foarte complex și greu de descoperit doar căutând câteva secunde la clasă.

Această soluție este recomandată numai dacă niciuna dintre celelalte sugestii de mai jos nu poate fi implementată cu ușurință sau dacă acestea ar provoca schimbări arhitecturale grave care nu justifică efortul.

Soluția 2: Utilizați modelul de design al strategiei

Aceasta este soluția cea mai comună și probabil cea mai potrivită pentru a respecta OCP. Este simplu și eficient.


Modelul de strategie introduce pur și simplu utilizarea unei interfețe. O interfață este un tip special de entitate în programarea orientată pe obiecte (OOP) care definește un contract între un client și o clasă de servere. Ambele clase vor adera la contract pentru a asigura comportamentul așteptat. Pot exista mai multe clase de servere independente care respectă același contract, fiind astfel capabile să deservească aceeași clasă de clienți.

interfață măsurabilă function getLength (); funcția getSent (); 

Într-o interfață putem defini doar comportamentul. De aceea, în loc să folosim direct variabilele publice, va trebui să ne gândim la folosirea getters și setters. Adaptarea celorlalte clase nu va fi dificilă în acest moment. IDE-ul nostru poate face cea mai mare parte a locului de muncă.

funcția testItCanGetTheProgressOfAFileAsAPercent () $ file = fișier nou (); $ File-> setlength (200); $ File-> setSent (100); $ progress = Progress nou (fișier $); $ this-> assertEquals (50, $ progres-> getAsPercent ()); 

Ca de obicei, începem cu testele noastre. Va trebui să folosim setteri pentru a seta valorile. Dacă sunt considerate obligatorii, acești setteri pot fi de asemenea definiți în măsurabil interfață. Cu toate acestea, aveți grijă ce ați pus acolo. Interfața este definirea contractului dintre clasa client progres și diferitele clase de servere cum ar fi Fişier și Muzică. Face progres trebuie să setați valorile? Probabil ca nu. Deci este foarte puțin probabil ca setterii să fie necesari pentru a fi definiți în interfață. De asemenea, dacă ați defini setatorii acolo, ați forța toate clasele de servere să implementeze setteri. Pentru unii dintre ei, ar putea fi logic să avem setteri, dar alții se pot comporta complet diferit. Și dacă vrem să ne folosim progres clasa pentru a arata temperatura cuptorului nostru? OvenTemperature clasa poate fi inițializată cu valorile din constructor sau să obțină informațiile dintr-o clasă a treia. Cine știe? A avea setteri în acea clasă ar fi ciudat.

fișierul implementează fișierul Masurabil privat $ length; $ private trimise; numele fișierului public $; public $ owner; set set Lungime ($ lungime) $ this-> length = $ length;  functie getLength () return $ this-> length;  funcția setSent ($ trimis) $ this-> sent = $ sent;  funcția getSent () return $ this-> sent;  funcția getRelativePath () retur dirname ($ this-> filename);  funcția getFullPath () returnează calea reală ($ this-> getRelativePath ()); 

Fişier clasa este modificată ușor pentru a se conforma cerințelor de mai sus. Ea implementează acum măsurabil interfață și are setters și getters pentru domeniile care ne interesează. Muzică este foarte asemănător, puteți verifica conținutul acestuia în codul sursă atașat. Suntem aproape gata.

Progress de clasă private $ measurableContent; funcția __construct (măsurabilă $ measurableContent) $ this-> measurableContent = $ measurableContent;  funcția getAsPercent () retur $ this-> measurableContent-> getSent () * 100 / $ this-> măsurabilContent-> getLength (); 

progres au nevoie, de asemenea, de o mică actualizare. Acum putem specifica un tip, folosind tipar, în constructor. Tipul așteptat este măsurabil. Acum avem un contract explicit. progres poate fi sigur că metodele accesate vor fi întotdeauna prezente deoarece sunt definite în măsurabil interfață. Fişier și Muzică pot fi, de asemenea, sigur că pot oferi tot ceea ce este necesar progres prin simpla implementare a tuturor metodelor de pe interfață, o cerință atunci când o clasă implementează o interfață.

Acest model de proiectare este explicat în detaliu în cursul Agile Design Patterns.

O notă privind denumirea interfeței

Oamenii tind să numească interfețe cu o capitală eu în fața lor sau cu cuvântul "Interfață"atasat la sfarsit, cum ar fi iFile sau FileInterface. Aceasta este o notație veche, impusă de unele standarde depășite. Suntem atât de mult trecut notațiile ungare sau necesitatea de a specifica tipul de o variabilă sau un obiect în numele său, pentru a identifica mai ușor. IDE-urile identifică ceva într-o secundă secundară pentru noi. Acest lucru ne permite să ne concentrăm asupra a ceea ce de fapt dorim să abrogem.

Interfețele aparțin clienților lor. Da. Când doriți să denumiți o interfață, trebuie să vă gândiți la client și să uitați de implementare. Când ne-am numit interfața măsurabilă am făcut-o gândindu-ne la Progress. Dacă aș fi un progres, de ce ar trebui să fiu în măsură să furnizez procentul? Răspunsul este simplu, ceva ce putem măsura. Astfel, numele este măsurabil.

Un alt motiv este faptul că implementarea poate fi din domenii diferite. În cazul nostru, există fișiere și muzică. Dar ne putem reutiliza foarte bine progres într-un simulator de curse. În acest caz, clasele măsurate ar fi viteza, combustibilul etc. Nisa, nu-i așa??

Soluția 3: Utilizați modelul de proiectare a metodei șablonului

Modelul de proiectare a metodei Template este foarte similar cu strategia, dar în loc de o interfață utilizează o clasă abstractă. Se recomandă utilizarea unui model de șablon de șablon atunci când avem un client foarte specific aplicației noastre, cu reutilizabilitate redusă și atunci când clasele serverului au un comportament comun.


Acest model de proiectare este explicat în detaliu în cursul Agile Design Patterns.

Vizualizare la nivel superior

Deci, cum toate acestea afectează arhitectura noastră la nivel înalt?


Dacă imaginea de mai sus reprezintă arhitectura actuală a aplicației noastre, adăugarea unui nou modul cu cinci clase noi (cele albastre) ar trebui să afecteze designul într-un mod moderat (clasa roșie).


În majoritatea sistemelor nu vă puteți aștepta absolut nici un efect asupra codului existent atunci când sunt introduse noi clase. Cu toate acestea, respectarea principiului Open / Closed va reduce considerabil clasele și modulele care necesită o schimbare constantă.

Ca și în cazul oricărui alt principiu, încercați să nu vă gândiți la totul de atunci. Dacă faceți acest lucru, veți ajunge la o interfață pentru fiecare clasă. Un astfel de design va fi greu de întreținut și de înțeles. De obicei, cel mai sigur mod de a merge este să vă gândiți la posibilități și dacă puteți stabili dacă vor exista alte tipuri de clase de servere. De multe ori vă puteți imagina cu ușurință o caracteristică nouă sau o puteți găsi pe restanțele proiectului, care vor produce o altă clasă de servere. În aceste cazuri, adăugați interfața de la început. Dacă nu puteți determina, sau dacă nu sunteți sigur - de cele mai multe ori - pur și simplu omiteți-o. Lăsați următorul programator, sau chiar dumneavoastră, să adăugați interfața atunci când aveți nevoie de oa doua implementare.

Gândurile finale

Dacă urmați disciplina dvs. și adăugați interfețe de îndată ce este necesar un al doilea server, modificările vor fi puține și ușoare. Amintiți-vă, în cazul în care codul necesită modificări o singură dată, există o posibilitate ridicată că va necesita o schimbare din nou. Atunci când această posibilitate se transformă în realitate, OCP vă va economisi mult timp și efort.

Mulțumesc că ați citit.

Cod