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.
Îmi place să mă gândesc la codul la fel cum mă gândesc la proză. Scrierile lungi, imbricate, compuse cu cuvinte exotice sunt greu de înțeles. Din când în când ai nevoie de unul, dar de cele mai multe ori poți folosi doar simple cuvinte simple în propoziții scurte. Acest lucru este foarte adevărat și pentru codul sursă. Condițiile complexe sunt greu de înțeles. Metodele lungi sunt ca propoziții nesfârșite.
Iată un exemplu "prozaic" pentru a vă înveseli. În primul rând, propoziția all-in-one. Cel urât.
Dacă temperatura în camera serverului este mai mică de cinci grade și umiditatea crește peste cincizeci la sută, dar rămâne sub optzeci și presiunea aerului este constantă, tehnicianul senior John, care are cel puțin trei ani de experiență de lucru în administrarea rețelelor și serverelor fi anunțat și trebuie să se trezească în miez de noapte, să se îmbrace, să iasă, să ia mașina sau să sune o taxă dacă nu are o mașină, conduce la birou, intră în clădire, pornește aerul condiționat și așteptați până când temperatura crește peste zece grade și umiditatea scade sub 20%.
Dacă înțelegeți, înțelegeți și vă amintiți acest paragraf fără să îl recitiți, vă dau o medalie (virtuală, desigur). Paragrafele lungi, încurcate, scrise într-o singură propoziție complicată, sunt greu de înțeles. Din nefericire, nu cunosc destule cuvinte exotice englezești care să facă chiar mai greu de înțeles.
Să găsim o modalitate de ao simplifica puțin. Toată prima sa parte, până la "atunci" este o condiție. Da, este complicat, dar am putea rezuma astfel: Dacă condițiile de mediu reprezintă un risc ... ... atunci ar trebui făcut ceva. Expresia complicată spune că ar trebui să informăm pe cineva care satisface o mulțime de condiții: apoi notificați asistență tehnică de nivel trei. În cele din urmă, un întreg proces este descris de la trezirea tehnicianului până când totul este fix: și așteptăm ca mediul să fie restaurat în parametrii normali. Să punem totul împreună.
Dacă condițiile de mediu reprezintă un risc, notificați asistența tehnică de nivel trei și așteptați revenirea mediului în parametrii normali.
Acum, aceasta este de aproximativ 20% în raport cu textul original. Nu cunoaștem detaliile, iar în majoritatea cazurilor, nu ne pasă. Și acest lucru este foarte adevărat și pentru codul sursă. De câte ori ți-a păsat detaliile de implementare ale unui a logInfo ("Unele mesaje");
metodă? Probabil o dată, dacă și când ai implementat-o. Apoi doar loghează mesajul în categoria "info". Sau când un utilizator achiziționează unul dintre produsele dvs., vă interesează cum să îl facturați? Nu. Tot ce vrei să-ți pese este dacă produsul a fost cumpărat, aruncați-l din inventar și facturați-l cumpărătorului. Exemplele ar putea fi nesfârșite. Ele sunt, în esență, modul în care scriem software-ul corect.
În această secțiune vom încerca să aplicăm filozofia prozei jocului nostru de trivia. Cu pasi marunti. Începând cu condiționalități complexe. Să începem cu un ușor cod. Doar să se încălzească.
Linia a douăzeci de ani GameRunner.php
fișierul se citește astfel:
dacă (rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId)
Cum ar suna asta în proză? Dacă un număr aleatoriu între ID-ul de răspuns minim și ID-ul răspunsului maxim este egal cu ID-ul răspunsului greșit, atunci ...
Acest lucru nu este foarte complicat, dar putem totuși simplifica. Ce zici de asta? Dacă este selectat un răspuns greșit, atunci ... Mai bine, nu-i așa??
Avem nevoie de o cale, o procedură, o tehnică pentru a mișca această afirmație condiționată în altă parte. Destinația poate fi cu ușurință o metodă. Sau în cazul nostru, deoarece nu suntem în interiorul unei clase aici, o funcție. Această mișcare a comportamentului într-o nouă metodă sau funcție se numește refactorizarea "Metoda de extragere". Mai jos sunt pașii, așa cum sunt definiți de Martin Fowler în cartea sa excelentă Refactoring: Îmbunătățirea designului codului existent. Dacă nu ați citit această carte, ar trebui să o puneți pe lista dvs. "Pentru a citi" acum. Este una dintre cele mai esențiale cărți pentru un programator modern.
Pentru tutorialul nostru, am luat pașii inițiați și le-am simplificat puțin pentru a se potrivi mai bine nevoilor noastre și tipului nostru de tutorial.
Acum este destul de complicat. Cu toate acestea, metoda de extragere este, fără îndoială, refactorizarea cea mai frecvent utilizată, cu excepția posibilității redenumizării. Deci trebuie să-i înțelegi mecanica.
Din fericire pentru noi, ideile IDE moderne precum PHPStorm oferă instrumente de refactorizare excelente, așa cum am văzut în tutorialul PHPStorm: Când IDE-ul contează cu adevărat. Așa că vom folosi caracteristicile pe care le avem la îndemână, în loc să le facem cu mâna. Acest lucru este mai puțin predispus la erori și mult, mult mai rapid.
Doar selectați partea dorită a codului și Click dreapta aceasta.
IDE va înțelege automat că avem nevoie de trei parametri pentru a rula codul nostru și va propune următoarea soluție.
// ... // $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; funcția isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) întoarcere rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; a face $ dice = rand (0, 5) + 1; $ AGame-> rola ($ zaruri); dacă (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> greșitAnswer (); altceva $ notAWinner = $ aGame-> a fost corectdescărit (); în timp ce ($ notAWinner);
Deși acest cod este corect din punct de vedere sintactic, acesta ne va rupe testele. Între zgomotul prezentat în culorile roșu, albastru și negru, putem observa motivul pentru care:
Eroare fatală: Nu se poate redeclora isCurrentAnswerWrong () (declarată anterior în / home / csaba / Personal / Programare / NetTuts / Refactoring Legacy Code - Partea 3: Condiții complexe și metode lungi /Source/trivia/php/GameRunner.php:16) home / csaba / Personal / Programare / NetTuts / Refactoring Code Legacy - Partea 3: Condiții complexe și metode lungi /Source/trivia/php/GameRunner.php pe linia 18
În principiu, spune că dorim să declarăm funcția de două ori. Dar cum se poate întâmpla asta? O avem o singură dată în noi GameRunner.php
!
Uitați-vă la teste. Este un generateOutput ()
metoda care face a require ()
pe noi GameRunner.php
. Se numește cel puțin de două ori. Iată sursa erorii.
Acum avem o dilemă. Din cauza însămânțării generatorului de aleatoare, trebuie să apelăm acest cod cu valori controlate.
funcția privată generateOutput ($ seed) ob_start (); srand ($ semințe); solicită __DIR__. '/ ... / trivia/php/GameRunner.php'; $ ieșire = ob_get_contents (); ob_end_clean (); returnați outputul $;
Dar nu există nicio modalitate de a declara o funcție de două ori în PHP, deci avem nevoie de o altă soluție. Începem să simțim povara testării noastre principale de aur. Rularea totul de 20000 de ori, de fiecare dată când schimbăm o bucată de cod, poate să nu fie o soluție pe termen lung. Pe lângă faptul că este nevoie de vârste pentru a alerga, ne obligă să ne schimbăm codul pentru a se potrivi felului în care îl testăm. Acesta este de obicei un semn de teste nepotrivite. Codul trebuie să se schimbe și să efectueze în continuare testul, dar schimbările ar trebui să aibă motive să se schimbe, provenind doar din codul sursă.
Dar vorbesc suficient, avem nevoie de o soluție, chiar și una temporară pentru moment. Migrarea la testele de unitate va începe cu următoarea noastră lecție.
O modalitate de a ne rezolva problema este să luăm tot restul codului GameRunner.php
și a pus-o într-o funcție. Sa spunem alerga()
include_once __DIR__. '/Game.php'; funcția isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) întoarcere rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; funcția execută () $ notAWinner; $ aGame = Joc nou (); $ AGame-> adaugă ( "Chet"); $ AGame-> adaugă ( "Pat"); $ AGame-> add ( "Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; face $ dice = rand (0, 5) + 1; $ AGame-> rola ($ zaruri); dacă (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> greșitAnswer (); altceva $ notAWinner = $ aGame-> a fost corectdescărit (); în timp ce ($ notAWinner);
Acest lucru ne va permite să îl testați, dar trebuie să știți că executarea codului din consola nu va executa jocul. Am făcut o mică schimbare în comportament. Am câștigat testabilitate cu prețul unei schimbări comportamentale, pe care nu am vrut să o facem în primul rând. Dacă doriți să executați codul din consola, acum veți avea nevoie de un alt fișier PHP care include sau necesită alergătorul și apoi va apela în mod explicit metoda de rulare pe acesta. Nu atât de mare de schimbare, ci de necesitate de reținut, mai ales dacă aveți terțe părți folosind codul existent.
Pe de altă parte, acum putem include fișierul în testul nostru.
solicită __DIR__. '/ ... / trivia/php/GameRunner.php';
Și apoi sunați alerga()
în interiorul metodei generateOutput ().
funcția privată generateOutput ($ seed) ob_start (); srand ($ semințe); alerga(); $ ieșire = ob_get_contents (); ob_end_clean (); returnați outputul $;
Poate că aceasta este o bună ocazie să ne gândim la structura directoarelor și dosarelor noastre. Nu există condiționalități mai complexe în nostru GameRunner.php
, dar înainte de a continua Game.php
fișier, nu trebuie să lăsăm o urmă în spatele nostru. Al nostru GameRunner.php
nu mai rulează nimic, iar noi am nevoie să aruncăm metode pentru a face testabil, ceea ce ne-a rupt interfața publică. Motivul pentru aceasta este că poate încercăm ceva greșit.
Testele noastre sună alerga()
în GameRunner.php
fișier, care include Game.php
, joacă jocul și se generează un nou fișier maestru de aur. Dacă introducem un alt dosar? Noi facem GameRunner.php
pentru a rula efectiv jocul prin apelare alerga()
si nimic altceva. Deci, dacă nu există logică acolo care ar putea merge prost și nu sunt necesare teste, și apoi ne mutăm codul nostru curent într-un alt fișier?
Acum este o poveste cu totul diferită. Acum, testele noastre accesează codul chiar sub alergator. Practic, testele noastre sunt doar alergători. Și bineînțeles în noile noastre GameRunner.php
va exista doar un apel pentru a rula jocul. Acesta este un alergător adevărat, nu face altceva decât să sune alerga()
metodă. Nicio logică înseamnă că nu sunt necesare teste.
requ_once __DIR__. '/RunnerFunctions.php'; alerga();
Există și alte întrebări pe care ni le-am putea pune în acest moment. Chiar avem nevoie de a RunnerFunctions.php
? Nu putem să luăm funcțiile de acolo și să le mutăm Game.php
? Probabil am putea, dar cu înțelegerea noastră actuală despre ce funcție aparține unde? Nu este de ajuns. Vom găsi un loc pentru metoda noastră într-o lecție viitoare.
Am încercat, de asemenea, să numim fișierele noastre în funcție de ceea ce face codul din interiorul lor. Una este doar o mulțime de funcții pentru alergător, funcții pe care noi, în acest moment, considerăm că le aparțin împreună, pentru a satisface nevoile alergătorului. Va deveni aceasta o clasă la un moment dat în viitor? Poate. Poate nu. Pentru moment, este destul de bun.
Dacă ne uităm la RunnerFunctions.php
fișier, există un pic de dezordine pe care le-am introdus.
Definim:
$ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7;
… în interiorul alerga()
metodă. Ei au un singur motiv de a exista și un singur loc în care sunt folosiți. De ce să nu le definiți doar în interiorul acelei metode și să eliminați total parametrii?
funcția esteCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;
Ok, testele trec și codul este mult mai plăcut. Dar nu destul de bun.
Este mult mai ușor, pentru mintea umană, să înțelegeți raționamentul pozitiv. Deci, dacă puteți evita condiționările negative, ar trebui să luați întotdeauna acea cale. În exemplul nostru curent, metoda verifică un răspuns greșit. Ar fi mult mai ușor să înțelegem o metodă care să verifice valabilitatea și să negată acest lucru, atunci când este necesar.
este funcțiaCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId)! = $ greșitAnswerId;
Am folosit refactorizarea metodei Rename. Acest lucru este din nou, destul de complicat dacă este folosit manual, dar în orice IDE este la fel de simplu ca lovirea CTRL + r, sau selectând opțiunea corespunzătoare din meniu. Pentru ca încercările noastre să treacă, trebuie să ne actualizăm declarația condiționată cu o negare.
dacă (! isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> greșitAnswer (); altceva $ notAWinner = $ aGame-> a fost corectdescărit ();
Acest lucru ne aduce un pas mai aproape de înțelegerea noastră condiționată. Utilizarea !
într-un dacă()
declarație, de fapt ajută. Se evidențiază și subliniază faptul că ceva este de fapt negat acolo. Dar putem să inversăm acest lucru pentru a evita negarea completă? da putem.
dacă (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> a fost CorrectlyAnswered (); altceva $ notAWinner = $ aGame-> wrongAnswer ();
Acum nu avem nici o negare logică prin folosire !
, nici negarea lexicală prin numirea și returnarea lucrurilor greșite. Toți acești pași au făcut condiția noastră mult mai ușor de înțeles.
Game.php
Am simplificat până la extrem, RunnerFunctions.php
. Să ne atacă Game.php
fișier acum. Există mai multe moduri în care puteți căuta condiționate. Dacă preferați, puteți să scanați codul doar dacă îl priviți. Acest lucru este mai lent, dar are valoarea adăugată de a te forța să încerci să o înțelegi secvențial.
Cea de-a doua modalitate evidentă de căutare a condiționărilor este de a face o căutare pentru "if" sau "if (") Dacă ați formatat codul cu caracteristicile încorporate ale IDE, puteți fi sigur că toate instrucțiunile condiționale au aceeași formă specifică.În cazul meu, există un spațiu între "dacă" și paranteză.De asemenea, dacă utilizați căutarea încorporată, rezultatele găsite vor fi evidențiate într-o culoare strident, în cazul meu galben.
Acum, când toți le aprinde codul ca un pom de Crăciun, le putem lua unul câte unul. Cunosc exercițiul, știm tehnicile pe care le putem folosi, este timpul să le aplicăm.
dacă ($ this-> inPenaltyBox [$ this-> currentPlayer])
Acest lucru pare destul de rezonabil. Am putea extrage-o într-o metodă, dar ar exista un nume pentru această metodă pentru a face starea mai clară?
dacă ($ roll% 2! = 0)
Pun pariu că 90% din toți programatorii pot înțelege problema în cele de mai sus dacă
afirmație. Încercăm să ne concentrăm asupra a ceea ce face metoda noastră actuală. Și creierul nostru este cuplat în domeniul problemei. Nu vrem să "începem un alt fir" pentru a calcula expresia matematică pentru a înțelege că verifică doar dacă un număr este ciudat. Aceasta este una dintre acele mici distrageri care pot ruina o deducție logică dificilă. Așa că eu spun să-l extragem.
dacă ($ this-> isOdd ($ roll))
Asta e mai bine pentru că este vorba despre domeniul problemei și nu necesită nici o putere suplimentară a creierului.
dacă ($ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard)
Acest lucru pare a fi un alt candidat bun. Nu este atât de greu de înțeles ca o expresie matematică, dar din nou, este o expresie care necesită prelucrare laterală. Mă întreb, ce înseamnă dacă poziția actuală a jucătorului a ajuns la sfârșitul consiliului? Nu putem exprima această stare într-un mod mai concis? Probabil putem.
dacă ($ this-> playerReachedEndOfBoard ($ lastPositionOnTheBoard))
Asa este mai bine. Dar ce se întâmplă de fapt în interiorul dacă
? Jucătorul este repoziționat la începutul tabloului. Jucătorul începe un nou turneu în cursă. Dacă, în viitor, vom avea un motiv diferit pentru a începe un tur nou? Ar trebui să fie dacă
declarația se schimbă când schimbăm logica subiacentă în metoda privată? Absolut nu! Deci, să redenumiți această metodă în ceea ce dacă
reprezintă, în ceea ce se întâmplă, nu ceea ce verificăm.
dacă ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))
Când încercați să denumiți metode și variabile, gândiți-vă întotdeauna la ce ar trebui să facă codul și nu la ce stare sau condiție reprezintă. Odată ce obțineți acest drept, redenumirea acțiunilor din codul dvs. va scădea semnificativ. Dar, chiar și un programator cu experiență trebuie să redenumească o metodă de cel puțin trei până la cinci ori înainte de a-și găsi numele corect. Deci, nu vă fie frică să loviți CTRL + r și redenumiți frecvent. Nu vă angajați niciodată modificările la VCS-ul proiectului dacă nu ați scanat numele metodelor recent adăugate, iar codul dvs. nu se citește ca proza scrisă. Redenumirea este atât de ieftină în zilele noastre, încât puteți redenumi lucrurile doar pentru a încerca diverse versiuni și a reveni cu un singur buton de buton.
dacă
declarația de la linia 90 este aceeași cu cea precedentă. Putem doar reutiliza metoda noastră extrasă. Voila, duplicarea eliminată! Și nu uitați să executați testele acum și apoi, chiar și atunci când refacționați folosind magia IDE. Ceea ce ne conduce la următoarea noastră observație. Magicul, uneori, eșuează. Verificați linia 65.
$ lastPositionOnTheBoard = 11;
Vom declara o variabilă și o vom folosi doar într-un singur loc, ca parametru pentru noua noastră metodă extrasă. Acest lucru sugerează că variabila ar trebui să fie în interiorul metodei.
funcția privată playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; returnează $ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard;
Și nu uitați să apelați metoda fără parametri dacă
declaraţii.
dacă ($ this-> playerShouldStartANewLap ())
dacă
declarații în cere întrebare()
metoda pare să fie în regulă, precum și cele din currentCategory ()
.
dacă ($ this-> inPenaltyBox [$ this-> currentPlayer])
Acest lucru este un pic mai complicat, dar în domeniu și destul de expresiv.
dacă ($ this-> currentPlayer == count ($ this-> players))
Putem lucra la asta. Este evident că mijloacele de comparație, în cazul în care jucătorul actual nu este obligat. Dar, așa cum am învățat mai sus, nu vrem să declarăm intenția.
dacă ($ this-> shouldResetCurrentPlayer ())
E mult mai bine și o vom reutiliza la linia 172, 189 și 203. Duplicarea, mă refer la triplicare, mă refer la quadruplicație, eliminată!
Testele trec și toate dacă
declarațiile au fost evaluate pentru complexitate.
Există mai multe lecții care pot fi învățate din condiționările refactorizării. În primul rând, ele contribuie la înțelegerea mai bine a intenției codului. Apoi, dacă numiți metoda extrasă pentru a reprezenta intenția corect, veți evita modificările viitoare ale numelui. Găsirea duplicării în logică este mai dificilă decât găsirea de linii dublate de cod simplu. S-ar putea să fi crezut că ar trebui să facem o dublare conștientă, dar prefer să mă descurc cu dublarea când am teste unitare cu care am încredere în viața mea. Maestrul de Aur este bun, dar este cel mult o plasă de siguranță, nu un parașut.
Vă mulțumim pentru lectură și păstrați-i acordul pentru următorul tutorial atunci când vom introduce testele noastre de unitate.