ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 155
Numărul 154 Numărul 153 Numărul 152 Numărul 151 Numărul 150 Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 155
Abonamente

Controlează-ți designul: Principii funcționale vs. Frameworkuri

Tiberiu Tofan
Software Engineer @ ING Hubs Romania



PROGRAMARE

Frameworkurile moderne, precum Spring, accelerează dezvoltarea, dar pot dicta arhitectura și deciziile de design. Întrebarea esențială: cine controlează cu adevărat designul aplicației — noi sau frameworkul?

Arhitectura hexagonală

Un principiu fundamental este izolarea logicii de business (domeniul) de infrastructură și framework. Dacă domeniul tău depinde direct de Spring, JPA, clientul HTTP sau orice altă integrare, orice schimbare tehnologică devine dificilă, iar testarea complicată. O soluție elegantă este arhitectura hexagonală. Domeniul definește doar interfețe (porturi), iar infrastructura le implementează prin adaptoare. Domeniul nu știe nimic despre framework sau infrastructură. În continuare vom explora o parte din tehnicile concrete pe care le putem folosi pentru a implementa arhitectura hexagonală în proiectele noastre.

Evită "Primitive Obsession"

Folosirea excesivă a tipurilor simple (String, int. etc.) pentru date cu semnificație de business duce la erori și la cod greu de întreținut. Soluția: tipuri specializate cu validare în constructor. Astfel, codul devine mai sigur și mai ușor de testat, iar validarea reutilizabilă.

public record MerchantCategoryCode(String value) {
  public MerchantCategoryCode {
   Objects.requireNonNull(value, 
   "MCC cannot be null");

  if (!value.matches("\\d{4}")) {
   throw new 
   IllegalArgumentException(
   "MCC must be exactly 4 digits");
    }
  }
}

O obiecție validă ar putea fi: dacă încapsulăm fiecare tip de bază într-o nouă clasă, cum va fi afectată performanța aplicației noastre? În limbaje precum Kotlin această problemă este rezolvată deja prin inline sau value classes, prin utilizarea cărora clasele precum cea de mai sus sunt șterse după compilare, păstrând toate avantajele enumerate fără a afecta performanța la runtime. Un concept similar, value objects, va fi introdus în Java prin proiectul Valhalla. Între timp considerăm că mentenabilitatea și lizibilitatea codului nostru ar trebui să fie o prioritate, iar optimizările le vom face acolo unde testele de performanță ne-o vor cere.

Segregarea interfețelor

În migrarea noastră de la o arhitectură stratificată către cea hexagonală, vom izola domeniul nostru prin folosirea de interfețe (porturi). O capcană în care am putea pica ușor este să folosim direct CategorizedTransactionRepository ca implementare de Spring Data JPA. Este doar o interfață, nu?! Dar la o privire atentă vom observa că este strâns legată de Spring, prin moștenire și prin adnotări.

Când ne definim interfața pentru accesul bazei de date în domeniu, putem face câțiva pași suplimentari. În loc de o singură interfață cu toate funcțiile de acces, ne putem defini interfețe funcționale individuale. Astfel, vom implementa principiul de segregare a interfețelor (I din SOLID) și vom obține o multitudine de beneficii, cum ar fi: în implementarea domeniului putem folosi doar interfețele de care avem nevoie, fără dependențe inutile care duc la cuplare suplimentară; putem testa folosind expresii lambda în loc de mockuri; putem înlocui cu ușurință o implementare fără a afecta alte părți ale sistemului; facilităm refactorizări viitoare și integrarea de noi tehnologii; facilităm migrarea tehnologiilor sau a bazelor de date.

Dar încercând să implementăm aceste concepte, în special în contextul utilizării unui framework pentru injectarea dependențelor (DI), ne va îngrijora numărul mare de implementări necesare. Vom vedea în continuare că avem o alternativă.

Un exemplu relevant este interfața repository cu care am început mai sus. Pentru fiecare metodă pe care o expune putem extrage în domeniul nostru o interfață funcțională:

@FunctionalInterface
interface FindByTransactionId {
    Optional 
    findBy(TransactionId id);
}

Similar vom avea SaveCategorizedTransaction, FindByTransactionId, FindBudgetsByCategory și FindExpenseCategoriesByClient. În implementarea serviciului nostru vom alege interacțiunile de care avem nevoie:

public class TransactionCategorizer {
    public TransactionCategorizer(
    SaveCategorizedTransaction saveCategorizedTr,    
    FindByTransactionId findByTransactionId) {/*...*/}
}

Dar, în același timp, putem folosi un singur adaptor care să implementeze toate aceste interfețe, făcând tranziția dinspre domeniu către infrastructură:

public class CategorizedTransactionRepositoryAdapter implements
    SaveCategorizedTransaction,
    FindByTransactionId, /*etc*/
{}

Iar folosind Spring putem injecta același adaptor de mai multe ori în mai multe puncte ale implementării noastre de business, refolosind aceeași implementare:

@Bean
public TransactionCategorizer 
transactionCategorizationService(
   CategorizedTransactionRepositoryAdapter 
   repositoryAdapter) {
    return new TransactionCategorizer(
    repositoryAdapter, repositoryAdapter);
}

Deși un număr mare de dependențe poate indica o problemă de design, există situații în care găsim acest lucru acceptabil, cum ar fi un REST controller care expune mai multe endpointuri de interogare a datelor. În acest caz putem aplica o soluție care va simplifica și mai mult codul, profitând de capabilitatea frameworkului de a face dependency injection pe baza tipurilor generice.

@RestController
public class CategorizedTransactionController {
    public CategorizedTransactionController(
    R repository) {
      this.repository = repository;
    }
}

În cazul de mai sus, beanul cu implementarea CategorizedTransactionRepositoryAdapter va fi injectat automat de către Spring, soluția fiind concisă și păstrând o cuplare minimă între componentele noastre.

Un mare avantaj al noului nostru design este ușurința cu care putem testa, fără a folosi mockuri:

final var service = new TransactionCategorizer(
    (categorizedTransaction) ->  
     categorizedTransaction.withId(
        new CategorizedTransactionId(1L)
    ),
    (transactionId) -> Optional.empty()
);

Erorile ca valori: o paradigmă funcțională

În mod tradițional erorile sunt tratate ca excepții, deseori introducând probleme subtile: fluxuri de control greu de urmărit, debugging anevoios și testare problematică. Toate acestea se traduc în costuri mari și pierderi semnificative. Dar cum ar fi dacă am putea gestiona erorile explicit, transparent și sistematic? Răspunsul vine din programarea funcțională, care propune tratarea erorilor ca valori clare și explicite, integrate în logica aplicațiilor. Această abordare ne permite să transformăm radical gestionarea erorilor, făcând-o vizibilă și ușor de administrat încă din faza de dezvoltare.

În locul excepțiilor, funcțiile noastre returnează explicit rezultatele, fie ele succese sau erori. Tipuri precum Either\ devin norma, clarificând direct posibilele rezultate. Fiecare eroare este modelată explicit folosind tipuri sumă algebrice (ADT-uri), ceea ce ne permite să definim clar și detaliat fiecare scenariu problematic. Astfel, orice nouă eroare adăugată este imediat evidențiată de compilator, reducând dramatic riscul să ignorăm cazuri neprevăzute.

sealed interface TransactionCategorizationError 
permits TransactionCategoryNotFound,
  MerchantCategoryLookupFailed {

  record TransactionCategoryNotFound(String message) 
  implements TransactionCategorizationError {}

  record MerchantCategoryLookupFailed(String message) 
  implements TransactionCategorizationError {}
}

public Either categorize(Transaction 
  transaction) { ... }

Concluzie

Frameworkurile sunt unelte, nu reguli absolute. Ține domeniul curat, definește-ți propriile interfețe și tipuri, tratează erorile ca parte din domeniu și lasă infrastructura să se adapteze la nevoile tale, nu invers. Vei avea un cod mai clar, mai ușor de testat și de întreținut, indiferent de frameworkul folosit. Iar înlocuirea unui framework sau a unei biblioteci va deveni palpabilă, nu un concept teoretic pe care nu am îndrăzni să-l punem în practică.

Principiile expuse mai sus reprezintă doar un sneak peek pentru prezentarea pe care o voi susține alături de colegul meu, Gabriel Bornea, pe 17 iunie, în cadrul hangoutului de la sediul ING Hubs din Cluj-Napoca, numit "Functional approaches to software design & concurrency". Vă puteți înregistra la eveniment aici.

LANSAREA NUMĂRULUI 156

Design and human touch

Joi, 19 Iunie, ora 18:00

msg systems Romania

Facebook Meetup StreamEvent YouTube

NUMĂRUL 155 - Software Craftsmanship

Sponsori

  • BT Code Crafters
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • GlobalLogic