We Do Not Need Senior Developers, We Need Senior Codebases

Intru si eu in discutie ca imi place subiectul :slight_smile:

Cineva spunea: “Odata lansat in productie (deci release) codul devine legacy deja si trebuie mentinut”. Nu exista cod perfect, asa cum nimic nu e perfect pe lumea asta. Orice cod poate fi (si trebuie sa fie) imbunatatit constant. Mie imi place sa ma uit la cod ca la o fiinta vie care trebuie sa creasca si sa evolueze, nu sa stea pe loc.

Multe proiecte incep, se dezvolta pe repede-inainte de o echipa de dezvoltare mare, iar dupa ce este lansat in productie intra intr-o faza de mentenanta.
De multe ori mai raman doar cativa programatori din echipa initiala sa mentina proiectul, iar de cele mai multe ori nu sunt aia bunii care au gandit proiectul. Aia fie sunt scumpi si nu se mai justifica ca buget pe proiect, fie s-au plictisit deja si cauta ei alte proiecte noi.

Mai sunt cateva meme-uri care imi place:
“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.” si
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

Ce inseamna asta concret? Cum punem in practica aceste idei care par simple?
Raspunsul: dupa ani si ani in care am scris cod si am evoluat ca programatori si ca oameni. Daca am vrut sa evoluam si nu am stat inchisi in bula noastra de autosuficienta si de “sunt foarte tare, uite ce cod complex am scris pe care nici eu nu il mai inteleg dupa 3 luni”.

E nevoie de multi ani si multe proiecte diverse sa ajungi sa scrii cod clar si frumos, care sa fie inteles de oameni.

Mai exista un termen care imi place: Scream Architecture. Adica un cod care “tipa” ce face doar cand te uiti la el. Un cod care nu are nevoie de comentarii sau documente de sute de pagini pentru a intelege ce face.
Abia asta numesc eu cod frumos: un cod la care ma uit pentru prima data (sau dupa cateva luni/ani) si in cateva minute pot sa imi dau seama “ce a vrut sa zica autorul”.
E foarte complicat sa ajungi la simplitatea asta, iar multi nu ajung niciodata :slight_smile:.

Cateva idei:

  • Un programator trebuie sa stie/invete arhitectura si design. Uneori trebuie efectiv sa stai sa modelezi, in loc sa scrii cod din prima zi a proiectului.
  • Un programator trebuie sa priveasca un proiect din mai multe puncte de vedere: in ansamblul sau, pe componente (servicii, APIs, etc.), low-level la nivelul codului pe care il scrie intr-o zi obisnuita. Intr-un proiect (sau sistem) mare, toate astea conlucreaza si trebuie sa se imbine frumos. Degeaba ai super arhitectura gandita de un guru, daca acesta a parasit proiectul dupa 3 luni, iar cei ramasi habar n-au despre arhitectura respectiva, ei privind proiectul doar de la nivelul codului pe care il scriu zilnic.
  • SOLID principles - eu ma intorc mereu si mereu la aceste principii simple, si mereu descopar lucruri noi sau modalitati de a-mi imbunatati codul.
  • KISS - Keep it Simple Stupid! Folositi un limbaj OOP? Cat de mari sunt clasele voastre, in linii de cod? Depasesc 100-200 de linii de cod? Refactor, sunt prea mari. La fel despre metode: cati parametri de intrare aveti pe o metoda? 10 deja? Sunt prea multi => refactor!
  • Composition over Inheritance! Eu folosesc limbaje OOP (C# mostly) de 15 ani, dar abia in ultimii 2-3 ani, cand am invatat Javascript, am inteles care e problema cu Inheritance-ul in OOP, daca e abuzat. Composition is better than Inheritance - o spun multi, nu doar eu.
15 Likes

Octav a subliniat toate punctele și dpdv al meu ăsta ar trebui să fie “răspunsul”.

Iar articolul original este f bun, nu știu de unde comparația cu “scris de un influencer” and shit.

Eu unul m-am săturat de mascota națională Dorel. Merge și așa, putem face bani oricum.
E adevărat că merge și așa.
Multe în România merg și așa.
Se vede cu ochiul liber.
Avem excepții dar majoritatea “codebase”-ului România e “spaghetti”.

Oare framework-urile, OS-urile, API-urile cele mai des folosite de cei care au scris aici sunt făcute tot în stilul ăsta? Cred că nu.

Mai toate framework-urile și “pachetele” open-source răspândite au teste, documentație clară, cod citibil pe > 50% din codebase.

7 Likes

Chiar ar fi interesant ca cineva de pe forum care a interactionat cu api-urile unui OS sa ne zica cum sunt facute si cum se interactioneaza cu ele :smiley:

Nota:
Din punctul meu de vedere, nu este o singura solutie. :smiley:

1 Like

LOC e o metrica foarte proasta pentru multe chestii, de ce ar fi o metrica buna aici?

La un proiect mare daca aplici sfatul asta, te alegi cu un zillion de clase de nu o sa mai stii ce si cum.

Sa le zici asta si la astia de fac biblioteci cu python, ca nu prea stiu de limita asta arbitrar trasa. Exemplu ales la intamplare: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html

In CakePHP pentru implode()/explode() folosea parametri inversati pana in 2019. Nici nu stiam ca se poate asa ceva. Lucrez de ~15 ani cu PHP si niciodata nu am folosit functiile asa. O fi de pe vremea PHP 3.x sau mai de demult. E de pe vremea versiunii 4.x se pare.

Cat despre Win32 API, nimeni? Toata lumea il uraste. La fel ca MFC (Microsoft Foundation Classes).

“Las’ ca merge s-asa” as zice ca e imnul Europei de vest. Doar ca indiferenta se face pe 2000+ de euro, fata de 2000+ de lei. Plus ca lantul e lung si cineva la un moment dat va munci de doua ori sa repare.

Altfel, lucrez cu API-uri la greu si toate sunt proaste:

  • null pointer exception la greu;
  • incarcare de fisiere de sute de MB direct in memorie
  • dashboard-uri nefunctionale (si parca ar fi scrise in MS Word)
  • date lipsite de consistenta. Primesc lista de entitati, separat descrierile. Ei bine, la descrieri primesc informatii despre entitati care nu mi-au fost trimise in apelul de entitati.
  • la ultima discutie despre un client, reprezentatul se mira cum de primim date pentru API key-ul care-l aveam ca era dezactivat la ei in sistem. Dar noi primeam date fara nici o problema.
  • documentatie scrisa in 80% din cazuri “ca sa fie”.
  • json_decode() din PHP are un memory leak al naibii de grav pentru o anumita forma de JSON care fix noi o folosim. Dupa vreo 10 decodificari, ramane procesul fara memorie si desi am eliberat memoria in nenumarate feluri, tot ramane fara. Folosim un parser custom si asa nu mai avem problema asta.
4 Likes

Pentru că acele „multe chestii” sunt pentru manageri. În cazul ăsta, LOC este o treabă de care te lovești iar și iar. Nu știu unde să trag linie, dar prefer metode cât mai scurte, sunt mai ușor de urmărit.

Permite-mi să citez un clasic în viață :troll::

Numărul mare al parametrilor este o altă abominație. Mai mult de trei (hai patru) non-opționali și mie îmi este imposibil să scriu cod fără să mă uit în manual. Iar exemplul tău are fix doi parametri obligatorii, deci e decent.

Sunt și excepții, desigur, dar, de obicei, mulți parametri indică faptul că metoda aia face prea multe.

1 Like

Bineinteles ca exista alte criterii pentru a sparge clasele, etc.

Iar metodele scurte, usor de urmarit, se pot dovedi horror cand de fapt sunt parte din ceva foarte complex, imprastiat fara sa fie necesar in tspe milioane de metode scurte.

Nah, n-am precizat nicaieri ca s-a implementat asa in urma vreunei credinte, vot, etc. Am indicat doar un fapt.

In rest, pun si aici ceva ce am trimis deja pe privat cuiva:

Sunt o gramada de locuri unde flexibilitatea si complexitatea nu iti permit refactorizari de-alea cu conditii arbitrare. Situatia poate fi chiar complexa, nu totdeauna poti sa KISS daca e complex. Oricat de stupid ar fi cineva.

Uite un apel lapack de exemplu:
http://www.phy.olemiss.edu/~kbeach/guide/2013/01/01/lapack/
Ce e mai enervant acolo nu e numarul mare de parametri, ci denumirile aiurea facute stil anii 70, ca le era greu sa bage gauri in cartele sau ce mama dracului.

1 Like

Nu zice nimeni să fii obsedat de treaba asta, să rămâi blocat dacă nu poți reduce numărul de parametri.

Dacă nu se poate altfel decât să ai 15% din metode în felul ăsta, nu e nici o problemă. Să nu faci măcar să nu faci un efort de a-i reduce… cam este, mai ales dacă folosești OOP.

Pai intotdeauna se poate. Impachetezi parametrii aia intr-o structura/clasa, ceva, si dai impresii ca e mai ‘clean’. Treaba e ca doar ascunzi complexitatea, nu o elimini.

Iar uneori complexitatea ascunsa poate fi mult mai problematica decat cea expusa (la intelegerea codului, etc).

Ceva mai modern, care foloseste OOP: https://docs.opencv.org/4.3.0/d9/d0c/group__calib3d.html#ga91018d80e2a93ade37539f01e6f07de5 15 parametri.

Da, am observat si eu patternul asta, eventual se mai pune un layer in fata si e si mai rau, e valid argumentul ca functia aia face pre-mult, dar pe de alta parta nu vrei sa ai foarte multe functii sa tot trebuie sa trimiti aceeasi parameterii de la una la alta.

Cred ca Java/Sping e mai ok-ish aici, ai dependency injection, si parameterii se muta pe class fields. Nici abordarea python nu e rea cu named parameters si defaults. In afara de asta, e o problema de care ma lovesc frecvent.

Poate am exagerat putin cu maxim 100-200 linii de cod. Daca mai ai si-un prettier acolo care-ti pune aproape orice pe o linie noua, ajungi repede la “limita”. Nu am vrut sa se inteleaga ca este o limita impusa. Poate ca nici eu nu ma limitez mereu la 200 de linii de cod, dar pe la 300-400 incep sa-mi pun intrebari daca nu as putea refactoriza.
Cred ca trebuia sa mentionez ca eu vorbesc din prisma C# si Typescript (ambele limbaje OOP), pentru ca doar aceste limbaje folosesc zilnic la serviciu.

Complexitatea este diferita de complicat. Orice proiect mediu spre mare are o complexitate mare. Depinde de programatorii care-l implementeaza daca-l fac si complicat.

Mie imi place sa ma uit la orice clasa ca la un API.
Partea publica (interfata cu exteriorul) este intotdeauna sus si trebuie sa fie extrem de clara, astfel incat oricine deschide fisierul respectiv, sa isi dea seama in cateva secunde care este rolul acelei clase, nu sa scroleze si sa citeasca ecrane si ecrane de cod pana intelege ceva.
Dupa ce isi face rapid o privire de ansamblu, poate decide daca trebuie sa sape mai adanc.

Cred ca depinde si ce IDE sau Tools folosesti, si cat de bine le folosesti.
Chiar saptamana trecuta faceam un fel de review/pair-programming remote la un coleg, iar el efectiv “scrolla” prin lista de fisiere ca sa gaseasca un fisier anume, desi stiam amandoi numele fisierului sau clasei cautate.
E doar un exemplu, dar daca stai mereu sa “scrollezi” prin toate fisierele din proiect cand cauti ceva anume, evident ca devine un chin cand ai mii si mii de fisiere/clase mici.
Pentru asta s-a inventat cautarea, iar anumite IDE-uri au o cautare foarte “smart” care iti permite sa gasesti orice clasa/fisier in cateva secunde, DACA stii sau intuiesti cum se numeste ceea ce cauti.

Iar aici incepe o alta discutie despre denumiri de clase, metode, variabile si asa mai departe.
Ca sa gasesti orice in proiect relativ repede, trebuie sa stii structura de ansamblu a proiectului si toti oamenii de pe proiect sa vorbeasca aceeasi limba - acel “Ubiquitous Language” din Domain Driven Design.

Eu, unul, nu cred ca se pune suficient de mult accent pe “a scrie cod pentru oameni, nu pentru computer”. Intr-o echipa mare si proiecte de lunga durata, acest lucru devine mult mai important sau chiar esential.
Cred ca un programator poate evolua extraordinar de mult prin code-reviews si pair-programming.

3 Likes

Ideea era ca exista criterii mult mai bune pentru a separa portiuni de cod (sau a impacheta parametri).

LOC nu e tocmai metrica ideala.

Exemplu: https://github.com/aromanro/TEBD/blob/master/TEBD/iTEBD.hpp
La linia 162 ai o functie, ContractTwoSites. Practic sunt 4 linii care fac treaba si au semantica esentiala.
Eu zic ca e mai dificil sa intelegi acele 4 linii decat sute de linii de prin alte parti.

Nu stiu cine ti-a pus flag acolo, dar nu eu.

N-am nici o treaba cu credintele tale, sincer eu iti dadeam ca exemplu si codul tau, singura problema e ca habar nu am daca exista ceva public si care e complexitatea lui, asa ca mi s-a parut mai la indemana sa folosesc exemplul propriu, nu de alta, dar stiu destul de bine despre ce e vorba, exact pentru ca eu am scris codul.

Sper ca ai inteles ideea.

Este mai clean, dar nu pentru ca elimini complexitatea ci pentru ca o encapsulezi intr-o structura bine definita si validata.

Cand dai acel exemplu modern (si daca nu esti ironic ar trebui sa fie ) are ca primi 3 parametri ceva cu explicatie :

Vector of vectors of the calibration pattern points. The same structure as in [calibrateCamera]

si cu mesajul:

Therefore, objectPoints.size(), imagePoints1.size(), and imagePoints2.size() need to be equal as well as objectPoints[i].size(), imagePoints1[i].size(), and imagePoints2[i].size() need to be equal for each i

Ceea ce inseamna ca aceasta metoda pe langa scopul ei principal trebuie sa valideze si toate aceste InputArrayOfArrays care nu ar trebui sa fie in responsabilitatea ei. Daca in schimb acei parametri erau encapsulati intr-o clasa puteai pastra single responsability

Nu am pus pentru tine NPC-ul stai chill, l-am pus pentru colegii care emit puternic virtue signals.

Intotdeauna poti sa critici cod pe care il vezi. Inainte sa faci asta, intreaba-te daca tu erai in stare sa-l implementezi.

Singura problema cu asemenea overenginnering e ca de fapt codul se complica si mai mult. Daca vrei cu adevarat, poti sa iei cod de 100 de linii si aplicand sfaturi de-alea sa-l faci de 10000.

O plangere, asa, gasita la repezeala: https://news.ycombinator.com/item?id=12377507 Sigur nu e unica.

Pana si xckd are gluma pe seama asta: https://xkcd.com/974/

Un proiect pe GitHub, stil ‘enterprise’: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition

1 Like

Daca gandesti asa de ce nu mergi mai departe :

Eu nu am spus sa faci overengineering ci sa encapsulezi, care crede-ma ca chiar face codul mai usor de inteles, pentru ca intotdeauna va fi mai usor sa am 1 variabila denumita user decat 15 variabile fiecare reprezentand un atribut al userului, asta ca sa nu mai vorbesc de cazul in care le bagi intr-un array/vector si trebuie sa iti umpli codul de if-uri ca sa verifici daca ai ce trebuie cand o clasa/struct iti garanteaza asta.

1 Like

Cand zici asa ti se pare rezonabil, doar ca atunci cand toate sfaturile de genul asta (ca doar nu numai tu ai idei de-astea rezonabile, toata echipa are, plus ca sunt sigur ca mai ai multe idei de-astea si la cum trebuie organizate clasele, etc) sunt aplicate pe un proiect, cantitatea de cod creste considerabil, idem complexitatea lui.

Hai sa venim cu idei ‘rezonabile’ pe clasa asta:


Am facut-o la repezeala pentru ca aveam nevoie de niste coeficienti. Isi face treaba. Nu e esentiala pentru proiect, puteam s-o substitui cu o librarie, dar n-am vrut sa adaug o dependinta. Acestea fiind spuse, hai sa aplicam doar sfatul tau. In loc de numerele cuantice transmise separat ca si parametri, folosim obiecte.

Sa le zicem GauntQuantumNumbers, ClebschGordanQuantumNumbers, Wigner3jQuantumNumbers… whatever. Bineinteles, derivam din clasa abstracta AbstractQuantumNumbers. Eventual facem si ceva factory?

Apoi, daca tot am inceput cu ideile bune cu impachetarea in clase a coeficientilor, de ce toate intr-o clasa, la implementarea propriu-zisa?

Hai sa spargem in mai multe: FactorialCoefficients, ClebschGordanCoefficients, Wigner3jCoefficients, GauntCoefficients… si pentru ca sunt ‘cached’, eventual bagam ceva implementare meseriasa la asta intr-o clasa de baza (ceva mai sofisticata, sa fie generica), ii zicem CachedCoefficientsBase, whatever.

Acum, ceva factory, ceva? Singleton? Poate gasim si loc de ceva bridge?

Asta e doar la repezeala, sunt sigur ca cu idei de-astea bune poti trece lejer de zece clase, doar pe o situatie simpla ca asta.

Si e doar inceputul.

Dupa ceva vreme vine careva si vede ‘impachetat’ un parametru si habar nu are ce contine, trebuie sa sape prin cod ca sa afle care-i rolul. In loc sa vada pur si simplu parametrii aia. Etc.

Codul ala ‘enterprise edition’ e scris la misto, dar unii sunt in stare sa-l ia in serios.

Asta este motivul pentru care este recomandat ca echipa sa stabileasca niste standarde si apoi sa se tina de ele. Cand cineva vine cu idee noi apoi se poate discuta si daca echipa considera ca standardele trebuie modificate, atunci se modifica.

In partea a doua generalizezi si, parerea mea, este ca nu stabiliesti bine conceptele si cand ar trebui ele aplicate.

Unul dintre primele care intra aici este Value Object. De obicei o clasa chioara in jurul unuei valori primitive dar care anumite costrangeri. Ex clasic este o adresa de e-mail care primitiv este un string, dar vei fi obligat ca peste tot in aplicatie sa faci validarea ca acel string este intradevar o adresa de e-mail. Pentru a combate asta creezi un value object caruia ii dai numele EmailString/EmailAddress etc iar apoi prin cod folosesti doar asta, ceea ce iti ofera garantia ca acolo este intradevar o adresa de e-mail. Genul asta de clasa nu are nevoie de factory sau altele, poti in schimb sa ii faci constructorul privat si sa adaugi diferite metode statice de forma : fromString, fromArray etc.

Al doilea concept aici ar fi un DTO care encapsuleaza mai multe valori primitive sau value objects, spre exemplu ai datele unui user si nu vrei sa le tii intr-un array/vector pentru ca iarasi te vei incurca cu validari peste tot. In rest functioneaza mai mult sau mai putin la fel ca un DTO.

Asta tine mai mult de denumirea claselor/variabilelor.

Cat despre factory il folosesti (in principiu) atunci cand instantierea se face in functie de input pentru a nu imple codul cu chestii de genul if(type == A) {new A} else {new B} ci apelezi pur factory->create(data).

Pentru exemplul tau propui (ironic) sa creezi GauntQuantumNumbers. Daca valoarea ta poate fi orice double (am vazut ca asta apare in cod) atunci nu este nevoie de un value object, dar daca ai nevoie sa incepi sa o limitezi (ex sa > 0 si < x si neaparat divizibila cu y) atunci este nevoie sa creezi un GauntQuantumNumber care iti va asigura acele limite.

In incheiere, daca alegi sa folosesti anumite principii, practici sau idei va ramane la latitudinea ta, dar recomandarea mea este sa incerci sa vezi si argumentele care stau la baza lor si sa iei singur decizia daca se potriveste pentru tine sau nu.

1 Like

Si totusi insisti pe povestile astea. Ok.

La exemplul initial trebuia sa observi ca ai atat parametri de intrare, cat si de iesire, sau de intrare/iesire. Nu ar fi chiar o idee buna ca sa ii bagi pe toti in oala la gramada intr-o singura clasa, fara sa conteze semnificatia lor… asa ca ai nevoie de fapt de vreo trei clase. Doar pentru un singur apel de functie care poate exista intr-un singur loc in program. Faci asta pentru cele 10000 de apeluri de functii, ai bagat 30000 de clase noi in program. Felicitari! :slight_smile:

Acum sa ne intoarcem la exemplul meu cu GauntCoefficients.

Apelul e aici:


pe la linia 249.
Linia e asta:
const double C = coeffs.getCoefficient(l, lp, L, m, mp);
Cum ar fi aratat conform indicatiilor tale?

GauntCoefficientsParams gauntCoefficientsParams(l, lp, L, m, mp); 
const double C = gauntCoeffs.getCoefficient(gauntCoefficientsParams);

???
Daca totusi e situatia mai complexa si ai si parametri de intrare, si de iesire, si intrare/iesire, faci dintr-o linie, patru?

Sau poate vrei asa:

const double C = gauntCoeffs.getCoefficient(GauntCoefficientsParams(l, lp, L, m, mp));

Ti se pare mai usor de inteles codul? Si pana la urma, de ce sa nu aplicam ideea recursiv?

const double C = gauntCoeffs.getCoefficient(GauntCoefficientsParams(GauntCoefficientsParamsParams(........(l, lp, L, m, mp).......));

Nu de alta, dar e o idee beton ca sa faci bloatware.

Ce zici tu poate fi o idee buna daca obiectul ala e folosit la mai mult decat un apel de functie. De exemplu ai mai multe functii care au exact aceiasi parametri. Sau apelezi aceeasi functie de multe ori cu aceiasi parametri. Sau (si in special) daca folosesti obiectul ala la mai mult decat pentru a pasa parametri.

Altfel, e overengineering si creeaza bloatware.

1 Like