Disseny
- Principis generals de programació
- És dolent el teu codi?
- Principis de POO (SOLID)
- Dependències a POO
- Disseny per a la testabilitat
- Seams
- Bones pràctiques per escriure codi
- Com anomenar
- Referències
Principis generals de programació
- 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.
- Evita crear YAGNI (no ho necessitaràs).
- Fes la feina més senzilla que sigui funcional.
- No em facis pensar.
- Escriu el codi per qui l’haurà de mantenir.
- El principi de la mínima sorpresa.
- Minimitzar l’acoblament i maximitzar la cohesió.
- Amagar els detalls de la implementació.
- La llei de Demeter: el codi només s’ha de comunicar amb les seves relacions directes.
- Evitar la optimització prematura: només si funciona i es lent.
- Reutilitzar codi és bo: el fa més llegible.
- 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.
És dolent el teu codi?
- É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.
- É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.
- 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.
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.
Principis de POO (SOLID)
- 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. Code smell: codi client que comprova el tipus concret d’un objecte (instanceof, type checks) per decidir com tractar-lo.
- Segregació d’interfície: es 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
A POO, si tenim dues classes A i B, i A necessita a B per fer la seva feina, llavors A té una dependència de B. Per a resoldre aquesta dependència podem fer:
- Que A crei o obtingui un objecte B. La classe A té el control de la dependència.
- 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:
- Que necessitem una nova instància cada cop. Per exemple, les factories creen múltiples objectes.
- 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:
- 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.
- 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. Un codi ben dissenyat és, quasi sempre, un codi fàcil de testejar, i les dificultats per escriure tests solen ser símptomes de problemes de disseny.
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 (p. ex.
new DatabaseClient()dins d’un mètode), 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. - 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
- Separar lògica de 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ï 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.
Seams
La idea que uneix la testabilitat amb les fronteres arquitectòniques té un nom propi: el seam.
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. 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.
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.
Bones pràctiques per escriure codi
Estàs escrivint codi per llegir-lo en el futur, o bé per un altre…
- Les classes han de ser petites, per sota de 500 línies, i han de tenir un nombre limitat de mètodes
- Els mètodes han de ser petits, per sota de 30 línies, i han de fer una feina concreta
- Has d’escriure el codi perquè s’expliqui a ell mateix, però on no arribis, utilitza comentaris
- No facis línies massa llargues, com a molt de 120 caràcters
- Manté baix el nivell de sagnat del codi, i intenta no superar els 3-4 nivells
- Anomena les classes, els mètodes i les variables amb els criteris ja explicats
- Decideix el teu estil i segueix-lo de forma consistent
Com 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. Per exemple, les regles tipogràfiques de Java són:
- 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.
Els criteris gramaticals solen ser més flexibles:
- 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
ableoible: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
isohas, 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.
Referències
- Inversion of Control Containers and the Dependency Injection pattern
- Best Practices for Designing and Implementing a Library in Java
- Dependency injection
- Working Effectively with Legacy Code (Michael Feathers)
- Top 20 Design Heuristics
- Design Principles (Bob Martin)
- Listing of Arthur Riel’s heuristics
- Simpler Encapsulation with Immutability
- Effective Java by Joshua Bloch