Cum să scrieți un cod care să reflecte schimbarea

Codul de scriere, ușor de schimbat, este Sfântul Graal al programării. Bine ați venit la nirvana de programare! Dar lucrurile sunt mult mai dificile în realitate: codul sursă este greu de înțeles, dependențele pun în nenumărate direcții, cuplarea este enervantă și în curând simți căldura programării iadului. În acest tutorial, vom discuta câteva principii, tehnici și idei care vă vor ajuta să scrieți un cod ușor de schimbat.


Unele concepte orientate pe obiecte

Programarea orientată pe obiecte (OOP) a devenit populară, datorită promisiunii sale de organizare și reutilizare a codurilor; a eșuat complet în această încercare. Am folosit conceptele OOP de mai mulți ani, dar continuăm să implementăm în mod repetat aceeași logică în proiectele noastre. OOP a introdus un set de principii de bază bune care, dacă sunt utilizate în mod corespunzător, pot conduce la un cod mai bun și mai curat.

Coeziune

Lucrurile care aparțin împreună trebuie păstrate împreună; în caz contrar, acestea ar trebui mutate în altă parte. Acesta este termenul de coeziune. Cel mai bun exemplu de coeziune poate fi demonstrat cu o clasă:

clasa ANOTCohesiveClass private $ firstNumber; privat $ secondNumber; lungime privată $; lățime privată $; funcția __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  set setLength ($ lungime) $ this-> length = $ length;  funcția setHeight ($ înălțime) $ this-> width = $ height;  funcția add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  zona funcției () retur $ this-> length * $ this-> width; 

Acest exemplu definește o clasă cu câmpuri care reprezintă numere și dimensiuni. Aceste proprietăți, judecate doar prin numele lor, nu aparțin împreună. Apoi avem două metode, adăuga() și scãdere (), care funcționează numai pe cele două variabile de număr. Mai avem un zonă() , care operează pe lungime și lăţime câmpuri.

Este evident că această clasă este responsabilă pentru grupuri separate de informații. Are coeziune foarte scăzută. Să o refacem.

clasa ACohesiveClass private $ firstNumber; privat $ secondNumber; funcția __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funcția add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber; 

Acesta este un clasa foarte coerentă. De ce? Pentru că fiecare secțiune din această clasă aparține una de cealaltă. Trebuie să vă depuneți eforturi pentru coeziune, dar aveți grijă, poate fi dificil de realizat.

ortogonalitate

În termeni simpli, ortogonalitatea se referă la izolarea sau eliminarea efectelor secundare. O metodă, o clasă sau un modul care modifică starea altor clase sau module care nu au legătură nu este ortogonală. De exemplu, cutia neagră a avionului este ortogonală. Are funcționalitatea interioară, sursa de putere internă, microfoane și senzori. Nu are nici un efect asupra avionului în care se află sau în lumea exterioară. Acesta oferă doar un mecanism de înregistrare și recuperare a datelor de zbor.

Un exemplu al unui astfel de sistem non-ortogonal este electronica mașinii. Creșterea vitezei vehiculului dvs. are mai multe efecte secundare, cum ar fi creșterea volumului radio (printre altele). Viteza nu este ortogonală față de mașină.

Calculator de clasă private $ firstNumber; privat $ secondNumber; funcția __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funcția add () $ sum = $ this-> firstNumber + $ this-> secondNumber; dacă ($ sum> 100) (nou AlertMechanism ()) -> tooBigNumber (suma $);  returnați suma $;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism funcția tooBigNumber (număr $) echo $ number. "este prea mare!"; 

În acest exemplu, Calculator clasa lui adăuga() metoda prezintă un comportament neașteptat: creează un AlertMechanism obiect și solicită una dintre metodele sale. Acesta este un comportament neașteptat și nedorit; consumatorii de biblioteci nu vor aștepta niciodată un mesaj imprimat pe ecran. În schimb, se așteaptă doar suma numerelor furnizate.

Calculator de clasă private $ firstNumber; privat $ secondNumber; funcția __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funcția add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  AlertMechanism de clasă function checkLimits ($ firstNumber, $ secondNumber) $ sum = (noul Calculator ($ firstNumber, $ secondNumber)) -> add (); dacă ($ sum> 100) $ this-> tooBigNumber (suma $);  funcția tooBigNumber ($ number) echo $ number. "este prea mare!"; 

Asa este mai bine. AlertMechanism nu are niciun efect asupra Calculator. In schimb, AlertMechanism folosește tot ce are nevoie pentru a determina dacă ar trebui să fie emisă o alertă.

Dependența și cuplarea

În majoritatea cazurilor, aceste două cuvinte sunt interschimbabile; dar, în unele cazuri, un termen este preferat în raport cu altul.

Deci, ce este a dependenţă? Când obiect A trebuie să utilizeze obiectul B, pentru a-și îndeplini comportamentul prescris, spunem asta A depinde de B. În PLO, dependențele sunt extrem de comune. Obiectele lucrează frecvent și depind unul de celălalt. Deci, eliminând dependența este o urmărire nobilă, este aproape imposibil să o faceți. Controlul dependențelor și reducerea acestora este totuși de preferat.

Termenii, greu de cuplare și cuplaj slab, se referă de obicei la cât de mult depinde un obiect de alte obiecte.

Într-un sistem cuplat slab, schimbările într-un obiect au un efect redus asupra celorlalte obiecte care depind de el. În astfel de sisteme, clasele depind de interfețe în loc de implementări concrete (vom vorbi mai târziu despre asta). Acesta este motivul pentru sistemele cu cuplaj liber sunt mai deschise la modificări.

Cuplarea într-un câmp

Să luăm în considerare un exemplu:

clasa Afișați private $ calculator; funcția __construct () $ this-> calculator = nou Calculator (1,2); 

Este comun pentru a vedea acest tip de cod. O clasa, Afişa în acest caz, depinde de Calculator clasă prin referirea directă la acea clasă. În codul de mai sus, Afişa„s $ Calculator câmpul este de tip Calculator. Obiectul care conține câmpul este rezultatul chemării directe Calculatorconstructorul.

Cuplarea prin accesarea celorlalte metode de clasă

Consultați următorul cod pentru o demonstrație a acestui tip de cuplare:

clasa Afișați private $ calculator; funcția __construct () $ this-> calculator = nou Calculator (1, 2);  funcția printSum () echo $ this-> calculator-> add (); 

Afişa clasa apeluri Calculator obiecte adăuga() metodă. Aceasta este o altă formă de cuplare, deoarece o clasă accesează metoda celeilalte.

Cuplarea prin referință de metodă

Puteți grupa și clase cu referințe de metode. De exemplu:

 clasa Afișați private $ calculator; funcția __construct () $ this-> calculator = $ this-> makeCalculator ();  funcția printSum () echo $ this-> calculator-> add ();  funcția makeCalculator () returnează Calculator nou (1, 2); 

Este important să rețineți că makeCalculator () metoda returnează a Calculator obiect. Aceasta este o dependență.

Cuplajul prin polimorfism

Moștenirea este probabil cea mai puternică formă de dependență:

AdvancedCalculator de clasă extinde Calculator funcția sinus (valoarea $) retur sin (valoarea $); 

Nu numai că poate AdvancedCalculator nu-și face treaba fără Calculator, dar nici nu putea exista fără ea.

Reducerea cuplării prin injecție de dependență

Se poate reduce cuplarea prin injectarea unei dependențe. Iată un astfel de exemplu:

clasa Afișați private $ calculator; funcția __construct (Calculator $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  // ... //

Prin injectarea Calculator obiect prin Afişaconstructorul, am redus Afişadependența de Calculator clasă. Dar aceasta este doar jumătate din soluție.

Reducerea cuplării cu interfețe

Putem reduce în continuare cuplarea prin utilizarea interfețelor. De exemplu:

interfața CanCompute function add (); funcția scade ();  Calculatorul de clasă implementează CanCompute private $ firstNumber; privat $ secondNumber; funcția __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funcția add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  class Afișați private $ calculator; funcția __construct (CanCompute $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  funcția printSum () echo $ this-> calculator-> add ();  funcția makeCalculator () returnează Calculator nou (1, 2); 

Vă puteți gândi la ISP ca la un principiu de coeziune la nivel superior.

Acest cod introduce CanCompute interfață. O interfață este la fel de abstractă pe care o puteți obține în OOP; definește membrii pe care o clasă trebuie să le implementeze. În cazul exemplului de mai sus, Calculator implementează CanCompute interfață.

Afişaconstructorul așteaptă un obiect care se implementează CanCompute. In acest punct, Afişadependența de Calculator este rupt efectiv. În orice moment, putem crea o altă clasă pe care o implementează CanCompute și să treacă un obiect al acelei clase Afişaconstructorul. Afişa acum depinde doar de CanCompute interfață, dar chiar și această dependență este opțională. Dacă nu vom trece nici un argument Afişaconstructorul, va crea pur și simplu un clasic Calculator obiect prin apel makeCalculator (). Această tehnică este frecvent utilizată și este extrem de utilă pentru dezvoltarea bazată pe teste (TDD).


Principiile SOLID

SOLID este un set de principii pentru scrierea unui cod curat, care apoi face mai ușoară schimbarea, menținerea și extinderea în viitor. Acestea sunt recomandări care, atunci când sunt aplicate codului sursă, au un efect pozitiv asupra mentenabilității.

O mică istorie

Principiile SOLID, cunoscute și ca principii Agile, au fost inițial definite de Robert C. Martin. Chiar dacă nu a inventat toate aceste principii, el a fost cel care le-a pus laolaltă. Puteți citi mai multe despre ele în cartea sa: Agile Software Development, Principii, modele și practici. Principiile SOLID acoperă o gamă largă de subiecte, dar le voi prezenta într-un mod atât de simplu în care sunt capabil. Simțiți-vă liber să cereți detalii suplimentare în comentarii, dacă este necesar.

Principiul unic de responsabilitate (SRP)

O clasă are o singură responsabilitate. Acest lucru poate părea simplu, dar uneori poate fi dificil de înțeles și de pus în practică.

Clasa Reporter function generateIncomeReports (); funcția generatePaymentsReports (); funcția computeBalance (); funcția printReport (); 

Cine credeți că beneficiază de comportamentul acestei clase? Ei bine, un departament contabil este o opțiune (pentru sold), departamentul de finanțe poate fi altul (pentru rapoartele de venituri / plăți), și chiar departamentul de arhivare ar putea tipări și arhiva rapoartele.

Există patru motive pentru care este posibil să trebuiască să schimbați această clasă; fiecare departament poate dori ca metodele lor să fie personalizate pentru nevoile lor.

SRP recomandă ruperea acestor clase în clase mai mici, specifice fiecărui curs, fiecare având doar un singur motiv de schimbare. Astfel de clase tind să fie foarte coezive și libere cuplate. Într-un sens, SRP este coeziunea definită din punctul de vedere al utilizatorilor.

Principiul deschis închis (OCP)

Clasele (și modulele) ar trebui să primească extensia funcționalității lor, precum și să reziste la modificările funcționalității lor actuale. Să jucăm cu exemplul clasic al unui ventilator electric. Aveți un comutator și doriți să controlați ventilatorul. Deci, puteți scrie ceva după cum urmează:

clasa Switch_ privat $ fan; funcția __construct () $ this-> fan = new Fan ();  funcția turnOn () $ this-> fan-> on ();  funcția turnOff () $ this-> fan-> off (); 

Moștenirea este probabil cea mai puternică formă de dependență.

Acest cod definește a Intrerupator_ clasa care creează și controlează a Ventilator obiect. Rețineți sublinierea după "Switch_". PHP nu vă permite să definiți o clasă cu numele "Switch".

Șeful tău decide că dorește să controleze lumina cu același comutator. Aceasta este o problemă, pentru că tu trebuie să se schimbe Intrerupator_.

Orice modificare a codului existent reprezintă un risc; alte părți ale sistemului pot fi afectate și necesită chiar și alte modificări. Este întotdeauna preferabil să lăsați singuri funcționalitatea existentă atunci când adăugați o nouă funcționalitate.

În terminologia OOP, puteți vedea asta Intrerupator_ are o dependență puternică Ventilator. Aici stă problema noastră și unde ar trebui să facem schimbările noastre.

interfață Comutator function on (); funcția oprit ();  clasa Fan implements Comutare funcția publică pe () // cod pentru a porni fan funcția publică off () // cod pentru a opri fan clasa Switch_ private $ comutare; funcția __construct (Switchable $ comutare) $ this-> switchable = $ switchable;  funcția turnOn () $ this-> switchable-> on ();  funcția turnOff () $ this-> switchable-> off (); 

Această soluție introduce comutabil interfață. Definește metodele pe care trebuie să le implementeze toate obiectele activate de comutare. Ventilator ustensile comutabil, și Intrerupator_ acceptă o referire la o comutabil obiect în cadrul constructorului său.

Cum ne ajută acest lucru?

În primul rând, această soluție sparge dependența dintre Intrerupator_ și Ventilator. Intrerupator_ nu are nici o idee că începe un fan, nici nu-i pasă. În al doilea rând, introducerea a Ușoară clasa nu va afecta Intrerupator_ sau comutabil. Doriți să controlați a Ușoară obiect cu dvs. Intrerupator_ clasă? Pur și simplu creați o Ușoară obiect și treceți-l Intrerupator_, asa:

clasa Light implements Comutare funcția publică pe () // cod pentru a întoarce lumina funcția publică off () // cod pentru a opri lumina clasa SomeWhereInYourCode function controlLight () $ light = new Light (); $ switch = comutator nou ($ light); $ Switch-> turnOn (); $ Switch-> Turnoff (); 

Principiul de substituire Liskov (LSP)

LSP afirmă că o clasă de copii nu ar trebui să sporească niciodată funcționalitatea clasei părinte. Acest lucru este extrem de important deoarece consumatorii unei clase părinte așteaptă ca clasa să se comporte într-un anumit mod. Trecerea unei clase de copil la un consumator trebuie doar să funcționeze și să nu afecteze funcționalitatea originală.

Acest lucru este confuz la prima vedere, deci haideți să aruncăm o privire la un alt exemplu clasic:

clasa dreptunghiulară private $ width; suma înălțimii private; funcție setWidth ($ lățime) $ this-> width = $ width;  funcția setHeigth ($ heigth) $ this-> height = $ heigth;  zona funcțiilor () return $ this-> width * $ this-> height; 

Acest exemplu definește o simplă Dreptunghi clasă. Putem seta înălțimea și lățimea lui, și a lui zonă() metoda oferă zona dreptunghiului. Utilizarea Dreptunghi clasa ar putea arata astfel:

clasa Geometrie function rectArea (dreptunghi $ dreptunghi) $ rectangle-> setWidth (10); $ Rectangle-> setHeigth (5); întoarcere $ dreptunghi-> zonă (); 

rectArea () metoda acceptă a Dreptunghi obiect ca argument, își stabilește înălțimea și lățimea și returnează suprafața formei.

În școală, suntem învățați că pătratele sunt dreptunghiuri. Acest lucru indică faptul că dacă modelăm programul nostru în obiectul nostru geometric, a Pătrat clasa ar trebui să se extindă a Dreptunghi clasă. Cum ar arăta o astfel de clasă?

clasa Square extinde Rectangle // Ce cod scrie aici? 

Îmi pare greu să găsesc ce să scriu în Pătrat clasă. Avem mai multe opțiuni. Am putea suprascrie zonă() și returnați pătratul $ lățime:

clasă dreptunghiulară lățime protejată $; protejată înălțime $; // ... // clasa Square extinde Rectangle area function () return $ this-> width ^ 2; 

Rețineți că m-am schimbat Dreptunghia câmpurilor protejat, oferindu- Pătrat accesul la aceste domenii. Acest lucru pare rezonabil din punct de vedere geometric. Un pătrat are laturi egale; întoarcerea pătratului de lățime este rezonabilă.

Cu toate acestea, avem o problemă dintr-un punct de vedere al programării. Dacă Pătrat este A Dreptunghi, nu ar trebui să avem nici o problemă de a ne alimenta în Geometrie clasă. Dar, făcând asta, puteți vedea asta Geometriecodul nu are prea mult sens; stabilește două valori diferite pentru înălțime și lățime. Acesta este motivul pentru care un pătrat nu este un dreptunghi în programare. LSP încălcat.

Principiul de segregare a interfeței (ISP)

Testarea unităților ar trebui să se desfășoare rapid - foarte rapid.

Acest principiu se concentrează pe ruperea interfețelor mari în interfețe mici, specializate. Ideea de bază este că diverși consumatori din aceeași clasă nu ar trebui să știe despre diferite interfețe - doar interfețele pe care consumatorul trebuie să le utilizeze. Chiar dacă un consumator nu utilizează direct toate metodele publice pe un obiect, acesta depinde încă de toate metodele. Deci, de ce să nu furnizați interfețe cu care să declare doar metodele pe care fiecare utilizator are nevoie?

Acesta este în strânsă concordanță cu faptul că interfețele ar trebui să aparțină clienților și nu implementării. Dacă vă adaptați interfețele la clasele consumatoare, ele vor respecta ISP. Implementarea însăși poate fi unică, deoarece o clasă poate implementa mai multe interfețe.

Să ne imaginăm că implementăm o aplicație pentru piața de capital. Avem un broker care cumpără și vinde acțiuni și își poate raporta câștigurile și pierderile zilnice. O implementare foarte simplă ar include ceva asemănător Agent interfață, a NYSEBroker clasa care implementează Agent și câteva clase de interfață cu utilizatorul: una pentru crearea de tranzacții (TransactionsUI) și unul pentru raportare (DailyReporter). Codul pentru un astfel de sistem ar putea fi similar cu următorul text:

interfața Broker buy function (simbol $, volum $); funcția vinde (simbol $, volum $); funcția dailyLoss ($ date); Funcție zilnică de funcționare ($ date);  clasa NYSEBroker implementează Broker public function buy ($ simbol, $ volume) // implementsation goes here funcția publică currentBalance () // implementsation goes here funcția publică zilnicEarnings ($ date) // implementsation goes here public funcția dailyLoss ($ date) // implementsation goes here funcția publică sell ($ symbol, volume $) // implementsation goes here class TransactionsUI privat $ broker; funcția __construct (Broker $ broker) $ this-> broker = $ broker;  funcția buyStocks () // UI logică aici pentru a obține informații dintr-un formular în $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']);  funcția sellStocks () // UI logică aici pentru a obține informații dintr-un formular în $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  clasa DailyReporter broker privat $; funcția __construct (Broker $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Baliza curenta pentru ziua de azi'. data (ora ()). "\ N"; ecou "Câștiguri:". $ this-> broker-> Earnings zilnic (timp ()). "\ N"; ecou "Pierderi:". $ this-> broker-> dailyLoss (timp ()). "\ N"; 

Deși acest cod poate funcționa, acesta încalcă ISP-ul. Ambii DailyReporter și TransactionUI depinde de Agent interfață. Cu toate acestea, fiecare utilizează doar o fracțiune din interfață. TransactionUI utilizează a cumpara() și vinde() metode, în timp ce DailyReporter utilizează dailyEarnings () și dailyLoss () metode.

Ați putea argumenta asta Agent nu este coerentă deoarece are metode care nu au legătură și astfel nu aparțin împreună.

Acest lucru poate fi adevărat, dar răspunsul depinde de implementările lui Agent; vânzarea și cumpărarea pot fi strâns legate de pierderile și câștigurile curente. De exemplu, este posibil să nu aveți permisiunea de a cumpăra acțiuni dacă pierdeți bani.

Puteți susține și asta Agent de asemenea, încalcă SRP. Pentru că avem două clase care o folosesc în moduri diferite, pot exista doi utilizatori diferiți. Ei bine, eu spun nu. Singurul utilizator este probabil brokerul real. El / ea vrea să cumpere, să vândă și să vadă fondurile curente. Dar din nou, răspunsul real depinde de întregul sistem și de afaceri.

ISP este cu siguranță încălcat. Ambele clase de UI depind de ansamblu Agent. Aceasta este o problemă obișnuită, dacă credeți că interfețele aparțin implementărilor lor. Cu toate acestea, schimbarea punctului de vedere poate sugera următorul model:

interfața BrokerTransactions buy function (simbol $, volum $); funcția vinde (simbol $, volum $);  interfață BrokerStatistics function dailyLoss ($ date); Funcție zilnică de funcționare ($ date);  clasa NYSEBroker implementează BrokerTransactions, BrokerStatistics public function buy ($ simbol, $ volume) // implementsation goes here funcția publică currentBalance () // implementsation goes here funcția publică zilnicEarnings ($ date) // implementsation goes here  funcția publică dailyLoss ($ date) // implementsation goes here funcția publică sell ($ symbol, $ volume) // implementsation goes here clasa TransactionsUI privat $ broker; funcția __construct (BrokerTransactions $ broker) $ this-> broker = $ broker;  funcția buyStocks () // UI logică aici pentru a obține informații dintr-un formular în $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']);  funcția sellStocks () // UI logică aici pentru a obține informații dintr-un formular în $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  clasa DailyReporter broker privat $; funcția __construct (BrokerStatistics $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Baliza curenta pentru ziua de azi'. data (ora ()). "\ N"; ecou "Câștiguri:". $ this-> broker-> Earnings zilnic (timp ()). "\ N"; ecou "Pierderi:". $ this-> broker-> dailyLoss (timp ()). "\ N"; 

Acest lucru are sens și respectă ISP-ul. DailyReporter depinde numai de BrokerStatistics; nu-i pasă și nu trebuie să știe despre orice operațiuni de vânzare și cumpărare. TransactionsUI, pe de altă parte, știe doar despre cumpărare și vânzare. NYSEBroker este identic cu clasa noastră anterioară, cu excepția faptului că implementează acum BrokerTransactions și BrokerStatistics interfețe.

Vă puteți gândi la ISP ca la un principiu de coeziune la nivel superior.

Când ambele clase de UI depind de Agent au fost similare cu două clase, fiecare având patru câmpuri, dintre care două au fost utilizate într-o metodă, iar celelalte două într-o altă metodă. Clasa nu ar fi fost foarte coerentă.

Un exemplu mai complex al acestui principiu poate fi găsit într-unul din primele lucrări ale lui Robert C. Martin cu privire la subiectul: Principiul de separare a interfeței.

Principiul inversării dependenței (DIP)

Acest principiu prevede că modulele la nivel înalt nu ar trebui să depindă de modulele de nivel scăzut; ambele ar trebui să depindă de abstracții. Abstracțiile nu ar trebui să depindă de detalii; detaliile ar trebui să depindă de abstractizări. Puneți pur și simplu, ar trebui să depindeți de abstracții cât mai mult posibil și niciodată de implementări concrete.

Trucul cu DIP este că doriți să inversați dependența, dar întotdeauna doriți să păstrați fluxul de control. Să examinăm exemplul nostru din OCP ( Intrerupator și Ușoară clase). În implementarea inițială, am avut un comutator care controlează direct o lumină.

După cum puteți vedea, atât dependența, cât și fluxul de control Intrerupator spre Ușoară. În timp ce asta vrem, nu vrem să ne bazăm în mod direct Ușoară. Așa că am introdus o interfață.

Este uimitor modul în care pur și simplu introducerea unei interfețe face ca codul nostru să respecte atât DIP, cât și OCP. După cum puteți vedea nu, clasa depinde de implementarea concretă a Ușoară, și ambele Ușoară și Intrerupator depinde de comutabil interfață. Am inversat dependența, iar fluxul de control a rămas neschimbat.


Proiectare la nivel înalt

Un alt aspect important al codului dvs. este designul la nivel înalt și arhitectura generală. O arhitectură încurcată produce un cod greu de modificat. Păstrarea unei arhitecturi curate este esențială, iar primul pas este înțelegerea modului de separare a diferitelor preocupări ale codului.

În această imagine, am încercat să rezumă principalele preocupări. În centrul schemei este logica noastră de afaceri. Ar trebui să fie bine izolată de restul lumii și să poată funcționa și se comporta cum era de așteptat fără existența vreuneia dintre celelalte părți. Vedeți-o drept ortogonalitate la un nivel superior.

Începând din partea dreaptă, aveți "principalul" punct de intrare în aplicație și fabricile care creează obiecte. O soluție ideală ar obține obiectele sale de la fabrici specializate, dar acest lucru este aproape imposibil sau impracticabil. Cu toate acestea, ar trebui să utilizați fabrici atunci când aveți posibilitatea să faceți acest lucru și să le păstrați în afara logicii dvs. de afaceri.

Apoi, în partea de jos (în portocaliu), avem persistență (baze de date, accese de fișiere, comunicații în rețea) în scopul informațiilor persistente. Nici un obiect în logica noastră de afaceri nu ar trebui să știe cum persistă munca.

În stânga este mecanismul de livrare.

Un MVC, ca Laravel sau CakePHP, ar trebui să fie doar mecanismul de livrare, nimic mai mult.

Acest lucru vă permite să schimbați un mecanism cu altul fără a vă atinge logica de afaceri. Acest lucru poate suna scandalos pentru unii dintre voi. Ni sa spus că logica noastră de afaceri ar trebui să fie plasată în modelele noastre. Ei bine, nu sunt de acord. Modelele noastre ar trebui să fie "modele de solicitare", adică obiecte de date prost folosite pentru a transmite informații de la MVC la logica de afaceri. Opțional, nu văd nici o problemă, inclusiv validarea intrărilor în modele, dar nimic mai mult. Logica de afaceri nu ar trebui să fie în modele.

Când vă uitați la arhitectura sau structura directorului aplicației, ar trebui să vedeți o structură care sugerează ceea ce face programul, spre deosebire de ce cadru sau baza de date ați folosit.

În cele din urmă, asigurați-vă că toate dependențele indică logica noastră de afaceri. Interfețele utilizatorilor, fabricile, bazele de date sunt implementări foarte concrete și nu trebuie niciodată să le depindeți de ele. Inversarea dependențelor pentru a ne îndrepta către logica noastră de afaceri modulează sistemul nostru, permițându-ne să schimbăm dependențele fără a modifica logica de afaceri.


Unele gânduri despre modelele de design

Modelele de design joacă un rol important în a face codul mai ușor de modificat, oferind o soluție comună de proiectare pe care fiecare programator o poate înțelege. Din punct de vedere structural, modelele de design sunt evident avantajoase. Acestea sunt soluții bine testate și gândite.

Dacă doriți să aflați mai multe despre modelele de design, am creat un curs Tuts + Premium pe ele!


Forța de testare

Test-Driven Development încurajează scrierea unui cod ușor de testat. TDD vă obligă să respectați majoritatea principiilor de mai sus pentru a face codul ușor de testat. Includerea dependențelor și scrierea claselor ortogonale sunt esențiale; în caz contrar, veți termina cu metode uriașe de testare. Testarea unităților ar trebui să se desfășoare rapid - foarte rapid, de fapt, și tot ceea ce nu este testat ar trebui să fie batjocorit. Mocking multe clase complexe pentru un test simplu poate fi copleșitoare. Deci, atunci când te simți batjocorind zece obiecte pentru a testa o singură metodă pe o clasă, poate ai o problemă cu codul tău ... nu testul tău.


Gândurile finale

La sfârșitul zilei, totul se reduce la cât de mult îți pasă de codul sursă. Cunoștințele tehnice nu sunt suficiente; trebuie să aplicați aceste cunoștințe din nou și din nou, fără a fi 100% mulțumiți de codul dvs. Trebuie să dorești să îți faci codul ușor de întreținut, curat și deschis să se schimbe.

Vă mulțumim pentru lectură și nu ezitați să contribuiți la tehnicile dvs. în comentariile de mai jos.

Cod