Cele șase porunci ale unui cod bun: scrieți un cod care să reziste testului timpului

Publicat: 2022-03-11

Oamenii s-au confruntat cu arta și știința programării computerelor doar de aproximativ jumătate de secol. În comparație cu majoritatea artelor și științelor, informatica este în multe privințe doar un copil mic, care intră în pereți, se împiedică de propriile picioare și, uneori, aruncă mâncare peste masă.

Ca o consecință a tinereții sale relative, nu cred că avem încă un consens cu privire la definiția corectă a „codului bun”, deoarece această definiție continuă să evolueze. Unii vor spune că „cod bun” este un cod cu acoperire de 100% test. Alții vor spune că este super rapid și are o performanță uimitoare și va funcționa acceptabil pe hardware vechi de 10 ani.

Deși toate acestea sunt obiective lăudabile pentru dezvoltatorii de software, totuși mă aventurez să introduc o altă țintă în amestec: mentenabilitatea. Mai exact, „codul bun” este un cod care este ușor și ușor de întreținut de către o organizație (nu doar de către autorul ei!) și va trăi mai mult decât doar sprintul în care a fost scris. Următoarele sunt câteva lucruri pe care le-am descoperit în mine. cariera de inginer la companii mari și mici, în SUA și în străinătate, care par să se coreleze cu software-ul care poate fi întreținut, „bun”.

Nu vă mulțumiți niciodată cu codul care doar „funcționează”. Scrieți cod superior.
Tweet

Porunca #1: Tratează-ți codul așa cum vrei ca codul altora să te trateze

Sunt departe de a fi prima persoană care scrie că publicul principal pentru codul tău nu este compilatorul/calculatorul, ci cel care urmează să citească, să înțeleagă, să întrețină și să îmbunătățească codul (care nu vei fi neapărat tu în șase luni de acum încolo). ). Orice inginer care își merită plata poate produce cod care „funcționează”; ceea ce distinge un inginer superb este faptul că pot scrie cod care poate fi întreținut în mod eficient, care susține o afacere pe termen lung și au abilitatea de a rezolva problemele simplu și într-un mod clar și ușor de întreținut.

În orice limbaj de programare, este posibil să scrieți cod bun sau cod prost. Presupunând că judecăm un limbaj de programare după cât de bine facilitează scrierea unui cod bun (oricum ar trebui să fie cel puțin unul dintre criteriile de top), orice limbaj de programare poate fi „bun” sau „rău”, în funcție de modul în care este utilizat (sau abuzat). ).

Un exemplu de limbaj care de către mulți este considerat „curat” și ușor de citit este Python. Limbajul în sine impune un anumit nivel de disciplină a spațiului alb, iar API-urile încorporate sunt abundente și destul de consistente. Acestea fiind spuse, este posibil să creezi monștri de nedescris. De exemplu, se poate defini o clasă și se poate defini/redefini/nedefini orice metodă din acea clasă în timpul rulării (deseori denumită corecție maimuță). Această tehnică duce în mod natural la un API inconsecvent și în cel mai rău caz un monstru imposibil de depanat. S-ar putea gândi naiv: „Sigur, dar nimeni nu face asta!” Din păcate, o fac și nu durează mult să răsfoiți pypi înainte de a întâlni biblioteci substanțiale (și populare!) care (ab)folosesc în mod extensiv patch-urile maimuțelor ca nucleu al API-urilor lor. Am folosit recent o bibliotecă de rețea a cărei întreg API se modifică în funcție de starea rețelei a unui obiect. Imaginați-vă, de exemplu, apelând client.connect() și, uneori, obțineți o eroare MethodDoesNotExist în loc de HostNotFound sau NetworkUnavailable .

Porunca #2: Codul bun este ușor de citit și înțeles, în parte și în întregime

Codul bun este ușor de citit și de înțeles, parțial și în întregime, de către alții (precum și de către autor în viitor, încercând să evite sindromul „Chiar am scris asta?” ).

Prin „parțial” vreau să spun că, dacă deschid un modul sau o funcție din cod, ar trebui să pot înțelege ce face fără a fi nevoie să citesc și restul bazei de cod. Ar trebui să fie cât mai intuitiv și auto-documentat posibil.

Codul care face referire constant la detalii minuscule care afectează comportamentul din alte părți (aparent irelevante) ale bazei de cod este ca și cum ai citi o carte în care trebuie să faci referire la notele de subsol sau la un apendice la sfârșitul fiecărei propoziții. Nu vei trece niciodată de prima pagină!

Alte gânduri despre lizibilitatea „locală”:

  • Codul bine încapsulat tinde să fie mai lizibil, separând preocupările la fiecare nivel.

  • Numele contează. Activați sistemul Thinking Fast and Slow 2 mod în care creierul formează gânduri și puneți câteva gânduri reale și atente în numele variabilelor și metodelor. Cele câteva secunde în plus pot aduce dividende semnificative. O variabilă bine numită poate face codul mult mai intuitiv, în timp ce o variabilă prost numită poate duce la falsuri de cap și confuzie.

  • Inteligența este inamicul. Când utilizați tehnici, paradigme sau operații fanteziste (cum ar fi liste de înțelegere sau operatori ternari), aveți grijă să le utilizați într-un mod care să facă codul mai ușor de citit, nu doar mai scurt.

  • Consecvența este un lucru bun. Consecvența în stil, atât în ​​ceea ce privește modul în care plasați bretele, cât și în ceea ce privește operațiunile, îmbunătățește foarte mult lizibilitatea.

  • Separarea preocupărilor. Un proiect dat gestionează un număr nenumărat de ipoteze importante la nivel local în diferite puncte ale bazei de cod. Expuneți fiecare parte a bazei de cod la cât mai puține dintre aceste preocupări posibil. Să presupunem că ai avut un sistem de management al persoanelor în care un obiect persoană poate avea uneori un nume de familie nul. Pentru cineva care scrie cod într-o pagină care afișează obiecte persoane, asta ar putea fi foarte incomod! Și dacă nu mențineți un manual de „Prepoziții incomode și neevidente pe care le are baza noastră de cod” (știu că nu știu), programatorul paginii dvs. de afișare nu va ști că numele de familie pot fi nule și probabil va scrie cod cu un indicator nul. excepție atunci când apare cazul nul de familie. În schimb, gestionați aceste cazuri cu API-uri și contracte bine gândite pe care diferitele părți ale bazei de cod le folosesc pentru a interacționa între ele.

Porunca #3: Codul bun are un aspect și o arhitectură bine gândite pentru a face ca starea de gestionare să fie evidentă

Statul este inamicul. De ce? Pentru că este cea mai complexă parte a oricărei aplicații și trebuie tratată foarte deliberat și atent. Problemele obișnuite includ inconsecvențele bazei de date, actualizări parțiale ale interfeței de utilizare în cazul în care datele noi nu sunt reflectate peste tot, operațiuni în afara ordinului sau doar codul extrem de complex, cu instrucțiuni if ​​și ramuri peste tot ducând la coduri dificil de citit și chiar mai greu de întreținut. Punerea stării pe un piedestal pentru a fi tratată cu mare grijă și a fi extrem de consecventă și deliberată în ceea ce privește modul în care este accesată și modificată starea, simplifică dramatic baza de cod. Unele limbi (Haskell de exemplu) impun acest lucru la nivel programatic și sintactic. Ai fi uimit cât de mult se poate îmbunătăți claritatea bazei de cod dacă ai biblioteci de funcții pure care nu accesează nicio stare externă și apoi o suprafață mică de cod cu state care face referire la funcționalitatea pură exterioară.

Porunca #4: Codul bun nu reinventează roata, ci stă pe umerii giganților

Înainte de a reinventa o roată, gândiți-vă cât de comună este problema pe care încercați să o rezolvați sau funcția pe care încercați să o îndepliniți. Poate că cineva a implementat deja o soluție pe care o puteți folosi. Fă-ți timp să te gândești și să cercetezi orice astfel de opțiuni, dacă este cazul și disponibil.

Acestea fiind spuse, un contraargument complet rezonabil este că dependențele nu vin „gratuit” fără niciun dezavantaj. Folosind o bibliotecă terță parte sau open source care adaugă unele funcționalități interesante, vă asumați angajamentul față de acea bibliotecă și deveniți dependent de aceasta. Acesta este un angajament mare; dacă este o bibliotecă uriașă și aveți nevoie doar de puțină funcționalitate, chiar doriți să aveți sarcina de a actualiza întreaga bibliotecă dacă faceți upgrade, de exemplu, la Python 3.x? În plus, dacă întâmpinați o eroare sau doriți să îmbunătățiți funcționalitatea, fie sunteți dependent de autor (sau furnizor) pentru a furniza remedierea sau îmbunătățirea, fie, dacă este open source, vă aflați în situația de a explora un ( potențial substanțială) bază de cod cu care nu sunteți complet familiarizat cu încercarea de a remedia sau modifica un fragment obscur de funcționalitate.

Cu siguranță, cu cât codul de care depindeți este mai bine folosit, cu atât mai puțin probabil va trebui să investiți timp în întreținere. Concluzia este că merită să vă faceți propria cercetare și să vă faceți propria evaluare dacă să includeți sau nu tehnologia externă și cât de multă întreținere va adăuga această tehnologie specială stivei dvs.

Mai jos sunt câteva dintre cele mai comune exemple de lucruri pe care probabil că nu ar trebui să le reinventați în epoca modernă în proiectul dvs. (cu excepția cazului în care acestea SUNT proiectele dvs.).

Baze de date

Aflați care dintre CAP aveți nevoie pentru proiectul dvs., apoi alegeți baza de date cu proprietățile potrivite. Baza de date nu mai înseamnă doar MySQL, puteți alege dintre:

  • Schema SQL „tradițională”: Postgres / MySQL / MariaDB / MemSQL / Amazon RDS etc.
  • Magazine cu valori cheie: Redis / Memcache / Riak
  • NoSQL: MongoDB/Cassandra
  • Baze de date găzduite: AWS RDS / DynamoDB / AppEngine Datastore
  • Ridicare grele: Amazon MR / Hadoop (Hive/Pig) / Cloudera / Google Big Query
  • Chestii nebunești: Mnesia lui Erlang, datele de bază ale iOS

Straturi de abstracție a datelor

În cele mai multe circumstanțe, nu ar trebui să scrieți interogări brute în orice bază de date pe care se întâmplă să alegeți să utilizați. Mai mult decât atât, există o bibliotecă care să stea între DB și codul aplicației dvs., separând preocupările legate de gestionarea sesiunilor de baze de date concurente și detaliile schemei de codul dvs. principal. Cel puțin, nu ar trebui să aveți niciodată interogări brute sau SQL în linie în mijlocul codului aplicației. Mai degrabă, includeți-o într-o funcție și centralizați toate funcțiile într-un fișier numit ceva cu adevărat evident (de exemplu, „queries.py”). O linie precum users = load_users() , de exemplu, este infinit mai ușor de citit decât users = db.query(“SELECT username, foo, bar from users LIMIT 10 ORDER BY ID”) . Acest tip de centralizare face, de asemenea, mult mai ușor să aveți un stil consecvent în interogările dvs. și limitează numărul de locuri în care să mergeți pentru a schimba interogările în cazul în care schema se schimbă.

Alte biblioteci și instrumente obișnuite pe care trebuie să le luați în considerare

  • Servicii de așteptare sau Pub/Sub. Alegeți furnizorii AMQP, ZeroMQ, RabbitMQ, Amazon SQS
  • Depozitare. Amazon S3, Google Cloud Storage
  • Monitorizare: Grafit/Graphit găzduit, AWS Cloud Watch, New Relic
  • Colectare/agregare jurnal. Loggly, Splunk

Scalare automată

  • Scalare automată. Heroku, AWS Beanstalk, AppEngine, AWS Opsworks, Digital Ocean

Porunca nr. 5: Nu treceți râurile!

Există multe modele bune pentru design de programare, pub/sub, actori, MVC etc. Alegeți ceea ce vă place mai mult și rămâneți la el. Diferite tipuri de logică care se ocupă de diferite tipuri de date ar trebui să fie izolate fizic în baza de cod (din nou, acest concept de separare a preocupărilor și reducerea sarcinii cognitive asupra viitorului cititor). Codul care vă actualizează interfața de utilizare ar trebui să fie fizic diferit de codul care calculează ceea ce intră în interfața de utilizare, de exemplu.

Porunca #6: Când este posibil, lăsați computerul să facă treaba

Dacă compilatorul poate detecta erori logice în codul dvs. și poate preveni fie comportamentul rău, erorile sau blocările directe, ar trebui neapărat să profităm de asta. Desigur, unele limbi au compilatoare care fac acest lucru mai ușor decât altele. Haskell, de exemplu, are un compilator renumit, strict, care are ca rezultat ca programatorii să-și petreacă cea mai mare parte a efortului doar pentru a obține codul de compilat. Odată ce se compila, „funcționează”. Pentru aceia dintre voi care fie nu au scris niciodată într-un limbaj funcțional puternic tastat, acest lucru poate părea ridicol sau imposibil, dar nu mă credeți pe cuvânt. Serios, dați clic pe unele dintre aceste link-uri, este absolut posibil să trăiți într-o lume fără erori de rulare. Și chiar este atât de magic.

Desigur, nu orice limbă are un compilator sau o sintaxă care se pretează la multe (sau, în unele cazuri, oricare!) verificări în timpul compilării. Pentru cei care nu o fac, alocă câteva minute pentru a cerceta ce verificări opționale de strictețe le poți activa în proiectul tău și pentru a evalua dacă au sens pentru tine. O listă scurtă, necuprinzătoare a unora dintre cele comune pe care le-am folosit în ultimul timp pentru limbi cu durate de execuție îngăduitoare includ:

  • Python: pylint, pyflakes, avertismente, avertismente în emacs
  • Ruby: avertismente
  • JavaScript: jslint

Concluzie

Aceasta nu este nicidecum o listă exhaustivă sau perfectă de porunci pentru producerea unui cod „bun” (adică, ușor de întreținut). Acestea fiind spuse, dacă fiecare bază de cod pe care a trebuit să o iau în viitor ar urma chiar și jumătate din conceptele din această listă, voi avea mult mai puține fire de păr și ar putea chiar să adaug încă cinci ani la sfârșitul vieții mele. Și cu siguranță voi găsi munca mai plăcută și mai puțin stresantă.