Durant dècades es va assumir que l'arquitectura era una cosa que es decidia al principi i que després costava moltíssim de canviar: "l'important és encertar des del primer dia". La realitat és que cap disseny no sobreviu intacte al contacte amb l'evolució dels requisits. L'arquitectura evolutiva, formulada per Neal Ford, Rebecca Parsons i Patrick Kua, proposa el contrari: en comptes d'intentar predir el futur, construïm sistemes que suporten el canvi guiat i incremental a través de múltiples dimensions. Com evitem que, en evolucionar, l'arquitectura es degradi sense que ningú se n'adoni? Amb fitness functions (funcions d'aptitud): mecanismes objectius i automatitzats que mesuren si una característica arquitectònica es manté dins dels límits acceptables. En aquesta lliçó veurem què és el canvi incremental guiat, els tipus de fitness functions (atòmiques, holístiques, activades, contínues), exemples reals de tests d'arquitectura amb ArchUnit i com automatitzar tot això en el pipeline.
Contingut
- Què és l'arquitectura evolutiva
- Canvi incremental guiat
- Què és una fitness function
- Tipus de fitness functions
- Tests d'arquitectura amb ArchUnit
- Altres fitness functions: rendiment, acoblament, seguretat
- Automatització en el pipeline
- Errors Comuns i Consells
- Exercicis
- Conclusió
- Què és l'arquitectura evolutiva
Una arquitectura evolutiva és aquella que suporta el canvi guiat i incremental com un principi de primer nivell, al llarg de múltiples dimensions. Desglossem aquesta definició:
- Canvi guiat: no qualsevol canvi, sinó canvi en la direcció correcta. Necessitem alguna cosa que ens digui si anem pel bon camí: aquí entren les fitness functions.
- Incremental: l'arquitectura canvia en passos petits i reversibles, igual que vam veure amb el patró Strangler Fig, no en salts enormes.
- Múltiples dimensions: no només el codi. També rendiment, seguretat, escalabilitat, accessibilitat... Cada característica important és una dimensió a vigilar.
La premissa de fons és humil: no podem predir el futur, així que en comptes de sobredissenyar per a requisits imaginaris, dissenyem per poder canviar i muntem guardavies que ens avisin si l'arquitectura es desvia.
- Canvi incremental guiat
"Incremental" i "guiat" són dues propietats separades que es necessiten mútuament:
- Sense incrementalitat, cada canvi és gran i arriscat: necessites desplegaments sense interrupció, automatització de proves i pipelines fiables per moure el sistema en passos petits.
- Sense guia, el canvi incremental pot portar-te a poc a poc però segur... cap al desastre. La guia la donen les fitness functions: comproven que cada increment manté les característiques arquitectòniques desitjades.
Una analogia útil: les fitness functions són a l'arquitectura el que els tests unitaris al codi. Els tests no garanteixen que el codi sigui bo, però impedeixen que es trenqui el que ja funcionava. Les fitness functions no garanteixen una arquitectura perfecta, però impedeixen que es degradi en silenci mentre evoluciona.
- Què és una fitness function
El terme ve dels algorismes genètics, on una fitness function mesura com de prop està una solució de l'objectiu. En arquitectura, una fitness function és qualsevol mecanisme que proporciona una avaluació objectiva que una o diverses característiques arquitectòniques es mantenen dins de límits acceptables. La paraula clau és objectiva: ha de donar un resultat mesurable, no una opinió.
Exemples de característiques i la seva fitness function:
| Característica arquitectònica | Fitness function (exemple) |
|---|---|
| Modularitat | Test que prohibeix dependències cícliques entre paquets |
| Rendiment | El percentil 95 de latència es manté < 200 ms |
| Acoblament | L'acoblament aferent d'un mòdul no supera N |
| Seguretat | Cap dependència amb CVE crítica coneguda |
| Mantenibilitat | La capa de domini no importa frameworks d'infraestructura |
L'important és que cadascuna es pot mesurar i automatitzar. Si no pots escriure una comprovació que retorni passa/falla (o un número comparable amb un llindar), no és una fitness function, és un desig.
- Tipus de fitness functions
Les fitness functions es classifiquen en diversos eixos. Els més útils:
| Eix | Tipus | Significat |
|---|---|---|
| Abast | Atòmica vs Holística | Atòmica: mesura una sola característica de forma aïllada. Holística: mesura la interacció de diverses alhora (p. ex. seguretat sota càrrega). |
| Execució | Activada (triggered) vs Contínua | Activada: corre per un esdeveniment (un commit, un desplegament). Contínua: monitoritza en producció en temps real. |
| Mètrica | Estàtica vs Dinàmica | Estàtica: resultat fix (passa/falla, com un test). Dinàmica: depèn del context (un llindar que varia amb la càrrega). |
| Automatització | Automatitzada vs Manual | La immensa majoria han de ser automatitzades; les manuals (revisions) es reserven per al que no es pot mesurar. |
Les més comunes i barates d'implementar són les atòmiques, activades i automatitzades: tests que corren en cada build i comproven una regla concreta. Els tests d'arquitectura amb ArchUnit cauen just aquí, i són el millor punt de partida.
- Tests d'arquitectura amb ArchUnit
ArchUnit és una llibreria Java que permet escriure regles d'arquitectura com a tests normals (JUnit). Analitza el bytecode de les teves classes i comprova afirmacions sobre paquets, dependències, noms, capes, etc. És la forma més directa de convertir un ADR en una fitness function executable.
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.domain.JavaClasses;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
class ReglasArquitecturaTest {
// Importem totes les classes del projecte una sola vegada
private final JavaClasses clases =
new ClassFileImporter().importPackages("com.fiatc.tienda");
@Test
void el_dominio_no_depende_de_la_infraestructura() {
noClasses()
.that().resideInAPackage("..dominio..")
.should().dependOnClassesThat().resideInAPackage("..infraestructura..")
.check(clases);
}
}Analitzem aquest test, que materialitza una regla clau de l'arquitectura hexagonal:
importPackages("com.fiatc.tienda")carrega el bytecode de totes les classes sota aquest paquet. ArchUnit no executa el teu codi; l'inspecciona estàticament.noClasses().that().resideInAPackage("..dominio..")selecciona les classes del domini. El..és un comodí d'ArchUnit: "qualsevol paquet que continguidominioa qualsevol nivell"..should().dependOnClassesThat().resideInAPackage("..infraestructura..")defineix el que està prohibit: dependre de la infraestructura..check(clases)executa la regla i fa fallar el test si alguna classe del domini importa alguna cosa d'infraestructura.
Si demà algú afegeix en una classe de domini un import de Spring o d'un repositori JPA, aquest test es posarà vermell en el pipeline. La regla arquitectònica deixa de dependre de la disciplina humana.
@Test
void los_servicios_de_aplicacion_se_llaman_correctamente() {
classes()
.that().resideInAPackage("..aplicacion..")
.and().areAnnotatedWith(Service.class)
.should().haveSimpleNameEndingWith("ServicioAplicacion")
.check(clases);
}
@Test
void no_debe_haber_dependencias_ciclicas_entre_modulos() {
slices()
.matching("com.fiatc.tienda.(*)..")
.should().beFreeOfCycles()
.check(clases);
}Aquests dos tests afegeixen més guardavies:
- El primer imposa una convenció de noms: tota classe
@Servicea la capa d'aplicació ha d'acabar enServicioAplicacion. Sembla menor, però la consistència de noms és una característica de mantenibilitat real. - El segon és dels més valuosos:
slices()divideix el sistema en "rodanxes" pel primer subpaquet (Comandes, Catàleg, Pagaments...) ibeFreeOfCycles()comprova que no hi hagi dependències cícliques entre elles. Els cicles són el principi de la "bola de fang"; detectar-los automàticament protegeix la modularitat.
ArchUnit fins i tot ofereix una API d'alt nivell per a capes:
@Test
void se_respetan_las_capas() {
layeredArchitecture().consideringAllDependencies()
.layer("Presentacion").definedBy("..presentacion..")
.layer("Aplicacion").definedBy("..aplicacion..")
.layer("Dominio").definedBy("..dominio..")
// El domini no pot ser accedit per ningú de fora tret d'Aplicació
.whereLayer("Dominio").mayOnlyBeAccessedByLayers("Aplicacion")
.whereLayer("Presentacion").mayNotBeAccessedByAnyLayer()
.check(clases);
}Aquí declarem les capes i les seves regles d'accés: la presentació no pot ser accedida per ningú (està al capdamunt) i el domini només és accessible des d'aplicació. ArchUnit verifica que les dependències reals del codi respectin aquesta jerarquia. És un ADR sobre capes convertit en una prova que vetlla per si mateixa.
- Altres fitness functions: rendiment, acoblament, seguretat
No tot es mesura amb ArchUnit. Altres dimensions necessiten altres eines:
- Rendiment (holística, contínua): un test de càrrega (Gatling, k6) que falla si el p95 de latència supera un llindar. Pot córrer en CI (activada) o monitoritzar-se en producció (contínua).
- Acoblament (atòmica, activada): mètriques d'acoblament aferent/eferent amb eines com JDepend o el mateix ArchUnit, comparades contra un màxim.
- Seguretat (atòmica, activada): un escàner de dependències (OWASP Dependency-Check, Trivy) que fa fallar el build si apareix una CVE crítica.
- Cobertura/mida (atòmica): regles que impedeixen que un mòdul creixi per sobre de cert nombre de classes sense revisió.
# Fitness function de seguretat al pipeline: escaneig de dependències
escaneo-dependencias:
stage: verificacion
script:
- trivy fs --severity CRITICAL --exit-code 1 .
# exit-code 1 fa que el job (i el build) falli si hi ha CVE crítiquesAquest job converteix la característica "el sistema no fa servir dependències amb vulnerabilitats crítiques conegudes" en una fitness function automatitzada: trivy escaneja les dependències i, amb --exit-code 1, fa fallar el build si troba una CVE crítica. La seguretat deixa de ser una auditoria puntual i passa a verificar-se en cada canvi.
- Automatització en el pipeline
Una fitness function que s'ha d'executar a mà s'acabarà oblidant. El valor real apareix quan viuen en el pipeline de CI/CD i fan fallar el build automàticament.
graph LR
Commit[Commit / PR] --> Build[Compilar]
Build --> Unit[Tests unitaris]
Unit --> Arch[Fitness: ArchUnit]
Arch --> Sec[Fitness: seguretat]
Sec --> Perf[Fitness: rendiment]
Perf -->|tot verd| Deploy[Desplegar]
Arch -->|vermell| Stop[Build falla]El diagrama mostra les fitness functions com a etapes del pipeline, al mateix nivell que els tests unitaris. Si la regla d'arquitectura (ArchUnit) falla, el build s'atura i el canvi no arriba a producció. Recomanacions perquè això funcioni:
- Ràpides primer. Posa les fitness atòmiques i barates (ArchUnit, linters) al principi; les cares (càrrega) al final o en paral·lel.
- Missatges clars. Quan una regla falli, l'error ha d'explicar quina regla i per què, no només "test vermell".
- Versionades amb el codi. Les regles viuen al repo i evolucionen en pull requests, igual que els ADR.
- Poques i significatives al principi. Comença amb 3-4 regles que de veritat importin; afegeix-ne més a mesura que faci mal.
- Errors Comuns i Consells
- Escriure fitness functions subjectives. "El codi ha de ser llegible" no és una fitness function. Si no retorna passa/falla o un número, no serveix.
- Massa regles de cop. Un projecte que afegeix 50 regles d'ArchUnit el primer dia acaba amb builds vermells per tot arreu i la gent desactivant-les. Comença amb poques.
- Regles que no fan fallar el build. Una fitness function que només genera un informe que ningú no llegeix és decorativa. Ha de poder aturar el pipeline.
- Confondre fitness functions amb tests unitaris. Els unitaris proven comportament; les fitness functions proven característiques arquitectòniques (estructura, rendiment, seguretat).
- No mantenir-les. Quan una decisió arquitectònica canvia (nou ADR), les regles associades s'han d'actualitzar. Una regla obsoleta que falla sense raó erosiona la confiança.
- Consell: aparella cada ADR important amb una fitness function que el verifiqui. Així la decisió documentada i la decisió real no se separen mai.
- Exercicis
Exercici 1. Classifica aquestes fitness functions segons abast (atòmica/holística) i execució (activada/contínua): (a) un test d'ArchUnit en CI que prohibeix cicles; (b) un monitor en producció que alerta si el p95 supera 300 ms; (c) un test de càrrega que mesura latència mentre corre un escaneig de seguretat.
Exercici 2. Escriu (en pseudocodi o amb l'API d'ArchUnit) una regla que prohibeixi que qualsevol classe de la capa de presentació (..presentacion..) accedeixi directament a la capa de persistència (..persistencia..), saltant-se la capa d'aplicació.
Exercici 3. La teva organització ha acceptat un ADR que diu "cap dependència amb CVE crítica en producció". Com el convertiries en una fitness function automatitzada i en quina etapa del pipeline la posaries?
Solucions
Solució 1. (a) Atòmica i activada (mesura una característica —modularitat— i corre per un commit). (b) Atòmica i contínua (mesura rendiment de forma permanent en producció). (c) Holística i activada (mesura la interacció de rendiment i seguretat alhora, disparada pel pipeline).
Solució 2.
noClasses()
.that().resideInAPackage("..presentacion..")
.should().dependOnClassesThat().resideInAPackage("..persistencia..")
.check(clases);La presentació ha de passar sempre per aplicació; aquesta regla fa fallar el build si una classe de presentació importa directament alguna cosa de persistència.
Solució 3. Amb un escàner de dependències (Trivy, OWASP Dependency-Check) executat en el pipeline amb un llindar de severitat crítica i --exit-code 1, de manera que el build falli si apareix una CVE crítica. Va en una etapa de verificació, abans del desplegament, per impedir que l'artefacte vulnerable arribi a producció.
- Conclusió
L'arquitectura no és una foto fixa que es decideix a l'inici, sinó una cosa viva que evoluciona. L'arquitectura evolutiva abraça aquesta realitat recolzant-se en el canvi incremental guiat, i les fitness functions són la brúixola que manté la direcció: comprovacions objectives i automatitzades que impedeixen que les característiques arquitectòniques es degradin en silenci. Hem vist com classificar-les (atòmiques/holístiques, activades/contínues), com escriure tests d'arquitectura reals amb ArchUnit per protegir capes, noms i cicles, com cobrir altres dimensions com rendiment i seguretat, i com integrar-ho tot en el pipeline perquè l'arquitectura es defensi sola. Amb això tanquem el bloc conceptual del mòdul. En la darrera lliçó del curs posarem en pràctica tot el que hem après —estils, dades, desplegament, governança i evolució— en un estudi de cas d'extrem a extrem: el disseny complet d'una plataforma de comerç electrònic.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
