În ultimele trei luni am încercat să scriu despre diferite subiecte prezentate în Clean Code. Chiar dacă acesta este al patrulea articol pe această temă, am sentimentul că mai există încă multe probleme despre care ar trebui să discutăm atunci când vorbim despre un cod curat şi bine scris.
Am putea spune că această carte, „Clean Code” scrisă de Robert C. Martin, a stabilit standardele în industria noastră din această perspectivă. Este biblia dezvoltatorilor şi de multe ori este utilizată drept „legea” codului. Nu vreau să intru mai adânc în acest subiect, dar promit că într-o zi voi vorbi în detaliu despre motivele pentru care ar trebui să utilizăm sau nu această carte ca un reper important al dezvoltatorilor .
În acest articol, analiza va fi orientată asupra obiectelor şi structurii datelor XXXX. Vom trece de la formatul codului la cum ar trebui să arate codul şi la cum ar trebui să implementăm funcţionalităţi diferite.
Cred că ne amintim cu toţii cursurile de la Universitate, când profesorii încercau să ne explice că ar trebui să expunem dintr-o clasă numai informaţiile de care au nevoie ceilalţi. Dar, din cauză că limbajul de programare actual ne oferă atât de uşor posibilitatea de a expune datele în afara unei clase, adesea sfârşim prin a avea o mulţime de date private dezvăluite sistemului.
Unul dintre colegii mei s-a referit la getter /setter drept dispozitivul diavolului. Este amuzant, dar uneori este adevărat.
Nu este atât de important dacă utilizăm un getter / setter al unei metode. Cel mai important lucru este să expunem datele într-un mod abstract, care să nu dezvăluie detaliile de implementare. De exemplu, dacă este nevoie să dezvăluim o informaţie legată de greutatea unei persoane, noi putem folosi un getter sau o metodă simplă. Ambele soluţii sunt bune atâta timp cât nu oferim niciun fel de detalii de implementare.
public class Person
{
public double WeightInKg
{
...
}
// OR
public double GetWeightInKg()
{
...
}
}
În afara acestei clase, nu ştii cum arată aceste date sau care este formatul lor. Dacă am adăuga getters şi setters peste tot, care ar fi valoarea încapsulării? … ZERO.
Atunci când începeți să scrieți cod, e important să țineți cont de faptul că există o mare diferenţă între structurile de date şi obiecte. Cel mai bun lucru pe care puteţi să îl faceţi este să păstraţi o linie clară între acestea două.
Structura datelor expune numai datele, fără nici un fel de funcţionalitate, în contrast cu obiectele, care expun numai funcţionalitatea. Bineînţeles, noi trebuie să ţinem minte că echilibrul dintre cele două este greu de păstrat. Vei avea nevoie de o structură a datelor care expune funcţionalitatea. Trebuie doar să ai în minte datele pe care doreşti să le expui şi unde ar trebui adăugată implementarea funcţionalităţii.
Atunci când implementezi o funcţionalitate, ar trebui să vorbeşti numai cu prieteni şi niciodată cu străini (Legea lui Demeter). Acest lucru înseamnă că o funcţie ar trebui să acceseze / apeleze numai:
• Metode din clasa în care este implementată, • Metode din obiectele create prin înseşi acele metode, • Metode din obiectele care sunt trimise drept argumente, • Metode din obiectele care ajută drept instanţe în clasele în care metoda este implementată
Structurile hibride sunt obiecte care conţin de asemenea şi structuri de date. Problema cu acestea este că e destul de greu să le adaugi funcţionalitate nouă sau date noi. Aceasta creează confuzie deoarece nu ştii ce ar trebui să adaugi acolo. Poate indica faptul că scopul acelei entităţi nu este clar şi nici dacă este necesară protejarea datelor.
Sunt utilizate mult când este nevoie să stocăm datele undeva (DB) sau să trimitem date prin fir. Acestea se numesc DTO şi de obicei nu au nici un fel de funcţionalitate. Scopul lor este bun, dar ar trebui să reţinem că trebuie să le utilizăm numai în scopul pentru care eu fost create. Altfel, o schimbare în structura de date va atrage multe schimbări în codul nostru.
Deasupra DTO avem Active Records, care sunt similare cu DTO-urile, dar au metode utilizate pentru navigare, cum ar fi Fiind, Save, Delete, Send şi aşa mai departe. Această funcţionalitate este de obicei oferită de DB. Problema cu ele este că dezvoltatorii le folosesc de obicei drept obiecte şi le adaugă noi funcţionalităţi. De aceea ajungem să avem un Active Record care are logică business în interior.
Ce ar trebui să facem? Să creăm Active Records care stochează numai structura de date şi să utilizăm obiecte separate pentru a stoca regulile de business.
De ce trebuie să discutăm despre gestionarea erorilor? Deoarece, chiar dacă scopul principal al codului nu este gestionarea erorilor, ci funcţionalitatea care este implementată, ajungem să avem cod în care poate fi văzută numai gestionarea erorilor şi ne este aproape imposibil să găsim detaliile despre funcţionalitatea reală care este expusă acolo.
Pentru a evita aceste situaţii, există câteva lucruri mărunte care pot fi făcute la nivelul codului. Mai întâi de toate, evitaţi să folosiţi coduri de eroare. Acest lucru adaugă mult cod metodelor voastre şi ascunde informaţia după excepţia în sine. De asemenea, cel care apelează trebuie să verifice de fiecare dată codul raportat şi să implementeze un manipulator special pentru diferitele coduri de eroare.
Excepţia poate fi utilizată cu succes în blocuri try/catch care pot fi văzute drept blocuri de „tranzacţie”, unde te aştepţi la excepţii şi eşti pregătit să le gestionezi. De asemenea, există o separare clară între funcţionalitate şi gestionarea excepţiei.
try
{
// Functionality
} catch (FantaException nullEx)
{
...
} catch (CokeException nullEx)
{
...
}
Este important să reţinem că funcţia n
care naşte o aşteptare ar trebui să furnizeze suficient conţinut despre sursa erorii. Ar trebui să încercăm să definim excepţiile specifice pe baza nevoilor apelantului. De ce? Pentru că scopul lor principal este să îl ajute pe apelant să găsească sursa problemelor.
Nu ar trebui niciodată să faci două lucruri:
-Să returnezi un NULL. Apelantul va fi nevoit să verifice rezultatul, dacă este nul sau nu şi să adauge un manipulator specific. Mai bine ar fi să returnezi o excepţie care poate fi arhivată şi gestionată de către apelant.
Noi suntem înconjuraţi de frontiere. Atunci când utilizăm biblioteci ale unor părţi terţe, cod implementat de alte echipe, nucleu API şi aşa mai departe. În toate aceste situaţii avem trasată o limită, dar şi un set de funcţii pe care le putem utiliza pentru a trece peste ea.
Este important de ştiut cum să păstrăm aceste hotare curate şi utile. Primul lucru pe care ar trebui să îl facem când este nevoie să utilizăm o resursă externă este să ne rezervăm timp pentru a învăţa despre ea şi a o explora. Noi trebuie să descoperim hotarul şi cum putem să gestionăm şi să utilizăm funcţionalitatea expusă de acesta.
Cea mai simplă metodă de a învăţa este să scriem teste care validează scenarii diferite. Astfel, putem fi siguri 100% că diferitele fluxuri vor funcţiona şi că ştim cum să le gestionăm. De asemenea, vom putea valida şi faptul că partea terţă ne oferă ceea ce ne trebuie cu adevărat.
Atunci când avem părţi terţe externe care expun limitări este obligatoriu să definim un adaptor care să ne izoleze de biblioteca terţei părţi. Vom avea cazuri în care va trebui să mimăm comportamentul sau când API-ul terţei părţi se va modifica. În acest caz, nu dorim să creăm efectul de domino în întregul nostru cod.
Adaptorul nu ar trebui să expună 1:1 funcţionalitatea expusă de partea terţă, ci să expună numai funcţionalitatea de care avem nevoie, nu cea care este oferită. Toate detaliile implementate ar trebui să fie puse chiar în adaptor.
Ar mai fi atât de multe lucruri de spus despre subiectele atinse în acest articol. Iată trei lucruri pe care aş dori să le ţineţi minte din acest articol:
De asemenea: Da! Fiecare dintre noi ar trebui să citească „Clean Code”.