CodeGym /Blog Java /Aleatoriu /Reguli de codificare: de la crearea unui sistem până la l...
John Squirrels
Nivel
San Francisco

Reguli de codificare: de la crearea unui sistem până la lucrul cu obiecte

Publicat în grup
O zi bună, tuturor! Astăzi am dori să vă vorbim despre scrierea unui cod bun. Desigur, nu toată lumea vrea să mestece imediat cărți precum Clean Code, deoarece acestea conțin o mulțime de informații, dar nu prea este clar la început. Și până când termini de citit, s-ar putea să-ți distrugi toată dorința de a codifica. Având în vedere toate acestea, astăzi vreau să vă ofer un mic ghid (un mic set de recomandări) pentru a scrie un cod mai bun. În acest articol, să trecem peste regulile și conceptele de bază legate de crearea unui sistem și lucrul cu interfețe, clase și obiecte. Citirea acestui articol nu va dura mult timp și, sper, nu vă va plictisi. Îmi voi lucra de sus în jos, adică de la structura generală a unei aplicații până la detaliile ei mai restrânse. Reguli de codificare: de la crearea unui sistem la lucrul cu obiecte - 1

Sisteme

Următoarele sunt, în general, caracteristicile dezirabile ale unui sistem:
  • Complexitate minimă. Trebuie evitate proiectele prea complicate. Cel mai important lucru este simplitatea și claritatea (mai simplu = mai bine).
  • Ușurință de întreținere. Când creați o aplicație, trebuie să vă amintiți că va trebui întreținută (chiar dacă nu veți fi responsabil pentru întreținerea acesteia). Aceasta înseamnă că codul trebuie să fie clar și evident.
  • Cuplaj slab. Aceasta înseamnă că minimizăm numărul de dependențe dintre diferitele părți ale programului (maximizând conformitatea noastră cu principiile OOP).
  • Reutilizabilitate. Ne proiectăm sistemul cu capacitatea de a reutiliza componente în alte aplicații.
  • Portabilitate. Ar trebui să fie ușor să adaptați un sistem la alt mediu.
  • Stilul uniform. Proiectăm sistemul nostru folosind un stil uniform în diferitele sale componente.
  • Extensibilitate (scalabilitate). Putem îmbunătăți sistemul fără a-i încălca structura de bază (adăugarea sau modificarea unei componente nu ar trebui să le afecteze pe toate celelalte).
Este practic imposibil să construiești o aplicație care nu necesită modificări sau funcționalități noi. Va trebui să adăugăm în mod constant piese noi pentru a ne ajuta creația să țină pasul cu vremurile. Aici intervine scalabilitatea. Scalabilitatea înseamnă, în esență, extinderea aplicației, adăugarea de noi funcționalități și lucrul cu mai multe resurse (sau, cu alte cuvinte, cu o încărcare mai mare). Cu alte cuvinte, pentru a ușura adăugarea unei noi logici, respectăm câteva reguli, cum ar fi reducerea cuplării sistemului prin creșterea modularității.Reguli de codificare: de la crearea unui sistem la lucrul cu obiecte - 2

Sursa imaginii

Etapele proiectării unui sistem

  1. Sistem software. Proiectați aplicația în ansamblu.
  2. Divizarea în subsisteme/pachete. Definiți părți distincte din punct de vedere logic și definiți regulile de interacțiune între ele.
  3. Împărțirea subsistemelor în clase. Împărțiți părți ale sistemului în clase și interfețe specifice și definiți interacțiunea dintre ele.
  4. Împărțirea claselor în metode. Creați o definiție completă a metodelor necesare pentru o clasă, pe baza responsabilității care i-a fost atribuită.
  5. Proiectarea metodei. Creați o definiție detaliată a funcționalității metodelor individuale.
De obicei, dezvoltatorii obișnuiți se ocupă de acest design, în timp ce arhitectul aplicației se ocupă de punctele descrise mai sus.

Principii generale și concepte de proiectare a sistemului

Inițializare leneșă. În acest mod de programare, aplicația nu pierde timpul creând un obiect până când este folosit efectiv. Acest lucru accelerează procesul de inițializare și reduce sarcina colectorului de gunoi. Acestea fiind spuse, nu ar trebui să duceți acest lucru prea departe, deoarece asta poate încălca principiul modularității. Poate că merită mutarea tuturor instanțelor de construcție într-o anumită parte, de exemplu, metoda principală sau la o clasă din fabrică . O caracteristică a unui cod bun este absența codului repetitiv, standard. De regulă, un astfel de cod este plasat într-o clasă separată, astfel încât să poată fi apelat atunci când este necesar.

AOP

Aș dori, de asemenea, să notez programarea orientată pe aspecte. Această paradigmă de programare se referă la introducerea unei logici transparente. Adică, codul repetitiv este pus în clase (aspecte) și este apelat atunci când sunt îndeplinite anumite condiții. De exemplu, atunci când apelați o metodă cu un anumit nume sau accesați o variabilă de un anumit tip. Uneori, aspectele pot fi confuze, deoarece nu este imediat clar de unde este apelat codul, dar aceasta este încă o funcționalitate foarte utilă. Mai ales atunci când se memorează cache sau se înregistrează. Adăugăm această funcționalitate fără a adăuga o logică suplimentară la clasele obișnuite. Cele patru reguli ale lui Kent Beck pentru o arhitectură simplă:
  1. Expresivitatea — Intenția unei clase ar trebui să fie clar exprimată. Acest lucru se realizează prin denumirea corectă, dimensiunea redusă și aderarea la principiul responsabilității unice (pe care îl vom lua în considerare mai detaliat mai jos).
  2. Număr minim de clase și metode — În dorința dvs. de a face cursuri cât mai mici și cât mai puțin concentrate posibil, puteți merge prea departe (rezultând anti-modelul operației cu pușca). Acest principiu cere menținerea sistemului compact și nu merge prea departe, creând o clasă separată pentru fiecare acțiune posibilă.
  3. Fără duplicare — Codul duplicat, care creează confuzie și indică un design suboptim al sistemului, este extras și mutat într-o locație separată.
  4. Rulează toate testele — Un sistem care trece toate testele este gestionabil. Orice modificare ar putea face ca un test să eșueze, dezvăluindu-ne că schimbarea noastră în logica internă a unei metode a schimbat și comportamentul sistemului în moduri neașteptate.

SOLID

Atunci când proiectați un sistem, bine-cunoscutele principii SOLID merită luate în considerare:

S (responsabilitate unică), O (deschis-închis), L (substituție Liskov), I (segregarea interfeței), D (inversarea dependenței).

Nu ne vom opri asupra fiecărui principiu individual. Ar depăși puțin sfera acestui articol, dar puteți citi mai multe aici .

Interfață

Poate unul dintre cei mai importanți pași în crearea unei clase bine concepute este crearea unei interfețe bine proiectate care reprezintă o abstractizare bună, ascund detaliile de implementare ale clasei și prezentând simultan un grup de metode care sunt în mod clar consecvente unele cu altele. Să aruncăm o privire mai atentă la unul dintre principiile SOLID — segregarea interfeței: clienții (clasele) nu ar trebui să implementeze metode inutile pe care nu le vor folosi. Cu alte cuvinte, dacă vorbim despre crearea unei interfețe cu cel mai mic număr de metode menite să realizeze singura sarcină a interfeței (care cred că este foarte asemănătoare cu principiul responsabilității unice), este mai bine să creați câteva altele mai mici. de o interfață umflată. Din fericire, o clasă poate implementa mai mult de o interfață. Nu uitați să denumiți corect interfețele: numele ar trebui să reflecte sarcina atribuită cât mai exact posibil. Și, desigur, cu cât este mai scurt, cu atât va provoca mai puțină confuzie. Comentariile documentației sunt de obicei scrise la nivel de interfață. Aceste comentarii oferă detalii despre ce ar trebui să facă fiecare metodă, ce argumente este nevoie și ce va returna.

Clasă

Reguli de codificare: de la crearea unui sistem la lucrul cu obiecte - 3

Sursa imaginii

Să aruncăm o privire la modul în care cursurile sunt aranjate intern. Sau, mai degrabă, câteva perspective și reguli care ar trebui urmate atunci când scrieți orele. De regulă, o clasă ar trebui să înceapă cu o listă de variabile într-o anumită ordine:
  1. constante statice publice;
  2. constante statice private;
  3. variabile de instanță private.
Urmează diverșii constructori, în ordine de la cei cu cele mai puține argumente până la cei cu cele mai multe. Ele sunt urmate de metode de la cele mai publice la cele mai private. În general, metodele private care ascund implementarea unor funcționalități pe care dorim să le restricționăm sunt în partea de jos.

Mărimea clasei

Acum aș vrea să vorbesc despre dimensiunea claselor. Să ne amintim unul dintre principiile SOLID – principiul responsabilității unice. Afirmă că fiecare obiect are un singur scop (responsabilitate), iar logica tuturor metodelor sale urmărește să-l îndeplinească. Acest lucru ne spune să evităm clasele mari, umflate (care sunt de fapt anti-modelul obiectului lui Dumnezeu), iar dacă avem o mulțime de metode cu tot felul de logică diferită înghesuită într-o clasă, trebuie să ne gândim să o despărțim într-o clasă. două părți logice (clase). Acest lucru, la rândul său, va crește lizibilitatea codului, deoarece nu va dura mult pentru a înțelege scopul fiecărei metode dacă cunoaștem scopul aproximativ al oricărei clase date. De asemenea, fii atent la numele clasei, care ar trebui să reflecte logica pe care o conține. De exemplu, dacă avem o clasă cu peste 20 de cuvinte în numele ei, trebuie să ne gândim la refactorizare. Orice clasă care se respectă nu ar trebui să aibă atât de multe variabile interne. De fapt, fiecare metodă funcționează cu una sau câteva dintre ele, provocând multă coeziune în cadrul clasei (care este exact așa cum ar trebui să fie, deoarece clasa ar trebui să fie un tot unitar). Ca urmare, creșterea coeziunii unei clase duce la o reducere a dimensiunii clasei și, desigur, la creșterea numărului de clase. Acest lucru este enervant pentru unii oameni, deoarece trebuie să parcurgeți mai mult fișierele de clasă pentru a vedea cum funcționează o anumită sarcină mare. Pe deasupra, fiecare clasă este un mic modul care ar trebui să fie în relație minim cu ceilalți. Această izolare reduce numărul de modificări pe care trebuie să le facem atunci când adăugăm o logică suplimentară la o clasă. fiecare metodă funcționează cu una sau câteva dintre ele, provocând multă coeziune în cadrul clasei (care este exact așa cum ar trebui să fie, deoarece clasa ar trebui să fie un tot unitar). Ca urmare, creșterea coeziunii unei clase duce la o reducere a dimensiunii clasei și, desigur, la creșterea numărului de clase. Acest lucru este enervant pentru unii oameni, deoarece trebuie să parcurgeți mai mult fișierele de clasă pentru a vedea cum funcționează o anumită sarcină mare. Pe deasupra, fiecare clasă este un mic modul care ar trebui să fie în relație minim cu ceilalți. Această izolare reduce numărul de modificări pe care trebuie să le facem atunci când adăugăm o logică suplimentară la o clasă. fiecare metodă funcționează cu una sau câteva dintre ele, provocând multă coeziune în cadrul clasei (care este exact așa cum ar trebui să fie, deoarece clasa ar trebui să fie un tot unitar). Ca urmare, creșterea coeziunii unei clase duce la o reducere a dimensiunii clasei și, desigur, la creșterea numărului de clase. Acest lucru este enervant pentru unii oameni, deoarece trebuie să parcurgeți mai mult fișierele de clasă pentru a vedea cum funcționează o anumită sarcină mare. Pe deasupra, fiecare clasă este un mic modul care ar trebui să fie în relație minim cu ceilalți. Această izolare reduce numărul de modificări pe care trebuie să le facem atunci când adăugăm o logică suplimentară la o clasă. Coeziunea conduce la o reducere a mărimii clasei și, desigur, la creșterea numărului de clase. Acest lucru este enervant pentru unii oameni, deoarece trebuie să parcurgeți mai mult fișierele de clasă pentru a vedea cum funcționează o anumită sarcină mare. Pe deasupra, fiecare clasă este un mic modul care ar trebui să fie în relație minim cu ceilalți. Această izolare reduce numărul de modificări pe care trebuie să le facem atunci când adăugăm o logică suplimentară la o clasă. Coeziunea conduce la o reducere a mărimii clasei și, desigur, la creșterea numărului de clase. Acest lucru este enervant pentru unii oameni, deoarece trebuie să parcurgeți mai mult fișierele de clasă pentru a vedea cum funcționează o anumită sarcină mare. Pe deasupra, fiecare clasă este un mic modul care ar trebui să fie în relație minim cu ceilalți. Această izolare reduce numărul de modificări pe care trebuie să le facem atunci când adăugăm o logică suplimentară la o clasă.

Obiecte

Încapsulare

Aici vom vorbi mai întâi despre un principiu POO: încapsularea. Ascunderea implementării nu înseamnă crearea unei metode de izolație a variabilelor (restricționarea neatenționată a accesului prin metode individuale, getter și setters, ceea ce nu este bine, deoarece întregul punct de încapsulare este pierdut). Ascunderea accesului are ca scop formarea de abstracții, adică clasa oferă metode concrete partajate pe care le folosim pentru a lucra cu datele noastre. Și utilizatorul nu trebuie să știe exact cum lucrăm cu aceste date - funcționează și este suficient.

Legea lui Demeter

Putem lua în considerare și Legea lui Demeter: este un set mic de reguli care ajută la gestionarea complexității la nivel de clasă și metodă. Să presupunem că avem un obiect Car și are o metodă move(Object arg1, Object arg2) . Conform Legii lui Demeter, această metodă se limitează la apelarea:
  • metodele obiectului Car în sine (cu alte cuvinte, obiectul „acest”);
  • metode ale obiectelor create în cadrul metodei mutare ;
  • metodele obiectelor trecute ca argumente ( arg1 , arg2 );
  • metode ale obiectelor interne Car (din nou, „aceasta”).
Cu alte cuvinte, Legea lui Demeter este ceva asemănător cu ceea ce părinții i-ar putea spune unui copil: „poți vorbi cu prietenii tăi, dar nu cu străinii”.

Structură de date

O structură de date este o colecție de elemente înrudite. Când se consideră un obiect ca o structură de date, există un set de elemente de date pe care operează metodele. Existența acestor metode se presupune implicit. Adică, o structură de date este un obiect al cărui scop este stocarea și lucrul cu (procesarea) datelor stocate. Diferența sa cheie față de un obiect obișnuit este că un obiect obișnuit este o colecție de metode care operează pe elemente de date despre care se presupune implicit că există. Înțelegi? Principalul aspect al unui obiect obișnuit sunt metodele. Variabilele interne facilitează funcționarea lor corectă. Dar într-o structură de date, metodele sunt acolo pentru a vă sprijini munca cu elementele de date stocate, care sunt primordiale aici. Un tip de structură de date este un obiect de transfer de date (DTO). Aceasta este o clasă cu variabile publice și fără metode (sau numai metode de citire/scriere) care este folosită pentru a transfera date atunci când lucrezi cu baze de date, analizează mesaje din socket-uri etc. Datele nu sunt de obicei stocate în astfel de obiecte pentru o perioadă lungă de timp. Este aproape imediat convertit la tipul de entitate cu care funcționează aplicația noastră. O entitate, la rândul ei, este și o structură de date, dar scopul ei este de a participa la logica de afaceri la diferite niveluri ale aplicației. Scopul unui DTO este de a transporta date către/de la aplicație. Exemplu de DTO: este, de asemenea, o structură de date, dar scopul său este de a participa la logica de afaceri la diferite niveluri ale aplicației. Scopul unui DTO este de a transporta date către/de la aplicație. Exemplu de DTO: este, de asemenea, o structură de date, dar scopul său este de a participa la logica de afaceri la diferite niveluri ale aplicației. Scopul unui DTO este de a transporta date către/de la aplicație. Exemplu de DTO:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Totul pare destul de clar, dar aici aflăm despre existența hibrizilor. Hibrizii sunt obiecte care au metode de manipulare a logicii importante, stochează elemente interne și includ, de asemenea, metode accesorii (get/set). Astfel de obiecte sunt dezordonate și îngreunează adăugarea de noi metode. Ar trebui să le evitați, deoarece nu este clar pentru ce sunt acestea - stocarea elementelor sau executarea logicii?

Principii de creare a variabilelor

Să ne gândim puțin la variabile. Mai precis, să ne gândim la ce principii se aplică atunci când le creăm:
  1. În mod ideal, ar trebui să declarați și să inițializați o variabilă chiar înainte de a o folosi (nu creați una și uitați de ea).
  2. Ori de câte ori este posibil, declarați variabilele ca finale pentru a preveni modificarea valorii lor după inițializare.
  3. Nu uitați de variabilele counter, pe care le folosim de obicei într-un fel de buclă for . Adică, nu uitați să le eliminați. Altfel, toată logica noastră s-ar putea rupe.
  4. Ar trebui să încercați să inițializați variabilele în constructor.
  5. Dacă există posibilitatea de a alege între utilizarea unui obiect cu referință sau fără ( new SomeObject() ), optați pentru fără, deoarece după ce obiectul este utilizat, acesta va fi șters în următorul ciclu de colectare a gunoiului și resursele sale nu vor fi irosite.
  6. Păstrați durata de viață a unei variabile (distanța dintre crearea variabilei și ultima dată când este referită) cât mai scurtă posibil.
  7. Inițializați variabilele utilizate într-o buclă chiar înaintea buclei, nu la începutul metodei care conține bucla.
  8. Începeți întotdeauna cu cel mai limitat domeniu și extindeți-vă numai atunci când este necesar (ar trebui să încercați să faceți o variabilă cât mai locală).
  9. Utilizați fiecare variabilă pentru un singur scop.
  10. Evitați variabilele cu un scop ascuns, de exemplu, o variabilă împărțită între două sarcini - aceasta înseamnă că tipul acesteia nu este potrivit pentru rezolvarea uneia dintre ele.

Metode

Reguli de codificare: de la crearea unui sistem la lucrul cu obiecte - 4

din filmul "Star Wars: Episode III - Revenge of the Sith" (2005)

Să trecem direct la implementarea logicii noastre, adică la metode.
  1. Regula #1 - Compactitatea. În mod ideal, o metodă nu ar trebui să depășească 20 de linii. Aceasta înseamnă că, dacă o metodă publică „se umflă” în mod semnificativ, trebuie să vă gândiți să despărțiți logica și să o mutați în metode private separate.

  2. Regula #2 — if , else , while și alte instrucțiuni nu ar trebui să aibă blocuri puternic imbricate: o mulțime de imbricare reduce semnificativ lizibilitatea codului. În mod ideal, nu ar trebui să aveți mai mult de două blocuri {} imbricate .

    Și este, de asemenea, de dorit să păstrați codul din aceste blocuri compact și simplu.

  3. Regula #3 — O metodă ar trebui să efectueze o singură operație. Adică, dacă o metodă realizează tot felul de logică complexă, o împărțim în submetode. Ca urmare, metoda în sine va fi o fațadă al cărei scop este să apeleze toate celelalte operații în ordinea corectă.

    Dar dacă operația pare prea simplă pentru a fi pusă într-o metodă separată? Adevărat, uneori se poate simți ca și cum ai trage cu un tun în vrăbii, dar metodele mici oferă o serie de avantaje:

    • O mai bună înțelegere a codului;
    • Metodele tind să devină mai complexe pe măsură ce dezvoltarea progresează. Dacă o metodă este simplă pentru început, atunci va fi puțin mai ușor să-i complici funcționalitatea;
    • Detaliile de implementare sunt ascunse;
    • Reutilizare mai ușoară a codului;
    • Cod mai fiabil.

  4. Regula de retragere — Codul trebuie citit de sus în jos: cu cât citiți mai jos, cu atât vă adânciți în logică. Și invers, cu cât mergi mai sus, cu atât metodele sunt mai abstracte. De exemplu, instrucțiunile switch sunt mai degrabă necompacte și nedorite, dar dacă nu puteți evita utilizarea unui comutator, ar trebui să încercați să îl mutați cât mai jos posibil, la metodele de cel mai jos nivel.

  5. Argumente ale metodei — Care este numărul ideal? Ideal, deloc :) Dar se întâmplă cu adevărat asta? Acestea fiind spuse, ar trebui să încercați să aveți cât mai puține argumente, pentru că cu cât sunt mai puține, cu atât este mai ușor să folosiți o metodă și cu atât mai ușor este să o testați. Când aveți îndoieli, încercați să anticipați toate scenariile de utilizare a metodei cu un număr mare de parametri de intrare.

  6. În plus, ar fi bine să se separe metodele care au un steag boolean ca parametru de intrare, deoarece acest lucru în sine implică faptul că metoda efectuează mai multe operații (dacă este adevărată, atunci faceți un lucru; dacă fals, atunci faceți altul). După cum am scris mai sus, acest lucru nu este bine și ar trebui evitat dacă este posibil.

  7. Dacă o metodă are un număr mare de parametri de intrare (o extremă este 7, dar ar trebui să începeți cu adevărat să vă gândiți după 2-3), unele dintre argumente ar trebui grupate într-un obiect separat.

  8. Dacă există mai multe metode similare (supraîncărcate), atunci parametrii similari trebuie să fie trecuți în aceeași ordine: acest lucru îmbunătățește lizibilitatea și gradul de utilizare.

  9. Când treceți parametrii unei metode, trebuie să fiți sigur că toți sunt folosiți, altfel de ce aveți nevoie de ei? Decupați orice parametri neutilizați din interfață și terminați cu ea.

  10. try/catch nu arată foarte bine în natură, așa că ar fi o idee bună să-l mutați într-o metodă intermediară separată (o metodă pentru gestionarea excepțiilor):

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

Am vorbit mai sus despre codul duplicat, dar permiteți-mi să repet încă o dată: dacă avem câteva metode cu cod repetat, trebuie să-l mutăm într-o metodă separată. Acest lucru va face atât metoda, cât și clasa mai compacte. Nu uita de regulile care guvernează numele: detalii despre cum să denumești corect clasele, interfețele, metodele și variabilele vor fi discutate în următoarea parte a articolului. Dar asta e tot ce am pentru tine astăzi.
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION