TSM - Evolutionary Architecture: perspectiva developerului asupra unui soft de lungă durată

Denisa Lupu - Java Software Developer @ msg systems Romania


Conceptul de arhitectură, prin natura sa, descrie ceva solid, ceva ce oferă îndrumare și structură acolo unde altfel ar exista doar haos și posibilități infinite. Atunci când ne referim la arhitectura software-ului, o percepem ca fiind abordarea optimă pentru implementare - un principiu de ghidare, un cadru sau o bază la care developerii atașează conținut, cod și componente esențiale care permit întregului sistem să funcționeze. Cum putem caracteriza ceva atât de rigid precum arhitectura ca fiind evolutiv?

Termenul "Evolutionary Architecture" a fost introdus nu demult în ecosistemul software de către autorii Neal Ford și Rebbeca Parsons. Acesta este, de fapt, împrumutat din domeniul Evolutionary Computing și ne vine ca un set de bune practici care pot ajuta la ghidarea aplicațiilor noastre către viitor. Prin "viitor" nu ne referim doar la modernizare sau inovație, deoarece acest lucru ar face termenul să fie mai degrabă "Arhitectură Modernă". Acesta a fost aproape numit "Emergent Architecture", însă acest lucru ar fi fost, de asemenea, incorect, întrucât ar fi însemnat că arhitectura software-ului ar apărea treptat ca răspuns la diverse necesități și feedback, când, de fapt, arhitectura evolutivă urmărește să construiască pe o bază solidă și stabilă care se va dezvolta în timp. Aceasta poate implica orice tip de arhitectură, chiar și cu o bază fermă deja întocmită, care își propune să-și păstreze cele mai valoroase caracteristici și atribute, protejându-le în fața schimbărilor prin evoluția lor constantă.

De ce avem nevoie de evoluție?

Există multe tipuri de arhitectură, dar un lucru este cert: nu există conceptul real de arhitectură perfectă. Indiferent de tehnică, toate lucrurile au tendința de a se deteriora la un moment dat, iar termenul folosit, în principal, pentru a descrie acest proces în lumea software-ului este "bit rot" (lb. engleza: coroziunea/putrezirea biților). Acest proces de coroziune se referă, în primul rând, la degradarea progresivă a codului și a dependințelor asociate. Deși este cert că întotdeauna vor apărea noi tehnologii în ecosistemul software, acest lucru nu înseamnă că aplicațiile noastre trebuie neapărat să se deterioreze ireversibil. Ceea ce este vechi nu înseamnă neapărat că este și rău. În ceea ce privește arhitectura evolutivă, nu dorim ca aplicația noastră să fie nou-nouță și la modă în ceea ce înseamnă tehnologie, ci dorim o evoluție a codului nostru. Așadar, cum păstrăm codul nostru "viu și sănătos" în fața tuturor schimbărilor neprevăzute din ecosistemul software? Cheia arhitecturii evolutive, așa cum vom vedea în acest articol constă în protejarea funcționalităților sau capacităților celor mai valoroase ale aplicației noastre.

Există o lungă listă de software-uri care s-au prăbușit în timp din cauza gestionării slabe a caracteristicilor lor cele mai valoroase. Nu demult, aveam cu toții o pagină de profil pe MySpace. Acum, această companie a devenit o lecție pentru start-upuri despre ce înseamnă să ai un website ca "un mare ghem de spaghete" și despre ce se întâmplă atunci când ignori capabilitățile principale ale software-ului în favoarea lăcomiei și a politicii. MySpace a întâmpinat provocări tehnice semnificative și a acumulat o cantitate mare de cod prost întreținut și ineficient de-a lungul timpului din cauza schimbărilor rapide "necesare", motivate de conceptul de a câștiga cât mulți bani într-un timp cât mai scurt. Toate acestea în detrimentul creării unui mediu stabil pentru aplicația lor. Acest lucru a dus la probleme de performanță, vulnerabilități de securitate și o experiență generală proastă a utilizatorului. Ca rezultat, MySpace și-a pierdut utilizatorii în favoarea platformelor mai robuste și mai prietenoase pentru utilizatori, cum ar fi Facebook, pierzându-se în cele din urmă în anonimat.

Schimbările implementate au indicat o neglijare semnificativă a consecințelor pe termen lung asupra utilizatorilor, developerilor și arhitecturii generale a sistemului. Mai mult, ele au demonstrat o abordare greșită care s-a abătut de la caracteristicile fundamentale și influente care au definit MySpace în trecut. În loc să adopte o abordare de schimbare incrementală pentru a obține rezultate durabile, au preferat schimbările rapide în scopul obținerii de rezultate imediate. Această abordare a neglijat caracteristicile arhitecturale cruciale, prioritizând implementarea de funcționalități întregi proaspăt solicitate și trecând cu vederea dimensiuni vitale, cum ar fi securitatea, performanța și durabilitatea. Combinația rezultată a acestor factori a creat un scenariu nefavorabil care în cele din urmă a dus la un rezultat dezastruos.

Având acest anti-pattern în minte, ajungem la următoarea definiție a arhitecturii evolutive: "Arhitectura evolutivă este definită ca fiind o schimbare ghidată și incrementală pe mai multe planuri sau dimensiuni".

Schimbare ghidată și fitness functions

Schimbarea ghidată presupune o orientare intenționată a modificărilor, îmbunătățirilor și alterărilor din aplicația noastră către un obiectiv specific.

Inițial, trebuie să răspundem la întrebarea: cum determinăm care aspecte necesită schimbarea de fapt? Ar fi nu doar nepractic, ci aproape imposibil să actualizăm întreaga noastră aplicație ori de câte ori apare o nouă funcționalitate sau tehnologie în ecosistemul software. Trebuie să ne amintim că evoluția nu înseamnă să fii la "modă" în lumea software-ului, ci înseamnă a dezvolta produse de lungă durată. Aici intervine conceptul de schimbare ghidată, împrumutat și el din domeniul Evolutionary Computing: fitness functions.

Un fitness function servește drept mecanism de evaluare a performanței sau "sănătății" aplicației noastre. Cu toate acestea, ceea ce o diferențiază de abordările de monitorizare convenționale sunt doi factori cruciali care subliniază importanța sa.

Primul factor implică protejarea caracteristicilor arhitecturale pe care este construită fundația software-ului nostru. Acestea, la rândul lor, vor genera decizii și restricții arhitecturale. Atunci când creăm fitness functions, este vital să identificăm și să prioritizăm cele mai importante capacități sau caracteristici, precum disponibilitatea, scalabilitatea sau reactivitatea și multe altele. Stabilirea caracteristicilor care necesită protecție și atenție constantă devine primul pas în procesul de schimbare ghidată.

Al doilea factor constă în stabilirea unor referințe sau criterii măsurabile pentru a determina când o caracteristică protejată nu funcționează optim. Nu este suficient să monitorizăm doar aceste capacități, trebuie să definim și standarde la care fitness functionul să se raporteze.

Îndeplinind aceste condiții, schimbarea ghidată poate fi abordată. Devine ghidată deoarece avem o înțelegere clară a aspectelor care necesită corecție, unde să ne concentrăm atenția și ce domenii să îmbunătățim. Un mod simplu de a înțelege acest lucru este să ne întrebăm ce ne ghidează și spre ce suntem ghidați. Metodele utilizate nu trebuie neapărat să fie cele mai recente din industrie; obiectivul principal este să evoluăm în mod constant software-ul în ariile țintă, asigurând livrarea capacităților promise în mijlocul schimbărilor dinamice din ecosistemul software.

În cele din urmă, un fitness function reprezintă un mecanism de monitorizare orientat către scop, cu standarde și criterii stabilite. O abordare responsabilă ar fi de a integra "evoluția" ca o caracteristică de bază alături de celelalte caracteristici arhitecturale. Astfel, software-ul va fi întotdeauna proiectat și menținut la cel mai înalt nivel de performanță având în vedere capacitatea sa evolutivă.

Schimbare incrementală și dimensiunile multiple ale arhitecturii

În locul schimbărilor sau release-urilor la scară mare care reinventează toată aplicația peste noapte, schimbarea incrementală implică efectuarea de modificări mai mici, cu un scop precis (și ghidat!), pentru a limita impactul potențial sau "raza de acțiune". Această abordare ne asigură că schimbările care intră în cod sunt mai controlate și mai bine definite.

O tehnică adecvată pentru a realiza acest comportament implică atât dezvoltarea incrementală, cât și implementarea incrementală. În timpul dezvoltării, scopul schimbărilor realizate de echipa development este limitat pentru fiecare iterație, permițând îmbunătățiri sau rafinări continue (pentru că o aplicație poate fi definită rar spre niciodată ca fiind "completă") prin intermediul unor versiuni succesive mai mici.

Aici intervine conceptul de continuous delivery - livrare continuă, în special în cadrul arhitecturii evolutive. Prin încorporarea fitness functionurilor în fluxul de implementare, continuous delivery oferă un control sporit asupra caracteristicilor arhitecturale care trebuie protejate. Acest lucru face mai dificilă infiltrarea bit rotului în software. Prin protejarea capacităților cheie odată cu lansarea fiecărei versiuni, obținem control asupra "procesului de îmbătrânire" al software-ului, permițându-ne să abordăm problemele și să corectăm inconveniențele prompt, înainte de a deveni prea târziu sau prea costisitor pentru a face asta. De asemenea, facilitează o mai bună înțelegere a designului, analizei, programării și testării prin concentrarea asupra unui număr limitat de funcționalități livrate mai frecvent.

Pentru a realiza acest scenariu, sunt necesare cerințe clare pentru fiecare implementare și limite bine definite în cadrul arhitecturii. În cele din urmă, unul dintre cei mai mari inamici ai arhitecturii evolutive este cuplarea de slabă calitate. Cu cât dependințele din aplicația noastră sunt mai încurcate și mai necontrolate, cu atât crește probabilitatea creării unei aplicații instabile, determinând astfel un potențial efect în lanț care se poate declanșa la fiecare mică schimbare.

Este important de remarcat faptul că arhitectura cuprinde multiple dimensiuni. În timp ce adesea punem accent pe dimensiunea tehnică sau modul în care diferitele componente se îmbină, crearea unui sistem care poate lua cu adevărat viață și poate evolua necesită luarea în considerare a tuturor dimensiunilor. Fiecare dimensiune ar trebui să fie aliniată cu caracteristicile sale respective pentru a obține o arhitectură coerentă și adaptabilă.

Având acestea perspectivă, este esențial să înțelegem importanța fiecărei dimensiuni arhitecturale și ce probabilități de schimbare implică fiecare. De exemplu, dimensiunea securității necesită o vigilență continuă și posibile modificări pentru a aborda amenințările în evoluție sau legislația în vigoare.

Odată stabilite cerințele pentru fiecare dimensiune, următorul pas este crearea fitness functionurilor care să monitorizeze și evalueze eficient caracteristicile cheie pe care trebuie să le prioritizăm. Adoptând o abordare incrementală, aceste caracteristici pot fi evoluate constant în armonie cu ecosistemul software în perpetuă evoluție.

Prietenii și inamicii evoluției

Pentru a crea un traseu favorabil evoluției, putem beneficia de identificarea "prietenilor" și a "inamicilor" arhitecturii evolutive. Prezentăm mai jos o selecție de facilitatori și obstacole comune (dar nu limitate) în domeniul arhitecturii evolutive.

Prieteni

Clean Code: Scrierea unui cod lizibil, ușor de întreținut și bine structurat.

Fitness functions: Evaluarea capacităților sistemului în funcție de criterii și metrici predefinite.

Integrare și livrare continuă: Automatizarea procesului de integrare și implementare a schimbărilor pentru a asigura un ciclu de lansare lin și frecvent.

Cuplaj lejer: Minimizarea dependințelor între componente pentru a îmbunătăți flexibilitatea și a facilita evoluția independentă.

Schimbare incrementală: Adoptarea de modificări mici și iterative în locul transformărilor majore.

Inamici

Cuplaj strâns: Dependințe puternice între componente, ceea ce face dificilă modificarea unei componente fără a afecta celelalte.

Lipsa documentației: Documentație insuficientă sau învechită care îngreunează înțelegerea și modificarea aplicației.

"Datorie tehnică" și cod de calitate slabă: Acumularea de scurtături și cod suboptimal care crește complexitatea și dificultățile în implementarea schimbărilor viitoare.

Rezistență la schimbare: Reticență sau rezistență din partea părților interesate de a accepta și susține evoluția arhitecturală, ducând la stagnare și pierdere de oportunități.

Practici slabe de testare: Strategii de testare inadecvate care conduc la un cod fragil și fac introducerea de schimbări riscantă.

Toolkit de susținere a evoluției cu Java

Întotdeauna apare cel puțin o întrebare: unde se află responsabilitatea sau guvernarea arhitecturii? Este această responsabilitate ceva rezervat doar pentru un arhitect izolat sau se extinde ea și către developeri? Răspunsul corect ia în considerare ambele variante. În timp ce limitele clare sunt necesare pentru evoluție, responsabilitatea și obiectivul comun de a obține cele mai bune rezultate în cadrul aplicației se află atât în sarcina dezvoltatorilor, cât și a arhitecților. Această abordare colaborativă ne permite să identificăm fitness functionuri potrivite și să determinăm cele mai eficiente instrumente de utilizat.

Fitness functionurile pot acoperi un întreg spectru ca scop, de la nivelul de cod la nivelul enterprise. Cu toate acestea, în limita acestui articol, ne vom concentra în principal asupra domeniilor în care developerii pot aduce contribuții semnificative: nivelurile de cod și aplicație.

Acestea sunt doar câteva exemple de instrumente ce pot fi utilizate la acest nivel:

  1. JDepend: Un plugin Eclipse pentru analiza relațiilor de cuplaj în codul Java.

  2. SonarQube: Un instrument popular pentru inspectarea și raportarea continuă a calității codului.

  3. OpenApi.tools: O colecție de instrumente open-source pentru lucrul cu specificații OpenAPI, utile pentru proiectele Java în ceea ce privește proiectare a API-urilor[^7].

  4. Pact: O implementare a contractelor bazate pe consumator care facilitează testarea integrată între servicii.[^8]

  5. ArchUnit: Utilizează matcher-e Hamcrest pentru a defini accesibilitatea la pachete și pentru a identifica probleme de dependință/ cuplaj, dependințe ciclice, probleme de moștenire și respectarea regulilor de design arhitectural[^9].

Acesta din urmă oferă o colecție amplă de posibilități pentru a testa calitatea mai multor caracteristici. De exemplu, ne permite să testăm guvernarea dependințelor de pachet sub forma unui simplu test JUnit.

@Test
public void testPackageDependencyGovernance() {
   JavaClasses importedClasses = 
        new ClassFileImporter()
        .importPackages("one", "two");

        slices()
         .matching("..one.(*)..").should()
         .notDependOnClassesThat()
         .resideInAPackage("..two.(*)..")
         .check(importedClasses);
}

Metoda slices() din ArchUnit este utilizată pentru a defini o bucată (slice) pentru pachetul "one" și pentru a verifica dacă acesta nu depinde de clase din pachetul "two". Metoda check(importedClasses) este utilizată pentru a efectua testul de guvernare a dependințelor de pachete asupra claselor importate.

ArchUnit oferă o gamă largă de opțiuni pentru a testa diferite aspecte ale codului nostru, inclusiv, dar fără a se limita la:

Convenții de denumire:

ArchRule classNamingRule = classes()
    .should().haveSimpleNameEndingWith("Controller")
    .andShould().resideInAPackage("..controllers..");

classNamingRule.check(importedClasses);

Utilizare de adnotări:

ArchRule annotationUsageRule = fields()
    .should().beAnnotatedWith(Inject.class)
    .orShould().beAnnotatedWith(Autowired.class);

Arhitectură stratificată (layered):

ArchRule layeredArchitectureRule = layeredArchitecture()
    .layer("Controller").definedBy("..controllers..")
    .layer("Service").definedBy("..services..")
    .layer("Repository").definedBy("..repositories..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");

Concluzie

În concluzie, Evolutionary Architecture reprezintă o schimbare de paradigmă în dezvoltarea software. Prin adoptarea schimbării ghidate și îmbunătățiri incrementale, sistemele software pot evolua în timp ce își protejează funcționalitățile și capacitățile esențiale. Fitness functions servesc ca instrumente puternice pentru monitorizarea și evaluarea calității sistemului, asigurând menținerea și optimizarea caracteristicilor arhitecturale. Prin adoptarea unei abordări evolutive și utilizarea eficientă a instrumentelor, tehnicilor și comunicării, developerii și arhitecții pot construi sisteme software adaptabile, robuste și ușor de întreținut, care pot prospera în ecosistemul software în continuă schimbare.

Referințe

  1. Building Evolutionary Architectures.

  2. Code Quality Tool & Secure Analysis with SonarQube.

  3. Ford, Neal, et al. Building Evolutionary Architectures: Automated Software Governance. O'Reilly Media, Inc, 2023.

  4. Gafert, Peter. Unit Test Your Java Architecture.

  5. Hate, APIs You Won't. - OpenAPI.Tools.

  6. Introduction: Pact Docs.

  7. JDepend. Eclipse Plugins, Bundles and Products - Eclipse Marketplace,

  8. Martin, Robert C. Clean Architecture a Craftsman's Guide to Software Structure and Design. Prentice Hall, 2018.

  9. Myspace - What Went Wrong: 'The Site Was a Massive Spaghetti-Ball Mess.