Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arquitectura

Aquesta pàgina són els conceptes: què és una frontera, quins estils i patrons hi ha, com es documenta una arquitectura i què la manté viva dins d’un equip. Com es comuniquen els dos costats d’una frontera de servei és a Fronteres de servei, i l’arquitectura que ja et ve donada quan treballes amb un framework, a Arquitectura dels frameworks.

Arquitectura i fronteres

Definició de frontera

L’arquitectura d’un sistema defineix com es divideix en components, quines són les fronteres o límits (boundaries) d’aquests components i com es comuniquen a través d’aquestes. Quan no hi ha fronteres parlem del monòlit.

El propòsit d’aquestes fronteres és facilitar el seu desenvolupament, gestió, manteniment i evolució. Una bona definició de les fronteres redueix l’acoblament, facilitant la flexibilitat de modificar unes parts sense afectar unes altres, i evita la degradació gradual de l’arquitectura. Les fronteres, però, no eviten soles aquesta degradació: mantenir-les mentre el sistema canvia és el que s’anomena arquitectura evolutiva, i ho veiem a Arquitectura en equip.

  • Facilita el desenvolupament, perquè permet desenvolupar les parts de forma independent, a ritmes diferents.
  • Facilita les proves, podem crear objectes que simulen comportament (test doubles), com dummies, stubs, mocks o fakes.
  • Facilita els canvis i l’evolució, ja que podem canviar la implementació d’un comportament sense afectar la resta.

En el fons, definir fronteres és aplicar la separació de responsabilitats (separation of concerns, SoC): agrupar allò que canvia junt i separar allò que canvia per motius diferents. Si ho hem fet bé es reflecteix en dues propietats complementàries:

  • L’acoblament és el grau de dependència entre components. El volem baix, perquè així podem modificar-ne un sense arrossegar els altres.
  • La cohesió és el grau en què els elements d’un mateix component pertanyen realment junts. La volem alta, perquè un component centrat en una sola responsabilitat és més fàcil d’entendre, provar i reutilitzar.

Aquests dos objectius, baix acoblament i alta cohesió, són el criteri de fons per decidir on posar una frontera.

Les dependències entre components haurien de ser un graf acíclic dirigit. Això evita les dependències circulars, que augmenten l’acoblament dels components i limiten la possibilitat de reutilitzar-los de forma individual.

Tipologia de fronteres

Una frontera es classifica per dues preguntes independents: per on tallem el sistema i amb quina tecnologia fem el tall.

Tipus de separació

Un sistema es pot tallar per àrees tècniques o per àrees funcionals, i la tria condiciona com de cars sortiran els canvis.

  • L’horitzontal crea les fronteres entre àrees tècniques del sistema. Per exemple, una API, la lògica de negoci i la comunicació amb la base de dades. Els canvis impliquen habitualment diferents capes del sistema. Pot ser un problema si les capes les gestionen diferents equips de desenvolupament.
  • La vertical crea la frontera entre àrees funcionals del sistema. S’utilitza amb microserveis. Per exemple, la gestió d’usuaris o la creació de comandes. Els canvis en aquest tipus de separació són més àgils.

Mecanismes de separació

Els quatre mecanismes per implementar una frontera es diferencien per com de lluny queden les dues bandes, des de compartir el mateix procés fins a ser màquines diferents:

  • Codi font: utilitzant classes i interfícies per poder comunicar-se mitjançant mètodes sense veure la implementació. Si es fa bé (actuant amb bona fe), permet aïllar les parts per permetre múltiples equips treballant. És l’únic mecanisme dels monòlits.
  • Components vinculats dinàmicament: es fa una separació amb components desplegables, per exemple, arxius JAR. Es comuniquen amb crides a mètodes, i poden utilitzar el principi d’inversió de dependència per a establir les relacions entre ells.
  • Processos locals: tenim processos locals que estan a la mateixa màquina. Poden comunicar-se utilitzant memòria compartida o sòcols. Permeten utilitzar diferents entorns de desenvolupament i tecnologies, sempre que es comparteixi el protocol.
  • Serveis: permet que els serveis estiguin a diferents màquines i utilitzin la xarxa. No se sol compartir la base de dades (mala pràctica). S’estableixen protocols estàndards basats en la xarxa, com REST. Exemple dels microserveis.

Resumint, els mecanismes de separació es poden classificar segons el seu enfocament:

  • Basats en interfícies: Codi font i components vinculats dinàmicament.
  • Basats en protocols: Processos locals i serveis.

Estils d’arquitectura

L’arquitectura d’una solució pot ser monolítica o distribuïda. La distribuïda sol basar-se en client-servidor, tot i que també tenim la solució peer-to-peer.

A continuació revisarem alguns dels estils d’arquitectura.

  • Multicapa (multitier). Basada en la client-servidor, estableix una sèrie de capes independents que permeten desenvolupar cada capa de forma prou independent. Les capes habituals són:
    • Presentació
    • Aplicació
    • Dades
  • Microkernel. Es tracta d’una arquitectura amb una funcionalitat central que permet afegir funcionalitats addicionals en forma de plug-ins.
  • Orientada a esdeveniments (event-driven). Basada en la comunicació asíncrona d’esdeveniments. Associada al concepte de “event-broker”, un middleware que s’encarrega d’encaminar esdeveniments entre sistemes que implementen el patró productor-consumidor. Els missatges poden ser guardats a cues fins a ser processats.
  • Monòlit modular. Un únic desplegable, com el monòlit, però dividit internament en mòduls amb fronteres explícites que només es comuniquen a través d’interfícies públiques. Utilitza el mecanisme de separació per codi font que hem vist abans, i per això depèn de la disciplina de l’equip. És el punt de partida raonable per a la majoria de projectes: si més endavant cal escalar parts per separat, els mòduls ja marquen per on tallar.
  • Microserveis. És l’ús paradigmàtic de la frontera de servei que hem vist als mecanismes de separació: un ecosistema de serveis d’un propòsit senzill que es comuniquen mitjançant un gateway d’APIs. Cada servei utilitza les seves pròpies dades. Com es comuniquen aquests serveis ho veiem a Fronteres de servei.

Patrons d’arquitectura

L’arquitectura hexagonal és un model arquitectural que permet separar el core de negoci (o domini) de la infraestructura (UI, base de dades, APIs, frameworks, etc.). Ho fa proposant els adaptadors i els ports.

Els adaptadors fan la interacció de la nostra aplicació cap al món. Tenim dos tipus:

  • Els primaris: punts d’entrada de l’aplicació, operats principalment pels usuaris (UI, API Rest, CLIs).
  • Els secundaris: actors secundaris com les bases de dades o serveis de tercers.

Si volem seguir el principi d’inversió de dependència, les capes internes no poden dependre de les externes. O sigui, les dependències han de ser de fora a dins: dels adaptadors cap al core.

Els ports són fronteres abstractes a l’exterior, per exemple, utilitzant interfícies, i els adaptadors són implementacions concretes dels ports, habitualment injectades:

  • Els adaptadors primaris depenen de ports d’entrada, i dirigeixen l’aplicació (API, GUI, CLI).
  • Els adaptadors secundaris implementen ports de sortida, i són dirigits per l’aplicació (BBDD, API clients).

DDD (Domain-driven design) és una estratègia de disseny de software focalitzat en el modelatge del software, i que té l’objectiu de replicar una àrea temàtica o domini (domain) gràcies als coneixements dels experts d’aquesta àrea.

Dins del DDD hi trobem diversos tipus de models. En una botiga en línia, la comanda número 1024 continua sent la mateixa comanda encara que en canviïn les línies, mentre que dues adreces d’enviament amb els mateixos camps són intercanviables: la primera és una entitat, que té identitat, i la segona un objecte de valor (value object), immutable i sense identitat. També hi ha aggregates, que són altres models dirigits per una entitat arrel, i que són unitats de consistència, concurrència i distribució.

Del DDD en surten dos patrons més avançats que val la pena conèixer de nom. El CQRS (Command Query Responsibility Segregation) separa el model d’escriptura del de lectura, de manera que cadascun es pot optimitzar i escalar pel seu compte. L’event sourcing no guarda l’estat actual, sinó la seqüència d’esdeveniments que l’han produït, i el reconstrueix reproduint-los; enllaça amb l’arquitectura orientada a esdeveniments que hem vist i amb la persistència d’esdeveniments. Tots dos afegeixen força complexitat i només compensen en dominis que la justifiquin.

De vegades hem de comunicar el nostre domini amb altres externs, que provoquen una dependència externa. És una mala estratègia permetre que es filtri aquest model extern cap al nostre, i la forma d’evitar-ho és construir una capa anti-corrupció. La idea tècnica es pot implementar utilitzant els patrons Facade i Adapter.

L’arquitectura clean afegeix el concepte d’entitats i casos d’ús al core. Tant l’arquitectura hexagonal com la clean es recolzen en el DDD. Les capes que defineix, de més interna a més externa són:

  • Les entitats: objectes de negoci amb comportament
  • Els casos d’ús (use cases): mapejat de la funcionalitat de les user stories
  • Els adaptadors d’interfícies (controladors, vistes, presentadors)
  • Els frameworks i eines (BBDD, serveis de tercers)

La regla de dependència és la mateixa idea que acabem de veure a l’arquitectura hexagonal, formulada per a aquestes capes: cap capa interna ha de dependre d’una externa. Això es manifesta de la següent forma:

  • Sense dependències:
    • Les entitats no depenen de ningú.
  • Dependències cap a dins:
    • Els casos d’ús depenen de les entitats, la capa més interna.
    • Els adaptadors d’entrada depenen dels casos d’ús.
  • Dependències cap a fora, però abstractes (inversió de dependència):
    • Quan un cas d’ús necessita alguna cosa de l’exterior, per exemple la persistència, depèn d’un port de sortida que ell mateix defineix, i és l’adaptador qui l’implementa. Així la dependència continua apuntant cap a dins.

El diagrama mostra una arquitectura hexagonal amb conceptes DDD (clean). Les fletxes negres indiquen dependència d’una interfície i les blanques, implementació.

Cal no confondre la direcció de les dependències amb el flux de control. Per exemple, un flux de control d’una aplicació amb el patró MVP (model-vista-presentador) podria ser controlador => cas d'ús => entitats => cas d'ús => presentador.

Finalment, cal dir que el cablejat dels components de l’arquitectura es fa des de fora de l’aplicació, creant les dependències, connectant-les i iniciant l’aplicació. El patró es diu composition root. Aquí sol utilitzar-se la injecció de constructor i, si hi ha contenidors que fan inversió de control, és l’únic lloc on haurien d’aparèixer.

Tot el que hem vist en aquesta secció són arquitectures que es decideixen. A la pràctica, però, la major part del codi s’escriu dins d’un framework que ja en porta una de feta i que, entre altres coses, fa ell mateix de composition root. A Arquitectura dels frameworks veiem quina és aquesta arquitectura i com es relaciona amb els conceptes d’aquí.

Disseny per contracte

El disseny per contracte és la manera d’especificar una frontera concreta: fixa formalment què demana un component a qui el crida, els seus clients, i què els promet a canvi. Aquest contracte té dues parts:

  • Els requisits que demana el component als clients.
  • Les promeses fetes pel component als clients.

Resumint, si el client compleix els requisits, el component promet complir el contracte que defineix. Val més pensar-hi en termes de temps que d’entrades i sortides: els requisits són el que ha de ser cert abans de la crida (els arguments, però també l’estat de l’objecte), i les promeses, el que ho és després (el resultat, el canvi d’estat, els errors que pot llançar i els efectes que provoca).

Un mètode retirar d’un compte bancari ho ensenya de seguida:

/**
 * Retira una quantitat del compte.
 *
 * Requisits: la quantitat és positiva i no supera el saldo.
 * Promeses:  el saldo queda reduït exactament en aquesta quantitat i es retorna el saldo
 *            resultant. Si els requisits no es compleixen, llança IllegalArgumentException
 *            i el saldo no canvia.
 */
public BigDecimal retirar(BigDecimal quantitat) { ... }

Qui crida el mètode no ha de mirar com està implementat: li n’hi ha prou de garantir la seva part (quantitat positiva i suficient saldo) per poder confiar en l’altra.

Fixa’t que la meitat d’aquest contracte existeix perquè el mètode canvia coses. Un mètode sense efectes, que només calcula a partir dels arguments, gairebé no té contracte a respectar: no hi ha estat previ que exigir ni res a prometre més enllà del resultat. Com que els efectes són inevitables, el que es fa és concentrar-los, deixant el càlcul net i els canvis en una capa prima al damunt.

Si canviem un contracte, volem que els clients no quedin afectats. Per assegurar-nos utilitzem la frase “no requerir més ni prometre menys”: si el canvi no requereix més dels clients ni promet menys, la nova especificació és compatible i no trencarà el funcionament del client. Amb el mateix exemple:

  • Acceptar també la quantitat zero requereix menys, i deixar constància del moviment a l’historial promet més. Cap client no se n’adona, perquè tots complien els requisits antics i esperaven menys del que ara reben.
  • Exigir que el compte estigui verificat requereix més, i deixar de garantir que el saldo no canvia quan la quantitat és invàlida promet menys. Tots dos canvis trenquen codi que funcionava.

Aquesta regla no és exclusiva de les fronteres grans. El principi de substitució de Liskov n’és el mateix criteri aplicat a l’herència, i el versionat d’una API l’aplica entre dos desplegables.

Algunes pràctiques per a seguir el disseny per contracte:

  • Documentar el contracte amb comentaris, responent quins són els requisits i les promeses que es fan. En resum, explicant exactament què es fa sense haver de saber com. A Java es fa al javadoc de la classe i dels mètodes públics.

  • Validar arguments dels mètodes i constructors públics. És raonable no fer-ho amb els privats, ja que només es criden per la mateixa classe. D’això també se’n diu precondicions. A Java, aquesta validació sol produir excepcions unchecked com IllegalArgumentException, NullPointerException o IllegalStateException que no cal obligatòriament documentar.

  • Validar les promeses que fa un mètode al seu client. D’això també se’n diu postcondicions. Per exemple, comprovar valors, tipus de retorn, errors i excepcions que es produeixen. Es pot fer just abans d’acabar un mètode públic.

  • Validar l’estat de l’objecte. D’això també se’n diu invariants de classe. Implica mantenir una sèrie de condicions sobre l’estat entre les crides a mètodes públics. Si l’objecte és immutable, només cal validar l’estat al constructor. Si és mutable, a cada mètode que canvia l’estat.

  • Opcionalment, aspectes sobre el rendiment (temps i espai).

Estratègia de gestió d’errors

L’estratègia de gestió d’errors defineix què passa quan les promeses del contracte no es poden complir. És una decisió de disseny que afecta tota l’arquitectura i que convé prendre explícitament: si no es fa, cada part del sistema gestionarà els errors de manera diferent.

Fail-fast vs. defensiu

Davant d’un estat invàlid, un sistema pot fallar immediatament o intentar continuar:

  • Fail-fast: quan es detecta un estat invàlid, el sistema falla immediatament i de forma visible. Això facilita el debugging perquè l’error es manifesta a prop de la causa. Les precondicions del disseny per contracte segueixen aquest principi: si un argument és invàlid, es llança una excepció al moment.
  • Defensiu: el sistema intenta continuar operant malgrat condicions inesperades, amb valors per defecte, reintents o degradació de funcionalitat. Adequat per a sistemes que han de mantenir disponibilitat (p. ex. un frontend que mostra dades parcials en lloc de fallar completament).

La majoria de sistemes combinen els dos: fail-fast a la lògica interna (on un error és un bug) i defensiu a les fronteres (on les entrades són impredictibles).

Tipus d’errors

No tots els errors tenen el mateix tractament, i distingir-ne tres tipus ja resol la major part de les decisions:

  • Errors de programació (bugs): precondicions violades, índexs fora de rang, nulls inesperats. No s’han de gestionar, s’han de corregir. Fail-fast amb una excepció no recuperable.
  • Errors de domini: regles de negoci que no es compleixen (saldo insuficient, usuari no autoritzat). Formen part del contracte del component i s’han de comunicar explícitament al client, amb excepcions de domini, tipus de retorn que representin el fracàs, o codis d’error documentats.
  • Errors d’infraestructura: xarxa no disponible, disc ple, servei extern caigut. Són temporals i sovint recuperables amb reintents. S’han de gestionar a les fronteres del sistema (adaptadors), no al cor de la lògica.

On capturar els errors

Un error s’hauria de capturar al lloc on es pot prendre una decisió útil sobre què fer. Capturar-lo abans produeix codi que emmascarà problemes; capturar-lo després complica la depuració.

  • A les fronteres d’entrada: controladors, handlers d’API, punts d’entrada de CLI. Aquí es transformen errors interns en respostes adequades pel client (codis HTTP, missatges d’error).
  • A les fronteres de sortida: adaptadors de base de dades, clients HTTP, sistemes de fitxers. Aquí es decideix si reintentar, degradar o propagar.
  • No al mig: la lògica de negoci hauria de propagar errors, no capturar-los. Un try-catch genèric dins de la lògica de domini sol ser un senyal que falta una frontera ben definida.

Diagrames d’arquitectura

El model C4 dibuixa l’arquitectura d’una solució a quatre nivells de diagrames, de més general a més concret:

  1. De context de sistema: mostra el sistema software i el seu context al voltant.
  2. De contenidor: mostra els contenidors dins d’un sistema software i com es relacionen.
  3. De component: mostra els components d’un contenidor i les seves interaccions.
  4. De codi: mostra la implementació del codi amb diagrames UML, diagrames ER o similars.

Hi ha quatre abstraccions que poden aparèixer en el nostre diagrama:

  • La persona: actors o rols d’un sistema software.
  • El sistema software: el sistema que estàs modelant, i que es relaciona amb altres sistemes externs.
  • Un contenidor: una aplicació o un magatzem de dades. Per exemple:
    • Un backend web
    • Un frontend web
    • Una aplicació desktop (client)
    • Una app mòbil
    • Una aplicació de consola o script
    • Una funció serverless (cloud)
    • Una base de dades
    • Un magatzem al núvol
    • El sistema d’arxius
  • Un component: grup de funcionalitats encapsulat darrere d’una interfície (o contracte). Tots els components d’un contenidor solen executar-se en el mateix espai de processos o màquina virtual.

Els quatre nivells amb un exemple

Els diagrames següents dibuixen la mateixa botiga en línia als quatre nivells, cada vegada ampliant una part del nivell anterior. Estan escrits amb C4-PlantUML, una biblioteca que afegeix a PlantUML les abstraccions del model C4.

Nivell 1, context de sistema. Qui fa servir la botiga i amb quins sistemes externs parla. No diu res de com està construïda per dins.

Nivell 2, contenidors. Obre la caixa de la botiga: quines unitats desplegables la formen, amb quina tecnologia i com es comuniquen.

Nivell 3, components. Obre un únic contenidor, l’API de comandes. Aquí ja es veuen els adaptadors i els ports de l’arquitectura hexagonal.

Nivell 4, codi. Ja no és C4, sinó UML: les classes de dins d’un component. Només val la pena dibuixar-lo per a les parts que costen d’entendre llegint el codi.

Fixa’t que la comanda és una entitat (té identitat), la línia i l’adreça són objectes de valor, i el cas d’ús depèn del port ComandaRepository, no de la implementació JPA: la dependència apunta cap a dins.

Consells per dibuixar un diagrama

  • No hi ha una forma estàndard de fer-ho.
  • Han de reflectir la realitat. Abstraccions primer, notació després.
  • Conté blocs per als diferents components (SoC). Alguna cosa modular amb una interfície/fronteres. Els components contenen codi.
  • Agrupar els components que treballen junts amb caixes (contenidors): DB schema, app mòbil, backend server-side app, console app, windows service.
  • Ha d’incloure les dades, la lògica de negoci i la interfície d’usuari.
  • Fletxes sempre direccionals per indicar la direcció del flux (petició) amb descripció.
  • No cal afegir respostes (fletxes de tornada) dient OK, només qui les origina.
  • Ser consistent en forma i color, evitar acrònims no coneguts.

Arquitectura en equip

Una arquitectura només existeix mentre l’equip la manté viva: les fronteres que hem definit es dilueixen tan bon punt algú, amb pressa, hi fa una drecera. Fins aquí l’hem tractada com una decisió tècnica, i no ho és del tot. Ja ho hem vist amb el mecanisme de separació per codi font, que només funciona actuant amb bona fe. El repte, doncs, és sociotècnic, i la part difícil no és el codi.

Hi ha dues maneres típiques de fallar quan algú intenta posar ordre en l’arquitectura d’un equip, i totes dues apareixen també en projectes petits:

  • Decidir-ho tot al principi: es dissenya l’arquitectura els primers dies, es documenta i ningú no torna a obrir el document. Al cap d’un mes, el codi i el document ja no s’assemblen.
  • Revisar-ho tot: una persona revisa i reescriu el codi de la resta. O bé es converteix en un coll d’ampolla que frena l’equip, o bé la resta acaba treballant al marge d’ella.

El problema de fons és que no es pot encertar el disseny des del principi. Les fronteres bones s’aprenen construint, i per això la iteració ràpida sol guanyar al disseny previ detallat. L’objectiu, per tant, no és encertar l’arquitectura, sinó evitar que es degradi mentre canvia. És el que hem anomenat arquitectura evolutiva, i s’aconsegueix amb tres mecanismes: la cultura de l’equip, les decisions explícites i les comprovacions automàtiques.

Cultura: propietat i raonament

Dues pràctiques d’equip barates fan més per mantenir viva una arquitectura que qualsevol document:

  • Qui ho construeix, ho manté (you build it, you run it). Qui escriu un component és qui l’arregla quan falla i qui l’explica a la resta. L’alternativa, llançar el codi per damunt del mur perquè el problema passi a ser d’algú altre, garanteix que ningú no assumeixi les conseqüències de les seves pròpies decisions.
  • Explicar el perquè, no només el què. Quan revisem el codi d’algú, dedicar una mica més de temps a explicar el raonament d’un canvi fa que la propera vegada no calgui dir-ho. La revisió de codi és sobretot una eina de transmissió: el criteri es propaga i s’acaba aplicant sense que hi siguem al davant. Ajuda tenir un petit vocabulari compartit d’exemples i comparacions que l’equip repeteix.

Decisions explícites: els ADR

Un document de visió llarg envelleix malament i ningú no el llegeix. L’alternativa habitual són els ADR (Architecture Decision Records): un fitxer curt per decisió, guardat al repositori i versionat amb el codi, amb quatre apartats.

  • Context: quin problema teníem i amb quines restriccions.
  • Decisió: què hem decidit fer.
  • Estat: proposada, acceptada o substituïda per un altre ADR.
  • Conseqüències: què hi guanyem i què hi perdem, incloent-hi què ens costarà si algun dia ho volem canviar.

El valor no és el document en si, sinó deixar constància de per què les coses són com són. Quan algú es planteja saltar-se una frontera, l’ADR li diu si està trencant una decisió pensada o simplement una casualitat històrica. I quan una decisió deixa de tenir sentit, se substitueix per una de nova en lloc d’editar la vella: l’historial de decisions també és informació.

Comprovacions automàtiques: funcions d’aptitud

Les regles que depenen de la bona voluntat es trenquen tard o d’hora. Les que es comproven soles, no. A les comprovacions executables que verifiquen que el sistema manté les seves propietats arquitectòniques se’ls diu funcions d’aptitud (fitness functions), i són el mecanisme central de l’arquitectura evolutiva:

  • Proves: no verifiquen l’arquitectura directament, però la dificultat d’escriure-les sí que informa. Si per muntar una prova unitària necessites molts test doubles, el component té massa dependències; si necessites una base de dades per comprovar una regla de negoci, la lògica i la infraestructura estan barrejades.
  • Tipatge fort: detecta errors sistèmics que les proves no cobreixen, com el canvi de la signatura d’un mètode que afecta molts punts alhora.
  • Regles de dependència automatitzades: amb eines com ArchUnit (Java) es pot escriure com una prova més la regla “el domini no pot dependre de la infraestructura”. La regla de dependència de l’arquitectura clean deixa de ser una recomanació i passa a ser una prova que falla.
  • Format i estil automatitzats: els linters i els formatadors treuen de la revisió tota la discussió que no aporta res i deixen espai per parlar del que importa.
  • Observabilitat: registres, mètriques i traces. Sense veure què passa en execució, els casos límit només apareixen quan ja són un problema, i el cicle de correcció s’allarga.

Totes s’han d’executar a la integració contínua, no a la màquina de qui se’n recorda: una regla que no bloqueja la integració és només una recomanació. Comença a ser habitual complementar-les amb revisors automàtics basats en models de llenguatge, capaços de detectar regles més difuses (lògica de negoci que s’escola cap a la capa d’API, duplicacions), tot i que de moment no substitueixen les comprovacions deterministes.

Llegir els senyals de les proves

De tots aquests mecanismes, les proves són l’únic que qualsevol equip ja té, i val la pena saber-ne llegir els senyals. La taula relaciona la dificultat que trobes en escriure una prova amb el problema de disseny que sol indicar:

Símptoma en escriure la provaQuè sol indicar
Necessites molts test doubles només per instanciar la classeMassa dependències i poca cohesió: la classe fa més d’una cosa
Necessites una base de dades, la xarxa o el sistema de fitxers per comprovar una regla de negociEl domini i la infraestructura estan barrejats: falta un port
La preparació (arrange) és més llarga que la resta de la provaFalta una abstracció, o l’invariant hauria de viure en un altre objecte
Has d’ampliar la visibilitat d’un mètode o fer servir reflexió per comprovar resEstàs provant la implementació i no la promesa: el contracte públic no és el correcte
Canvis no relacionats trenquen la prova contínuamentLa frontera no està al lloc adequat i el contracte deixa escapar detalls d’implementació
No saps què comprovarEl contracte no està definit

L’últim cas és el més útil de tots, perquè enllaça amb el disseny per contracte: una classe amb un contracte ben definit és fàcil de provar, ja que la prova són les precondicions i les postcondicions. Si no saps què afirmar, el problema és anterior a la prova.

Compte, però, que no tota dificultat és un problema de disseny. La lògica de domini complexa, la concurrència i el codi que depèn del temps són difícils de provar per motius legítims, i les proves d’integració i d’extrem a extrem són cares de preparar per definició. El senyal només val per a les proves unitàries, que és on controlem la frontera.

La llei de Conway

La llei de Conway diu que l’estructura d’un sistema tendeix a reproduir l’estructura de comunicació de l’organització que el construeix. Ho hem insinuat en parlar de la separació horitzontal: si cada capa la manté una persona diferent, qualsevol funcionalitat nova necessita coordinar-les totes i acaba sent lenta. Si, en canvi, cada persona s’encarrega d’una àrea funcional completa, com en la separació vertical, els canvis es poden fer de punta a punta.

La conseqüència pràctica és que, quan es reparteix la feina d’un equip, s’està decidint també quina arquitectura acabarà sortint. Val la pena fer-ho a consciència: primer decidim les fronteres i després repartim la feina d’acord amb elles, no a l’inrevés.

A la pràctica

En un projecte d’equip petit, tot això es tradueix en quatre coses concretes:

  • Un directori docs/adr/ amb les decisions que us han costat una discussió.
  • Una prova de dependències que falli si algú importa la infraestructura des del domini.
  • Un linter i un formatador executats a la integració contínua.
  • Un responsable identificable per a cada component, i revisions de codi on s’expliqui el perquè.

En resum, la feina d’arquitectura d’un equip no és vigilar el codi, sinó cuidar el sistema que el produeix (garden the system, not the codebase): les persones, els acords i les comprovacions automàtiques. L’estructura dels components n’és el resultat.

Referències

Last change: , commit: 3af7ba4