← Înapoi la „cum e construit”

Documentație în lucru: e verificată rând cu rând înainte de testarea deschisă. Nu lua nimic de-a gata.

Arhitectura

O aplicație React Native, un site Next.js și o bază de date Postgres în Uniunea Europeană, cu toată logica de domeniu — viteza, coeficienții, consangvinitatea, construirea pedigree-ului, citirea inelelor, fiecare importator — într-un singur pachet pur, testat unitar, pe care îl folosesc amândoi clienții. Autorizarea stă în baza de date, nu în codul aplicației. Pagina asta e proiectul, deciziile din spatele lui și cele trei care au ieșit prost destul cât să merite citite.

Contextul și scopurile

Programele pentru columbofilie sunt o piață mică, prost servită. Uneltele consacrate de pe desktop sunt legate de Windows, costă peste 150 €, și nu au nimic pe telefon. Aplicația mobilă cu cele mai multe instalări stă la 3,1 stele. Unii concurenți din cloud fac în așa fel încât un abonament expirat să însemne că un crescător nu mai ajunge la propriile lui însemnări; aici, un abonament expirat înseamnă un cont rămas de citit, din care exportul iese la fel.

Evidența unei crescătorii înseamnă douăzeci de ani de decizii de împerechere. E felul de date cel mai nepotrivit de ținut ostatic, iar slăbiciunea celor consacrați nu stă în algoritmi — stă în faptul că sunt neplăcute de folosit și greu de părăsit.

Deci scopurile, în ordinea în care constrâng proiectul:

  1. Calitate pe telefon, întâi. Aplicația trebuie să se folosească cu o singură mână, în hală, cu mâinile murdare, pe un telefon Android vechi. Ăsta e produsul.
  2. Datele sunt ale crescătorului. Exportul e gratuit în luna de probă și după ea, formatul e simplu, și nu se reține nimic când expiră un abonament.
  3. Import de acolo unde e el deja. CSV-urile altor aplicații, exporturile aparatelor de cronometrare, clasamentele federațiilor.
  4. Româna întâi, construit ca să se extindă prin Europa. Româna e limba sursă; engleza o oglindește.
  5. Numere corecte. Un crescător care nu poate avea încredere în coeficient n-o să aibă încredere în nimic altceva de pe ecran.

Construit de un singur om, ceea ce e o constrângere de proiectare, nu o notă de subsol. De-asta convențiile sunt impuse de scripturi și nu de recenzii, și de-asta logica de domeniu e despărțită destul de tare cât să poată fi testată fără dispozitiv, fără rețea și fără o bază de date pornită.

Proiectul, în mare

Vedere de ansamblu: două suprafețe client și un singur pachet de domeniu comun vorbesc cu o bază de date, un depozit de fișiere și funcții pe server; doar funcțiile pe server ajung la site-ul extern al federației.

Șase componente, după rol:

  • Aplicația mobilă — produsul. Ecrane, formulare, grafice.
  • Site-ul web — prezentare, paginile publice de pedigree și de crescătorie pe care un crescător alege să le publice, generarea PDF-urilor și paginile legale.
  • Pachetul de domeniu — TypeScript pur. Fără intrări-ieșiri, fără framework, fără platformă. Amândoi clienții îl importă, și acolo se produce fiecare număr de pe ecranele proiectului.
  • Stratul de date — repozitoriile. Singurul cod care vorbește cu baza de date.
  • Baza de date — Postgres, cu securitate pe rând pe fiecare tabel. Fotografiile stau lângă ea, într-un depozit de fișiere privat.
  • Funcțiile pe server — puținul care nu se poate face pe telefon: ștergerea contului și importul de la federație.

Tot, în afară de aplicația mobilă însăși, e găzduit în Uniunea Europeană.

Site-ul federației e extern și doar de citit, iar doar funcțiile pe server ajung vreodată la el. Asta nu e un detaliu de implementare — e punctul care face demonstrabile regulile de purtare ale importului, iar o regulă din build oprește totul dacă aplicația încearcă să ajungă acolo direct.

Deciziile importante și ce ne-au costat

Expo și rutare după fișiere, pentru aplicația mobilă. Un singur cod, plus actualizări prin aer (over-the-air) pentru orice e scris în JavaScript, plus un serviciu de build administrat în locul unei configurații de semnare. Costul e o linie despărțitoare între modificările care ajung instantaneu la telefon și cele care cer o reconstrucție nativă — o deosebire pe care trebuie s-o ții mereu în cap și care e scrisă undeva pentru că, dacă o uiți, pierzi o zi.

Next.js pentru partea de web. Paginile publice trebuie să poată fi indexate, iar PDF-urile au nevoie de un mediu adevărat de randare, și un framework mobil nu face bine niciuna dintre ele pe web. Alegerea unei aplicații separate în loc de un singur cod universal înseamnă două suprafețe client de ținut în acord, iar pachetul de domeniu comun e ceea ce face asta suportabil: cele două aplicații se ceartă despre așezarea în pagină, niciodată despre ce e un coeficient.

Postgres cu securitate pe rând, în locul unui strat de API care verifică permisiuni. Regula „poți citi rândurile tale” e scrisă o singură dată, în baza de date, și fiecare drum spre date trece prin ea — aplicația, site-ul, o funcție pe server, o integrare viitoare. Varianta cealaltă strânge autorizarea în codul aplicației, unde un endpoint nou stă la o verificare uitată distanță de o scurgere.

Costul e real: politicile sunt mai greu de citit decât un middleware de permisiuni, iar o greșeală într-una e tăcută — filtrează rânduri în loc să ridice o eroare. Exact de-asta suitele de teste pentru ele sunt scrise așa cum sunt, iar Securitate le descrie.

Un workspace pnpm cu Turborepo. Două aplicații și două pachete, un singur lockfile, o singură comandă care verifică tipurile, dă cu linter, formatează și testează tot. Costul e o complexitate de unelte pe care un depozit cu o singură aplicație n-ar duce-o; câștigul e că pachetul comun e cu adevărat comun, nu copiat, și că CI-ul rulează exact ce rulează un dezvoltator local.

Pachetul de domeniu față de stratul de date — decizia pe care se sprijină tot codul. Logica de domeniu e pură și nu știe nimic despre stocare. Stratul de date știe despre stocare și nu conține logică de domeniu. Tot ce un crescător ar numi un număr stă în primul; tot ce e o interogare stă în al doilea.

Asta e ceea ce face numerele testabile. Viteza, consangvinitatea, decila și curba de formă sunt testate pe fixturi, fără bază de date, fără rețea și fără dispozitiv, ceea ce înseamnă că pot fi testate exhaustiv — inclusiv cazurile incomod de produs într-o crescătorie adevărată, cum ar fi un pedigree cu o buclă în el sau un sezon în care n-a venit niciun porumbel acasă.

Accesul la date, în spatele unei cusături de repozitoriu. Ecranele nu interoghează baza de date. Țin un client și îl dau unei funcții de repozitoriu, care duce interogarea și citirea. Cusătura există ca să se poată schimba mecanismul de stocare — un motor de sincronizare offline e viitorul intenționat — fără să se rescrie ecranele.

Cinstit: cusătura e aici o convenție, nu încă un mecanism. Ține aproape peste tot, și există un ecran care interoghează un tabel direct. E un defect, e trecut ca atare, iar regula care ține linia e scrisă și rulează — oprește build-ul dacă mai apare unul — dar e fixată la locurile care există, nu la zero, fiindcă astuparea corectă a găurii cere mai mult decât o linie și făcută prost ar fi mai rea decât descrisă exact. O pagină ca asta valorează mai puțin dacă îți spune doar despre părțile care au ieșit bine.

Ce nu e excepția aia e o problemă de securitate. Cusătura ține accesul la date într-un singur strat, ca să poată fi schimbat într-un singur loc; nu ea e cea care îți ține rândurile ale tale. Interogarea aia trece prin securitatea pe rând exact ca oricare alta, iar baza de date o leagă de proprietarul ei indiferent dacă a plecat dintr-un repozitoriu sau dintr-un ecran — care e tot argumentul pentru care autorizarea stă în baza de date și nu în codul care o apelează. Urmarea excepției e inconsecvența, iar prețul inconsecvenței se plătește în ziua în care se schimbă stratul de stocare.

Și cele care au durut

Motorul de JavaScript de pe telefon nu are ce are Node. Testele rulau în Node, treceau, iar codul ar fi crăpat pe un telefon adevărat — motorul vine fără un crypto global și fără o parte din suportul de codare a textului pe care mediul de test îl oferă din oficiu. O întreagă categorie de defecte pe care o suită verde le ascunde activ.

Reparația n-a fost o recenzie de cod, fiindcă o recenzie prinde defectul o dată. E o regulă din build care pică atunci când funcțiile cu pricina apar în codul aplicației. Tiparul ăsta — când un defect se poate repeta în tăcere, scrie regula în loc de notă — e mai toată disciplina de inginerie a proiectului, și Calitate e despre el.

O trecere de fus orar, în care codul era consecvent cu sine și greșit față de lume un sprint întreg. Formularul de cursă cerea ora lansării în UTC. Crescătorii tastau ora locală tipărită pe foaia de cursă. Importatoarele produceau ore locale ca text și le dădeau unor coloane de timp care le citeau ca UTC. Totul era de acord cu tot; momentele stocate erau cu două-trei ore greșite, iar aplicația le arăta înapoi în fusul telefonului, așa că nimic nu părea în neregulă.

Nimic din cod nu se contrazicea cu sine, și exact de-asta a supraviețuit atâta. Reparația nu e o migrare — e o rețetă, rulată pe fiecare proprietar în parte, de un om care se uită întâi la o histogramă a orelor stocate, pentru că cele două populații (tastate-ca-local și tastate-ca-UTC) se suprapun și nicio regulă automată nu le poate despărți. Rulată orbește, ar muta din nou rânduri corecte, fără să se vadă. Lecția rămasă: un bug consecvent cu sine e invizibil pentru teste, și doar un reper din afară îl găsește.

Un modul nativ ținut în urmă cu o versiune majoră, intenționat. Două dependențe sunt fixate în urma a ce așteaptă framework-ul, din motive care erau corecte atunci. Costul e o verificare de dependențe permanent zgomotoasă și pierderea unei coji comode de testare, așa că fiecare probă pe dispozitiv cere un build de dezvoltare complet. E scris ca decizie cu urmările ei, nu tolerat ca zgomot, fiindcă o avertizare neexplicată devine o avertizare pe care o ignoră toată lumea.

Cum curg datele

Un rezultat de cursă

Un rezultat de cursă, de la formular prin pachetul de domeniu, cusătura de repozitoriu și securitatea pe rând în baza de date, și înapoi prin cache.

Crescătorul tastează sosirea — o oră de aparat, uneori până la milisecundă, așa cum a tipărit-o sistemul lui electronic de cronometrare. Pachetul de domeniu transformă asta, împreună cu ora lansării și cu distanța proprie a crescătoriei, într-o viteză, și convertește ora locală într-un moment printr-un singur modul testat, nu prin baza de fusuri orare a platformei, pe care motorul de pe telefon nu te poți bizui că o are.

Repozitoriul o scrie. Securitatea pe rând hotărăște, în baza de date, dacă rândul e al celui care întreabă. Citit înapoi, se întoarce prin același repozitoriu într-un cache din care desenează ecranul — așa că numărul de pe ecran vine din aceeași funcție care l-a calculat la intrare.

O urmare care merită numită. Fiindcă baza de date e locul autorizării, nu există cod de server care să poată întoarce din greșeală rezultatul altui crescător. O interogare care ar cere așa ceva primește un răspuns gol, nu un refuz — de-asta testele care o dovedesc verifică golul, nu erorile.

Un import de la federație

Îl pornește o apăsare. Tot ce urmează e format de reguli de purtare pe care proiectul și le-a asumat înainte să scrie ceva din el, și patru dintre ele se văd în acest drum:

  1. Cererea se face pentru că a cerut-o crescătorul. Nu există sarcini programate, nici măturări în fundal, nici citiri în avans, nici colectare în masă — niciodată. O cerere către federație e întotdeauna urmarea directă a unei apăsări pe import.
  2. Cererea spune cinstit cine suntem, cu o adresă de contact în ea. Nu ne dăm niciodată drept un browser.
  3. Se păstrează doar rândurile crescătorului. Foaia de clasament e citită întreagă, în memorie, și apoi redusă: rândurile lui, plus fapte despre cursă care nu numesc pe nimeni. Numele, codurile și localitățile celorlalți crescători se aruncă înainte să se scrie ceva. Asta e regula cu dinți, și e motivul pentru care citirea și reducerea sunt funcții pure cu teste, nu un pas dinăuntrul unui apel de rețea.
  4. Nu se republică nimic. Rezultatele importate rămân în contul proprietarului. Nu reconstruim un clasament, nu oferim o pagină publică de răsfoit curse, și nu lăsăm niciodată un vizitator să interogheze federația prin noi.

Sub toate astea stă un cache, așa că mai mulți crescători care au zburat aceeași cursă costă federația o citire, nu multe — ceea ce e politețe și e totodată argumentul că accesul nostru nu pune o povară de care să se plângă cineva.

Mai departe

  • Securitate — modelul de autorizare și testele care îl țin.
  • Datele tale — același sistem, descris pentru crescătorul ale cărui date sunt în el.