Refactoring Cod Legacy Partea 8 - Inverting Dependencies pentru o arhitectura curata

Cod vechi. Cod urât. Cod complicat. Codul spaghetti. Gândurile proaste. Cu două cuvinte, Codul vechi. Aceasta este o serie care vă va ajuta să lucrați și să vă ocupați de aceasta.

Acum este momentul să vorbim despre arhitectură și despre modul în care organizăm noi straturi de cod. Este momentul să luați cererea noastră și să încercați să o cartografiați la designul arhitectural teoretic.

Clean Architecture

Acesta este un lucru pe care l-am văzut în toate articolele și tutorialele noastre. Arhitectură curată.

La un nivel înalt, se pare că schema de mai sus și sunt sigur că deja sunteți familiarizat cu aceasta. Este o soluție arhitecturală propusă de Robert C. Martin.

În centrul arhitecturii noastre este logica noastră de afaceri. Acestea sunt clasele care reprezintă procesele de afaceri pe care aplicația noastră încearcă să le soluționeze. Acestea sunt entitățile și interacțiunile care reprezintă domeniul problemei noastre.

Apoi, există mai multe alte tipuri de module sau clase în jurul logicii noastre de afaceri. Acestea pot fi văzute ca simple module auxiliare. Ele au scopuri diferite și majoritatea sunt indispensabile. Acestea asigură legătura dintre utilizator și aplicația noastră printr-un mecanism de livrare. În cazul nostru, aceasta este o interfață de linie de comandă. Există un alt set de clase auxiliare care leagă logica noastră de afaceri cu stratul nostru de persistență și cu toate datele din acest strat, dar nu avem un astfel de nivel în aplicația noastră. Apoi, există cursuri de ajutor cum ar fi fabrici și constructori care construiesc și furnizează obiecte noi logicii noastre de afaceri. În sfârșit, există clasele care reprezintă punctul de intrare în sistemul nostru. În cazul nostru, GameRunner poate fi considerată o astfel de clasă, sau toate testele noastre sunt, de asemenea, puncte de intrare în felul lor.

Ceea ce este cel mai important pentru a observa pe diagramă este direcția dependenței. Toate clasele auxiliare depind de logica afacerii. Logica de afaceri nu depinde de nimic altceva. Dacă toate obiectele din logica noastră de afaceri ar putea apărea în mod magic, cu toate datele din ele și am putea vedea ce se întâmplă direct în calculatorul nostru, ar trebui să poată funcționa. Logica noastră de afaceri trebuie să poată funcționa fără un UI sau fără un strat de persistență. Logica noastră de afaceri trebuie să existe izolată, într-un balon de un univers logic.

Principiul inversiunii dependenței

Modulele de nivel înalt nu ar trebui să depindă de modulele de nivel inferior. Ambele ar trebui să depindă de abstractizări.
B. Abstracțiile nu trebuie să depindă de detalii. Detaliile ar trebui să depindă de abstracții.

Acesta este ultimul principiu SOLID și, probabil, cel care are cel mai mare efect asupra codului tău. Este atât de simplu de înțeles și de simplu de implementat.

În termeni simpli, se spune că lucrurile concrete trebuie întotdeauna să depindă de lucruri abstracte. Baza dvs. de date este foarte concretă, deci ar trebui să depindă de ceva mai abstract. Interfața dvs. este foarte concretă, așa că ar trebui să depindă de ceva mai abstract. Întreprinderile dvs. sunt din nou foarte concrete. Dar despre logica dvs. de afaceri. În interiorul logicii dvs. de afaceri ar trebui să continuați să aplicați aceste idei, astfel încât clasele care sunt mai aproape de granițe să depindă de clase care sunt mai abstracte, mai mult în centrul logicii dvs. de afaceri.

O logică de afaceri pură reprezintă, într-un mod abstract, procesele și comportamentele unui domeniu definit sau al unui model de afaceri. O astfel de logică de afaceri nu conține specific (lucruri concrete) cum ar fi valori, bani, nume de cont, parole, dimensiunea unui buton sau numărul de câmpuri dintr-o formă. Logica de afaceri nu ar trebui să aibă grijă de lucrurile concrete. Ar trebui să aibă grijă numai de procesele dvs. de afaceri.

Trucul tehnic

Prin urmare, Principiul inversiunii dependenței (DIP) spune că ar trebui să inversăm dependențele noastre ori de câte ori există cod care depinde de ceva concret. Acum structura noastră de dependență arată așa.

GameRunner, utilizând funcțiile din RunnerFunctions.php este crearea unui Joc clasă și apoi o folosește. Pe de altă parte, ale noastre Joc clasa, reprezentând logica noastră de afaceri, creează și utilizează o Afişa obiect.

Deci, alergătorul depinde de logica noastră de afaceri. Este corect. Pe de altă parte, ale noastre Joc depinde de Afişa, care nu este bună. Logica noastră de afaceri nu ar trebui să depindă de prezentarea noastră.

Cel mai simplu truc tehnic pe care îl putem face este să folosim construcțiile abstracte din limba noastră de programare. O clasă tradițională este mai concretă decât o clasă abstractă, care este mai concretă decât o interfață.

Un Clasa abstractă este un tip special care nu poate fi inițializat. Acesta conține numai definiții și implementări parțiale. O clasă abstractă de bază are de obicei mai multe clase pentru copii. Aceste clase de copii moștenesc funcționalitatea parțială comună de la părintele abstract, ele adaugă propriul comportament extins și trebuie să implementeze toate metodele definite în parintele abstract, dar nu sunt implementate în acesta.

Un Interfață este un tip special care permite doar definirea metodelor și a variabilelor. Este cea mai abstractă construcție în programarea orientată obiect. Orice implementare trebuie să implementeze întotdeauna toate metodele interfeței părinte. O clasă de beton poate implementa mai multe interfețe.

Cu excepția limbilor orientate către obiecte din familia C, altele precum Java sau PHP nu permit moștenire multiplă. Astfel, o clasă concretă poate extinde o singură clasă abstractă, dar poate implementa mai multe interfețe, chiar dacă este necesar. Sau dintr-o altă perspectivă, o singură clasă abstractă poate avea multe implementări, în timp ce multe interfețe pot avea multe implementări.

Pentru o explicație mai detaliată a DIP, citiți tutorialul dedicat acestui principiu SOLID.

Inversarea dependenței folosind o interfață

PHP suportă pe deplin interfețele. Pornind de la Afişa clasa ca modelul nostru, am putea defini o interfață cu metodele publice pe care toate clasele responsabile cu afișarea datelor vor trebui să le pună în aplicare.

Uitandu-ma la Afişalista de metode, există 12 metode publice, inclusiv constructorul. Aceasta este o interfață destul de mare, trebuie să păstrați acest număr cât mai scăzut, expunând interfețele, pe măsură ce clienții au nevoie de ele. Principiul de separare a interfeței are câteva idei bune despre acest lucru. Poate vom încerca să rezolvăm această problemă într-un tutorial viitor.

Ceea ce vrem să realizăm acum este o arhitectură ca cea de mai jos.

În acest fel, în loc de Joc în funcție de mai concret Afişa, ambele depind de interfața foarte abstractă. Joc utilizează interfața, în timp ce Afişa o implementează.

Naming Interfaces

Phil Karlton a spus: "Există doar două lucruri grele în Informatică: invalidarea cache-ului și numirea lucrurilor".

Deși nu ne pasă de cache-uri, trebuie să numim clasele, variabilele și metodele noastre. Naming interfețele poate fi o provocare.

În vremurile vechi ale notării maghiare, am fi făcut-o în acest fel.

Pentru această diagramă, am folosit numele real al clasei / fișierelor și capitalizarea reală. Interfața este denumită "IDisplay" cu un "I" de capital în fața "Display". Există, de fapt, limbi de programare care necesită o astfel de denumire pentru interfețe. Sunt sigur că există câțiva cititori care încă le folosesc și zâmbesc chiar acum.

Problema cu această schemă de denumire este preocuparea greșită. Interfețele aparțin clienților lor. Interfața noastră aparține Joc. Prin urmare Joc nu trebuie să știe că folosește o interfață sau un obiect real. Joc nu trebuie să fie preocupat de implementarea pe care o primește. Din Jocpentru punctul de vedere, foloseste doar un "Display", asta-i tot.

Aceasta rezolvă problema Joc la Afişa problema numelui. Folosirea sufixului "Impl" pentru implementare este oarecum mai bună. Ajută la eliminarea preocupării Joc.

Este, de asemenea, mult mai eficient pentru noi. A se gandi la Joc așa cum arată acum. Folosește a Afişa obiect și știe cum să o folosească. Dacă numim interfața noastră "Afișaj", vom reduce numărul de modificări necesare Joc.

Totuși, această denumire este doar puțin mai bună decât cea anterioară. Permite o singură implementare pentru Afişa iar numele implementării nu ne va spune ce fel de afișare vorbim.

Acum este mult mai bine. Implementarea noastră a fost denumită "CLIDisplay", pe măsură ce se transmite către CLI. Dacă dorim o ieșire HTML sau un interfață UI desktop Windows, putem adăuga cu ușurință toate acestea la arhitectura noastră.

Arată-mi codul

Deoarece avem două tipuri de teste, testarea lentă a aurului și testele unității rapide, vrem să ne bazăm pe testele unitare cât mai mult posibil și pe maestrul de aur cât de puțin putem. Deci, haideți să ne marchem testele de master de aur ca fiind omis și să încercăm să ne bazăm pe testele unității noastre. Ei trec acum și vrem să facem o schimbare care să le împiedice să treacă. Dar cum putem face acest lucru, fără a face toate schimbările propuse mai sus?

Există o modalitate de testare care ne-ar permite să facem un pas mai mic?

Mocking salvează Ziua

Există un astfel de mod. În teste, există un concept numit "Mocking".

Wikipedia definește Mocking ca atare: "În programarea orientată pe obiecte, obiectele mocke sunt obiecte simulate care imită comportamentul obiectelor reale în moduri controlate".

Un astfel de obiect ar fi de mare ajutor pentru noi. De fapt, nici nu avem nevoie de ceva atât de complex ca simularea întregului comportament. Tot ce avem nevoie este un obiect fals și stupid pe care îl putem trimite Joc în loc de logica reală a afișării.

Crearea interfeței

Să creați o interfață numită Afişa cu toate metodele publice ale clasei actuale de beton.

După cum puteți observa, vechiul Display.php a fost redenumit DisplayOld.php. Acesta este doar un pas temporar, care ne permite să ne scoatem din drum și să ne concentrăm asupra interfeței.

interfață Afișaj  

Asta e tot ce trebuie să faceți pentru a crea o interfață. Puteți vedea că este definită ca "interfață" și nu ca o "clasă". Să adăugăm metodele.

interfața Afișaj function statusAfterRoll ($ rolledNumber, $ currentPlayer); funcția playerSentToPenaltyBox ($ currentPlayer); funcția playerStaysInPenaltyBox ($ currentPlayer); starea funcțieiAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); starea funcțieiAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); funcția playerAdded ($ playerName, $ numberOfPlayers); funcția askQuestion ($ currentCategory); funcția correctAnswer (); funcția corectAnswerWithTypo (); funcția incorectAnswer (); player de joc ($ currentPlayer, $ playerCoins);  

Da. O interfață este doar o grămadă de declarații de funcții. Imaginați-vă ca un fișier de antet C. Nu există implementări, doar declarații. Nu poate face o implementare deloc. Dacă încercați să implementați oricare dintre metode, va duce la o eroare.

Dar aceste definiții foarte abstracte ne permit ceva minunat. Al nostru Joc clasa depinde acum de ele, în loc de o punere în aplicare concretă. Cu toate acestea, dacă încercăm să ne executăm testele, acestea vor eșua.

Eroare fatală: nu poate fi instanțiată afișarea interfeței

Asta pentru că Joc încearcă să creeze un nou ecran pe cont propriu la linia 25, în constructor.

Știm că nu putem face asta. O interfață sau o clasă abstractă nu poate fi instanțiată. Avem nevoie de un obiect real.

Dependența de injecție

Avem nevoie de un obiect dummy pentru a fi folosit în testele noastre. O clasă simplă, implementând toate metodele Afişa interfață, dar nu face nimic. Să o scriem direct în testul unității. Dacă limba dvs. de programare nu permite mai multe clase în același fișier, nu ezitați să creați un fișier nou pentru clasa dummy.

clasa DummyDisplay implementează Afișare function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implementați statusAfterRoll ().  function playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementarea metodei playerSentToPenaltyBox ().  functie playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementarea metodei playerStaysInPenaltyBox ().  funcția statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementați statusAfterNonPenalizedPlayerMove ().  funcția statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementați statusAfterPlayerGettingOutOfPenaltyBox () metoda.  funcția playerAdded ($ playerName, $ numberOfPlayers) // TODO: Implementați metoda playerAdded ().  funcția askQuestion ($ currentCategory) // TODO: Implementarea metodei askQuestion ().  function correctAnswer () // TODO: Implementarea metodei correctAnswer ().  function correctAnswerWithTypo () // TODO: Implementarea metodei correctAnswerWithTypo ().  function incorrectAnswer () // TODO: Implementarea metodei incorecteAnswer ().  funcția playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implementarea metodei playerCoins (). 

De îndată ce spui că clasa implementează o interfață, IDE vă va permite să completați automat metodele lipsă. Acest lucru face ca aceste obiecte să fie create foarte rapid, în doar câteva secunde.

Acum să o folosim Joc prin inițializarea acestuia în constructorul său.

funcția __construct () $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> înPenaltyBox = array (0); $ this-> display = nou DummyDisplay (); 

Aceasta face testul, dar introduce o mare problemă. Joc trebuie să știe despre testul său. Chiar nu vrem asta. Un test este doar un alt punct de intrare. DummyDisplay este doar o altă interfață de utilizator. Logica noastră de afaceri, Joc clasa, nu ar trebui să depindă de interfața utilizator. Deci, să o facem să depindă numai de interfață.

funcția __construct (Display $ display) $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> înPenaltyBox = array (0); $ this-> display = $ display; 

Dar pentru a testa Joc, trebuie să trimitem din afișajul dummy din testele noastre.

funcția setUp () $ this-> game = joc nou (nou DummyDisplay ()); 

Asta e. A trebuit să modificăm o singură linie în testele unității noastre. În configurare, vom trimite, ca parametru, o nouă instanță de DummyDisplay. Aceasta este o injecție de dependență. Folosirea interfețelor și injecția de dependență ajută în special dacă lucrați într-o echipă. Noi de la Syneto am observat că specificarea unui tip de interfață pentru o clasă și injectarea acesteia ne vor ajuta să comunicăm mult mai bine intențiile codului clientului. Oricine se uită la client va ști ce tip de obiect este folosit în parametri. Și un bonus interesant este că IDE-ul dvs. va completa automat metodele pentru acești parametri, deoarece poate determina tipurile acestora.

O implementare reală pentru Maestrul de Aur

Testul de masterat de aur, rulează codul nostru ca în lumea reală. Pentru a face acest lucru, trebuie să transformăm vechea clasă de afișare într-o implementare reală a interfeței și să o trimitem în logica noastră de afaceri. Iată o modalitate de ao face.

clasa CLIDisplay implementează Afișaj // ... //

Redenumiți-o CLIDisplay și să o pună în aplicare Afişa.

execuție funcțională () $ display = new CLIDisplay (); $ aGame = Joc nou (afișare $); $ AGame-> adaugă ( "Chet"); $ AGame-> adaugă ( "Pat"); $ AGame-> add ( "Sue"); face $ dice = rand (0, 5) + 1; $ AGame-> rola ($ zaruri);  în timp ce (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ())); 

În RunnerFunctions.php, în alerga() , creați un nou afișaj pentru CLI și treceți-l la Joc când este creat.

Dezactivați-vă și conduceți-vă testele de masterat de aur. Ei vor trece.

Gândurile finale

Această soluție conduce în mod efectiv la o arhitectură ca în diagrama de mai jos.

Deci, acum alergătorul jocului nostru, care este punctul de intrare la cererea noastră, creează o concretă CLIDisplay și, prin urmare, depinde de ea. CLIDisplay depinde doar de interfața care se află la limita dintre prezentare și logica afacerii. Alergătorul nostru, de asemenea, depinde în mod direct de logica de afaceri. Acesta este modul în care arată aplicația noastră atunci când se proiectează pe arhitectura curată la care am pornit acest articol.

Vă mulțumim pentru lectură și nu ratați tutorialul următor când vom vorbi mai multe despre batjocura și interacțiunea de clasă.

Cod