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

Disseny

L’objectiu del disseny no és produir més codi, sinó que el programari es pugui entendre, verificar i canviar. Els principis i les tècniques d’aquest capítol són mitjans per aconseguir-ho, i tots responen al mateix criteri: controlar la complexitat.

La complexitat

La complexitat és tot allò de l’estructura d’un sistema que el fa difícil d’entendre i de canviar. No és qüestió de mida ni de sofisticació: un programa llarg pot ser senzill, i un de curt pot ser inabordable. La detectem per tres símptomes:

  1. És massa rígid? Es poden canviar els detalls interns d’aquest mòdul en el futur sense tocar el codi d’altres mòduls i altres capes? El codi rígid és el que té dependències que serpentegen en tantes direccions que no es pot fer un canvi aïllat sense canviar-ho tot al voltant.
  2. És massa fràgil? Seria difícil trobar llocs on fer canvis i refactoritzar en el futur? El codi fràgil es trenca de formes estranyes i que no es poden predir.
  3. Hauria de ser una característica reutilitzable? Si ho fos, el codi depèn de mòduls no desitjats que es podrien evitar? Vols una banana, però el que obtens és un goril·la agafant-la i tota la jungla amb ell.

Si mirem de prop, el fil conductor dels tres problemes esmentats és l’acoblament. Els mòduls depenen els uns dels altres de maneres no desitjades i resulten en un codi espagueti.

El codi hauria d’estar desacoblat entre els mòduls i les capes. Les polítiques d’alt nivell i les abstraccions no haurien de dependre de detalls de baix nivell, sinó d’abstraccions: caldria invertir la dependència dels mòduls als llocs necessaris. I escriure classes que només fan una cosa i només tenen un motiu per canviar.

Cap decisió aïllada fa complex un sistema. La complexitat s’acumula: cada canvi que afegeix una dependència, que duplica una regla o que eixampla una interfície en deixa una mica més. Per això el criteri no s’aplica una sola vegada, en una fase inicial de disseny, sinó a cada canvi.

El codi bo hauria d’explicar què està fent. Hauria de ser avorrit de llegir. Tot hauria de ser perfectament obvi. Això és bon codi - Robert Martin.

Els tres mecanismes

Cada principi clàssic de programació ataca la complexitat per una de tres vies: simplificar la peça, desacoblar-la de les altres o amagar-ne els detalls.

Simplificar

Simplificar vol dir reduir la quantitat de coses que cal tenir al cap per treballar amb una peça.

Aquesta peça té una responsabilitat clara i fàcil d’entendre?

  • DRY (Don’t repeat yourself): no et repeteixis.
  • Principi de l’abstracció: cada peça significant s’ha d’implementar en només un lloc del codi font.
  • KISS (Keep it simple): mantenir la senzillesa.
  • YAGNI (You aren’t gonna need it): no afegeixis el que encara no necessites.
  • Fes la feina més senzilla que sigui funcional.
  • Evitar l’optimització prematura: només si funciona i és lent.
  • Reutilitzar codi és bo: el fa més llegible.
  • Escriu el codi per qui l’haurà de mantenir.

Desacoblar

Desacoblar vol dir que cada peça sàpiga el mínim possible sobre les altres.

Si canvio aquesta peça, quantes altres n’hauré de tocar?

  • Minimitzar l’acoblament i maximitzar la cohesió.
  • La llei de Demeter: el codi només s’ha de comunicar amb les seves relacions directes.
  • Separació d’interessos: àrees de diferents funcionalitats han de tenir pocs solapaments.
  • Els usuaris d’una classe han de dependre de la seva interfície pública, però la classe no ha de dependre dels usuaris.

Amagar

Amagar vol dir que la complexitat es queda dins d’una peça i no es filtra cap als seus clients.

Qui utilitza aquesta peça, necessita saber com està feta per dins?

  • Amagar els detalls de la implementació.
  • El principi de la mínima sorpresa.
  • No em facis pensar.

Mòduls profunds

Un mòdul profund (deep module) ofereix molta funcionalitat darrere d’una interfície petita. Un de superficial en fa poca i, tot i així, obliga qui l’utilitza a conèixer-ne els detalls.

// Superficial: el client ha de conèixer connexions, transaccions i ordre de crides
public interface Magatzem {
    Connection obreConnexio();
    void iniciaTransaccio(Connection c);
    void insereix(Connection c, Comanda comanda);
    void confirmaTransaccio(Connection c);
    void tancaConnexio(Connection c);
}

// Profund: la mateixa feina, amb tota la complexitat continguda a dins
public interface Magatzem {
    void desa(Comanda comanda);
}

Les dues interfícies fan la mateixa feina. La primera reparteix la complexitat entre tots els clients; la segona la conté. Senyals que un mòdul és superficial:

  • Mètodes de pas (pass-through): mètodes que només deleguen en un altre sense afegir-hi res, i que obliguen a mantenir dues signatures per a una sola decisió.
  • Getters i setters que exposen l’estructura interna: la classe es converteix en un contenidor de dades i la lògica se’n va cap als clients.
  • Interfícies que calquen la implementació: si cada mètode públic correspon a un detall intern, la interfície no amaga res.

D’aquí surt la resposta a la pregunta habitual sobre la mida del codi. El que decideix no és el nombre de línies, sinó si puc entendre la peça i canviar-la amb confiança: una classe petita amb una interfície ampla és pitjor que una de més gran amb una interfície estreta.

Valors per defecte de mida i estil

Aquestes xifres funcionen com a defectes raonables, no com a lleis:

  1. Les classes han de ser petites, per sota de 500 línies, i han de tenir un nombre limitat de mètodes.
  2. Els mètodes han de ser petits, per sota de 30 línies, i han de fer una feina concreta.
  3. Has d’escriure el codi perquè s’expliqui a ell mateix, però on no arribis, utilitza comentaris.
  4. No facis línies massa llargues, com a molt de 120 caràcters.
  5. Mantén baix el nivell de sagnat del codi, i intenta no superar els 3-4 nivells.
  6. Anomena les classes, els mètodes i les variables amb els criteris que es descriuen més avall.
  7. Decideix el teu estil i segueix-lo de forma consistent.

SOLID

SOLID no són cinc lleis, sinó cinc heurístiques per aplicar els mecanismes anteriors a la programació orientada a objectes: redueixen l’acoblament, augmenten la cohesió i limiten l’abast dels canvis. Cadascuna ajuda a respondre una pregunta concreta.

PrincipiPregunta que ajuda a respondre
SRPAquesta peça té una sola responsabilitat?
OCPPuc afegir comportament sense provocar una cascada de canvis?
LSPAquesta implementació respecta el contracte de l’abstracció?
ISPEls clients depenen de coses que no necessiten?
DIPEstic acoblant la lògica a detalls concrets?
  • Responsabilitat única (SRP): un component de codi ha de fer una sola feina i ben definida. Només caldrà modificar-lo si necessitem canviar aquesta feina. Això millora la cohesió i redueix possibles errors. Code smell: quan vull canviar una funcionalitat, una altra no relacionada queda afectada, i cal refer-la.
  • Obert/tancat: les entitats software han d’estar obertes a ser esteses i tancades a ser modificades. Si cal estendre la funcionalitat és millor afegir codi que canviar l’existent. Això es pot fer amb abstracció, derivant classes i utilitzant polimorfisme, i amb encapsulació. Code smell: quan vull afegir funcionalitat, es produeix una cascada de canvis.
  • Substitució Liskov: qualsevol classe que hereta d’una altra (pare) pot ser utilitzada d’igual forma que la pare sense conèixer les diferències entre elles. Per tant, quan heretem no hem de canviar el comportament que defineix la classe pare. Dit amb la regla del disseny per contracte, una subclasse no pot requerir més ni prometre menys que la seva classe pare. Code smell: codi client que comprova el tipus concret d’un objecte (instanceof, type checks) per decidir com tractar-lo.
  • Segregació d’interfície: és millor tenir moltes interfícies de client específiques que una sola de propòsit general. Així, evitem que els clients depenguin de mètodes que no utilitzen. Code smell: classes que implementen mètodes d’una interfície amb cossos buits o llançant excepcions de “no suportat”.
  • Inversió de dependència: les dependències han de ser sobre abstraccions, no sobre concrecions. Es resumeix en dues qüestions:
    • Els mòduls d’alt nivell (més abstractes) no han d’importar res de mòduls de baix nivell (més concrets). Els dos han de dependre d’abstraccions (p. ex. interfícies).
    • Les abstraccions no han de dependre dels detalls. Són els detalls (implementacions concretes) les que han de dependre de les abstraccions.
    • Code smell: mòduls d’alt nivell que importen directament classes concretes de baix nivell (p. ex. un servei que instancia directament un client HTTP o una connexió a base de dades).

Un principi complementari a SOLID és preferir la composició a l’herència: en lloc de reutilitzar comportament heretant d’una classe pare, encapsular-lo en objectes que es combinen. L’herència crea un acoblament fort entre pare i fill que es propaga a tota la jerarquia; la composició permet canviar comportaments de forma independent.

Dependències a POO

Les dependències són la forma concreta que pren l’acoblament en el codi orientat a objectes. Si un servei de comandes necessita un Magatzem per desar-les, aquest servei depèn de Magatzem: qualsevol canvi en el magatzem el pot afectar. En general, si tenim dues classes A i B, i A necessita B per fer la seva feina, llavors A té una dependència de B. Per a resoldre aquesta dependència podem fer:

  1. Que A crei o obtingui un objecte B. La classe A té el control de la dependència.
  2. Que A rebi un objecte B. Algú altre li proporciona, sense que A s’hagi de preocupar.

A més, quan necessitem un objecte poden passar almenys dues coses:

  1. Que necessitem una nova instància cada cop. Per exemple, les factories creen múltiples objectes.
  2. Que necessitem una única instància compartida. Aquesta situació es relaciona amb el patró singleton.

La inversió de control (IoC) és un principi de POO que cedeix a un contenidor o framework la tasca de controlar la creació d’instàncies d’objectes.

Tenim principalment dues formes d’implementar IoC:

  1. L’injecció de dependència (DI), on un contenidor pren el control i fa crides al nostre codi per proporcionar les dependències d’un objecte. Hi ha principalment dos tipus: de construcció i de setter.
  2. El Service Locator, que introdueix un nou objecte al nostre codebase, el Locator, que permet resoldre dependències d’una certa classe.

Aquestes tècniques es poden implementar per a subministrar instàncies úniques o múltiples. Per a permetre instàncies úniques, com serveis, utilitzen un mapa o diccionari d’instàncies accessibles pel nom de la classe.

L’objectiu d’aquestes tècniques és seguir el principi d’inversió de dependència: fem les dependències sobre abstraccions (interfícies). Això permet separar l’ús de la construcció, i podem substituir les implementacions sense afectar el codi.

Disseny per a la testabilitat

Les decisions de disseny afecten directament la facilitat amb què el codi es pot verificar. La testabilitat és una propietat del disseny, no de les proves: quan escriure una prova costa, el problema gairebé sempre és del codi que es vol provar, no de l’eina que s’utilitza.

Per què el disseny condiciona la testabilitat

Un test unitari necessita tres coses: crear l’objecte, executar-lo amb unes entrades, i verificar el resultat. Quan alguna d’aquestes tres passa a ser difícil, el problema sol ser de disseny:

  • Dependències ocultes: si un objecte crea internament les seves dependències, el test no pot substituir-les. La solució és la injecció de dependència, el mateix principi que ja hem vist a la secció anterior, però ara amb una motivació concreta: poder injectar test doubles.

    // Dependència oculta: la prova no pot substituir el magatzem
    class ServeiComandes {
        void desa(Comanda c) {
            new DatabaseClient().insert(c);
        }
    }
    
    // Dependència explícita: la prova hi injecta un test double
    class ServeiComandes {
        private final Magatzem magatzem;
        ServeiComandes(Magatzem magatzem) { this.magatzem = magatzem; }
        void desa(Comanda c) { magatzem.desa(c); }
    }
    
  • Estat global i singletons: quan el comportament depèn d’estat compartit, els tests es contaminen entre ells. L’ordre d’execució passa a importar i els tests deixen de ser independents.

  • Efectes secundaris escampats: si una funció envia un correu, escriu a disc i actualitza una base de dades, testejar-la requereix simular tot això. Empènyer els efectes secundaris cap a les fronteres del sistema (ports, adaptadors) permet que el gruix de la lògica sigui pura i testejable sense infraestructura.

Principis pràctics

Quatre decisions de disseny determinen si el codi serà fàcil de provar.

  • Separar la lògica de la infraestructura. La lògica de negoci hauria de poder executar-se sense base de dades, xarxa ni sistema de fitxers. Les dependències d’infraestructura es connecten a les fronteres, no al cor.
  • Preferir funcions pures. Una funció que no té efectes secundaris i retorna sempre el mateix resultat per als mateixos paràmetres és trivialment testejable. No sempre és possible, però quan ho és, simplifica tant els tests com el raonament sobre el codi.
  • Injectar les dependències. Si un objecte necessita un servei extern, rep-lo per constructor o per paràmetre, no el creïs internament. Això permet substituir-lo per un test double (dummy, stub, mock o fake) en els tests.
  • Mantenir els constructors simples. Un constructor que fa feina (connectar-se a un servei, llegir configuració, iniciar fils) dificulta la creació d’objectes en tests. El constructor hauria d’assignar dependències; la feina hauria de començar després.

La connexió amb l’arquitectura és directa: les fronteres descrites al document d’arquitectura són els punts on es substitueixen implementacions reals per test doubles. Un sistema amb fronteres ben definides és un sistema fàcil de testejar. Les tècniques de prova (piràmide de proves, test doubles, TDD) es tracten al capítol de proves.

Seams

Un seam (costura, en el sentit del lloc per on una peça es pot descosir) és un punt on es pot canviar el comportament d’un programa sense modificar el codi d’aquell lloc. És el nom propi de la idea que uneix la testabilitat amb les fronteres arquitectòniques.

El terme el va introduir Michael Feathers a Working Effectively with Legacy Code. Dit d’una altra manera, és un punt de substitució: on podem canviar una implementació per una altra (una base de dades real per una de falsa, un client de correu per un mock, un proveïdor de pagament per un altre) sense tocar el codi que l’utilitza.

Els seams són, per tant, la mateixa idea que ja hem trobat sota diferents noms:

  • La injecció de dependència crea un seam al constructor o al mètode: qui construeix l’objecte decideix quina implementació hi entra.
  • Les interfícies, i els ports de l’arquitectura hexagonal, són seams: el contracte queda fix i la implementació concreta és intercanviable.
  • Les factories, el Service Locator i els sistemes de plugins (com al microkernel) són variants del mateix mecanisme: delegar o resoldre quina implementació s’usa.

Per això unes fronteres ben definides són alhora els seams del sistema: en producció s’hi connecta la implementació real i, en els tests, s’hi injecta un test double. Qui pren aquesta decisió és el composition root, l’únic lloc que ha de conèixer les implementacions concretes. Vist així, una suite de proves és un composition root alternatiu: cableja les mateixes peces amb unes altres implementacions.

De totes les maneres de crear seams, la més habitual en el software modern és la composició mitjançant injecció de dependència. La resta (configuració, herència amb sobreescriptura de mètodes, substitució en temps d’enllaç, monkey patching…) són alternatives que apareixen segons el llenguatge i el context, però comparteixen el mateix objectiu: deixar preparats, de manera intencionada, els punts on el comportament es pot canviar substituint components en lloc de modificar el codi existent.

El vocabulari del domini

El llenguatge ubic (ubiquitous language) és el conjunt de termes del domini que s’utilitzen igual a les converses, als requisits i al codi: un concepte, una paraula. Fixar-lo va abans de dissenyar cap peça.

El benefici immediat és mecànic. Un glossari de termes del domini, encara que sigui un fitxer de text, elimina tota una classe de malentesos, tant amb una persona com amb un agent d’IA.

El benefici de fons és de disseny. Si el negoci parla de comandes i al codi hi ha una classe Order, una PurchaseRequest i una taula pedidos, tenim tres noms per a un sol concepte i cap d’ells és el del domini. Quan el codi i l’expert del domini fan servir paraules diferents per a la mateixa cosa, o la mateixa paraula per a coses diferents, hi ha un concepte mal delimitat, i tard o d’hora es traduirà en un mòdul mal delimitat. Quan el vocabulari està fixat, la resta és tipografia i gramàtica.

Regles per anomenar

Cada llenguatge de programació té les seves regles tipogràfiques i gramaticals per anomenar els seus elements. Les regles tipogràfiques varien segons el llenguatge; els criteris gramaticals solen ser més universals.

Regles tipogràfiques de Java
  • Els paquets s’escriuen amb lletres minúscules, amb components separats per punts de més genèric a més específic: org.junit.jupiter.api.
  • Les classes tenen una o més paraules, sense abreviatures, amb cadascuna en majúscules: List, FutureTask.
  • Els mètodes i els camps segueixen la mateixa regla, però la primera lletra ha de ser minúscula: remove, ensureCapacity.
  • Les variables locals segueixen el mateix criteri, però permeten abreviatures: i, houseNum.
  • Els paràmetres de tipus són una lletra, on T és un tipus qualsevol, E un tipus d’element d’una col·lecció, K/V una clau i un valor, X una excepció, i R un tipus de retorn.
Criteris gramaticals

Solen ser més flexibles que els tipogràfics:

  • Les classes són noms o frases nominals: Thread, PriorityQueue.
  • Les interfícies poden anomenar-se com les classes o bé com un adjectiu que acaba amb able o ible: Runnable, Iterable.
  • Els mètodes que realitzen alguna acció són verbs o frases verbals: append, drawImage.
  • Els mètodes que retornen un boolean solen començar per is o has, i després poden seguir amb un nom, frase nominal o qualsevol paraula o frase que funcioni com adjectiu: isDigit, isProbablePrime, isEmpty, isEnabled, hasSiblings.
  • Els mètodes que retornen un valor no boolean o un atribut de l’objecte s’anomenen amb un nom o frase nominal, o una frase que comença per get: size, hashCode, getTime.
  • Els mètodes getters i setters són una construcció obsoleta en molts casos, utilitzar-los amb cautela.
  • Els camps de tipus boolean solen anomenar-se com l’accessor, però ometent ‘is’ o ‘has’: initialized, started.
  • Els camps d’altres tipus no boolean solen ser noms o frases nominals: height, digits, bodyStyle.

Programació estratègica i tàctica

La programació tàctica optimitza que allò funcioni ara: el criteri d’èxit és acabar la funcionalitat. La programació estratègica assumeix que el resultat de la feina no és la funcionalitat sinó el sistema, i dedica una part de cada canvi a mantenir el disseny en condicions.

La diferència no és una fase de neteja al final, sinó un percentatge constant de cada canvi. La refactorització és el cost normal del mode estratègic: que el codi funcioni no vol dir que el disseny sigui bo, i sense refactoritzar la complexitat s’acumula fins que qualsevol canvi surt car.

Tot el capítol respon a una sola pregunta: què passarà quan canviï el requisit? Un bon disseny fa que els canvis siguin locals.

Escriure codi s’ha fet barat; entendre’l i verificar-lo, no. Una eina que genera implementacions en segons també genera complexitat en segons, i el límit continua sent la capacitat de revisar-la. Per això els principis d’aquest capítol pesen més que abans, no menys: les fronteres clares, les interfícies estretes i el vocabulari fixat són el que permet delegar la implementació sense perdre el control del disseny. El treball amb aquestes eines es tracta a Fonaments de programació amb IA.

Manifest

El manifest recull els principis d’aquest capítol i els de la pràctica que els envolta: entendre abans de construir, avançar en passos petits i verificar de forma contínua. Aquests últims no es tracten aquí, sinó a proves i a Fonaments de programació amb IA.

Baixa el manifest en PNG

El criteri de correcció

Les passes 3 a 7 del bucle són el cicle red-green-refactor del desenvolupament guiat per proves. El TDD estricte és minoritari a la indústria, però el bucle continua sent vàlid, perquè el que el sosté no és l’ordre de les passes sinó una regla:

El criteri de correcció no pot sortir del codi que jutja.

Un exemple del que passa quan no es compleix: demanes a un agent una funció que calculi un descompte, llegeixes el resultat, et sembla raonable i escrius una prova que el confirma. La prova passa, però no ha verificat res: has copiat al test el comportament del codi. Escriure-la abans ho fa impossible, i per això el bucle la posa primer.

Quines formes pot tenir aquest criteri i quan s’escriu cadascuna es detalla a El criteri de correcció.

La IA dins del bucle

El manifest reparteix la feina: la persona decideix què i per què, la IA ajuda amb el com. Passa per passa:

  1. Entendre. La IA ajuda a explorar el domini: resums, exemples, o que et faci preguntes sobre el que vols construir. El que no pot fer és entendre-ho per tu. Si acabes la conversa sense saber explicar el problema, encara no pots passar a la passa següent.
  2. Dissenyar. Demana-li alternatives i contrasta-les, però decideix tu les fronteres, les interfícies i els noms. Són les decisions que fixen què costarà canviar demà, i les ha de prendre qui hi haurà de conviure.
  3. Especificar. Els casos i els valors esperats els fixes tu, perquè són el criteri. La IA pot escriure la mecànica de les proves: muntar l’escenari, preparar dades, repetir estructura. Evita l’atall de demanar-li proves i implementació alhora, perquè aleshores el criteri surt del mateix lloc que el codi.
  4. Provar. Executa les proves i comprova que fallen pel motiu que esperaves. La IA no et detectarà una prova trencada, perquè per a ella una prova verda és un èxit.
  5. Implementar. És la passa més delegable, i l’única que ho és del tot: si les anteriors estan fetes, l’encàrrec és petit i el criteri ja existeix. Continues decidint la mida del tros que encarregues, perquè el coll d’ampolla és revisar-lo, no escriure’l.
  6. Verificar. Les proves verdes no són l’acceptació: cobreixen el que has previst, i la implementació pot fer coses que no has previst. Accepta només el codi que has llegit.
  7. Refactoritzar. Amb les proves en verd, els canvis mecànics (extreure un mètode, reanomenar, eliminar duplicació) es deleguen bé. Jutjar si el disseny continua sent bo és feina teva i no té resposta automàtica.

El que fa una peça delegable és el mateix que la fa bona: un mòdul profund, amb una interfície estreta i el vocabulari del domini fixat, s’encarrega en una frase i es revisa en una lectura. Si no pots explicar l’encàrrec, el problema és el disseny, no l’eina.

Referències

Last change: , commit: 6491f93