Prácticas de ingeniería de software¶
De un vistazo
Materia: Capa 1 · Código correcto y mantenible · Tiempo de lectura: ~35 min · Requisitos previos: haber pasado por la Capa 0 (Cimientos).
En una frase: vas a entender por qué el código se lee muchísimo más de lo que se escribe y qué prácticas concretas —nombres, funciones pequeñas, acoplamiento y cohesión, DRY/KISS/YAGNI, SOLID, refactoring, commits, code review y deuda técnica— hacen que un programa siga siendo barato de cambiar dentro de dos años. Mensaje de fondo: la mantenibilidad es economía, no estética.
Por qué esto importa¶
Tu código funciona hoy. El test pasa, el cliente está contento, cierras el ticket. Entonces, ¿por qué obsesionarse con si un nombre "comunica intención" o una función tiene 8 líneas o 40?
Porque el código no se escribe una vez y se olvida. Se vive. Y la parte cara de vivirlo no es el rato que tardaste en teclearlo; es todo lo que viene después:
- Dentro de seis meses, tú u otro tenéis que añadir una funcionalidad aquí, y antes hay que entender qué hace este método de 200 líneas.
- Aparece un bug en producción un viernes a las 18:00 y hay que localizarlo leyendo código que nadie recuerda haber escrito.
- Entra alguien nuevo al equipo y tarda tres semanas en atreverse a tocar el módulo de facturación porque "es que ahí no se sabe qué rompe".
- Quieres cambiar el proveedor de pagos y descubres que la lógica de Stripe está esparcida por 30 ficheros.
Todas estas situaciones tienen la misma raíz: alguien tiene que leer y entender código que ya existe para poder cambiarlo sin miedo. Y ese "leer y entender" es, de largo, la actividad más frecuente y más cara de un equipo de software. Se estima que un programador pasa del orden de 10 veces más tiempo leyendo código que escribiéndolo —leyendo el propio, el ajeno, el de hace un año. Cada línea que escribes se leerá decenas de veces.
Cuando interiorizas eso, todas las "buenas prácticas" dejan de ser reglas de estilo que un linter te impone y pasan a ser lo que son: decisiones de economía. Un nombre claro no es por elegancia; es para que la próxima persona (probablemente tú) no tenga que reconstruir en su cabeza lo que ese nombre podría haber dicho gratis. Ese es el salto de "sé hacer que funcione" a "sé hacer que siga funcionando y siga siendo barato de cambiar".
Intuición primero: el código como texto que otros leerán¶
Imagina que el código no es una lista de instrucciones para la máquina —eso ya lo viste en Modelo de ejecución, y a la CPU le da igual cómo se llamen tus variables— sino un texto escrito para otro humano, que da la casualidad de que también compila.
El compilador se traga cualquier cosa: $x, $temp, $data2, funciones de 500 líneas, if anidados siete niveles. Le da exactamente igual. El único público que sufre o disfruta cómo está escrito el código son las personas: tú dentro de un año, el compañero que hace la review, quien herede el proyecto.
graph LR
A["Escribes código<br/>(una vez)"] --> B["La máquina<br/>lo ejecuta"]
A --> C["Humanos lo leen<br/>(decenas de veces)"]
C --> D["Lo entienden<br/>y lo cambian"]
C --> E["No lo entienden<br/>y lo temen"]
Con esa lente, la pregunta que gobierna cada decisión deja de ser "¿esto funciona?" y pasa a ser "¿cuánto le costará a la próxima persona entender y cambiar esto?". La primera pregunta la responde el test. La segunda la responde tu criterio, y es la que decide si el proyecto envejece bien o se pudre.
Guarda esta imagen —el código es texto para humanos— porque de ella cuelga literalmente todo lo demás de esta lección.
El detalle¶
1. Qué es "mantenible" y por qué es economía¶
Mantenibilidad es la facilidad con la que se puede cambiar un sistema: corregir un fallo, añadir una función, adaptarlo a un requisito nuevo. No es una propiedad estética ("qué bonito queda") ni moral ("hay que hacerlo bien"). Es una propiedad económica: mide cuánto cuesta el próximo cambio.
Pensemos en el coste total de una línea de código a lo largo de su vida:
- Coste de escritura: lo que tardas en teclearla y hacer que pase el test. Ocurre una vez.
- Coste de lectura: lo que tarda cada persona en entenderla cada vez que pasa por ella para cambiar algo cerca. Ocurre muchas veces.
- Coste de cambio: lo que cuesta modificarla sin romper nada, incluyendo el riesgo de que rompa algo lejano.
El error del principiante —y de los plazos apretados— es optimizar solo el primero. "Ya funciona, siguiente." Pero el primero es una fracción minúscula del total. Optimizar la escritura a costa de la lectura es como ahorrar cinco minutos escondiendo las herramientas en un cajón sin etiquetar: lo pagas cada vez que necesitas una.
La curva de coste del cambio
En un código sano, el coste de añadir una función parecida a las que ya hay se mantiene plano con el tiempo: la función número 50 cuesta parecido a la número 5. En un código podrido, la curva sube: cada cambio es más caro que el anterior porque hay que pelear con lo anterior. Cuando esa curva se dispara, el equipo dice cosas como "es más fácil reescribirlo". Eso no es un problema técnico; es el resultado acumulado de mil decisiones de legibilidad que se tomaron mal.
La consecuencia práctica: "mantenible" no significa "perfecto", significa "barato de cambiar para lo que razonablemente vaya a pasar". No es sobre-ingeniería (eso sale caro ya); es sobre no dejar minas para el yo del futuro. Todo lo que sigue son técnicas concretas para bajar ese coste de lectura y cambio.
2. Nombres que comunican intención¶
El nombre es la herramienta de comunicación más barata y más potente que tienes. Un buen nombre ahorra un comentario, ahorra leer la implementación, ahorra una pregunta en Slack. Un mal nombre miente o no dice nada, y obliga a reconstruir a mano lo que podría haber estado escrito.
La regla no es "nombres largos" ni "nombres cortos"; es nombres que revelan la intención: qué es esto, por qué existe, cómo se usa. Compara:
// ❌ El nombre no dice nada; hay que leer el cuerpo para saber qué es
function proc($d) {
$r = [];
foreach ($d as $x) {
if ($x->s === 1) {
$r[] = $x;
}
}
return $r;
}
// ✅ El nombre y las variables cuentan la historia; no hace falta leer más
function filtrarPedidosPagados(array $pedidos): array {
$pagados = [];
foreach ($pedidos as $pedido) {
if ($pedido->estado === EstadoPedido::PAGADO) {
$pagados[] = $pedido;
}
}
return $pagados;
}
Las dos funciones hacen exactamente lo mismo a ojos de la máquina. Para un humano son mundos distintos: la primera es un acertijo, la segunda es una frase. Y no ha costado más escribir la segunda.
Criterios concretos que funcionan:
- El nombre revela el propósito, no el mecanismo.
filtrarPedidosPagadosdice qué obtienes;procesarListadice que hay un bucle, que es lo que ya se ve. - Evita
$data,$info,$temp,$aux,$obj,$manager,$helper. No comunican nada: todo es datos, todo es información. Di qué datos. - La longitud del nombre escala con el tamaño del scope. En un
foreachde tres líneas,$ppuede valer. Una propiedad de clase usada en 200 líneas necesita un nombre completo. - Los booleanos se leen como una afirmación:
$estaActivo,$tienePermiso,puedeEditar(). Asíif ($usuario->puedeEditar())se lee como español. - Sé consistente: si en un sitio es
obtener, no lo llamesget,fetchyrecuperaren otros tres. El vocabulario del proyecto es un idioma; no lo hables con acento distinto en cada fichero.
La prueba del nombre honesto
Si para saber qué hace una función tienes que abrir su cuerpo, el nombre ha fallado. Y si el nombre promete algo que el cuerpo no cumple (una función validarEmail() que además guarda en base de datos), el nombre miente, que es peor que no decir nada: te hará confiar y te traicionará. Un nombre que miente es una trampa que tú mismo pones.
3. Funciones pequeñas y con un solo propósito¶
Una función debería hacer una cosa, hacerla bien, y ser lo único que hace. No porque "las funciones pequeñas sean bonitas", sino por tres razones económicas muy concretas:
- Se entiende de un vistazo. Si cabe en la pantalla y tiene un solo propósito, la lees y sabes qué hace sin scroll ni memoria de trabajo saturada.
- Se nombra bien. Una función que hace una sola cosa tiene un nombre honesto y preciso. Una que hace cinco cosas se llama
procesarPedidoy el nombre ya está mintiendo por omisión. - Se reutiliza y se testea aislada. Un trozo con un propósito claro se prueba solo y se usa desde varios sitios; un mazacote de 200 líneas no se puede probar sin montar medio mundo.
El síntoma clásico de una función que hace demasiado es que la puedes describir con un "y": "esta función valida el pedido y calcula el total y manda el email y actualiza el stock". Cada "y" es un candidato a función propia.
// ❌ Un método que hace cuatro cosas; imposible de nombrar honestamente
public function procesarPedido(Pedido $pedido): void {
// validar
if (empty($pedido->getLineas())) {
throw new PedidoVacioException();
}
// calcular total
$total = 0;
foreach ($pedido->getLineas() as $linea) {
$total += $linea->getPrecio() * $linea->getCantidad();
}
$total *= 1.21; // IVA
$pedido->setTotal($total);
// descontar stock
foreach ($pedido->getLineas() as $linea) {
$producto = $linea->getProducto();
$producto->setStock($producto->getStock() - $linea->getCantidad());
}
// notificar
mail($pedido->getCliente()->getEmail(), 'Pedido confirmado', '...');
}
// ✅ Cada paso es una función con nombre; el método de arriba se lee como un índice
public function procesarPedido(Pedido $pedido): void {
$this->validar($pedido);
$this->calcularTotal($pedido);
$this->descontarStock($pedido);
$this->notificarCliente($pedido);
}
Fíjate en lo que pasa con la versión buena: procesarPedido se ha convertido en un resumen legible de lo que ocurre, en el nivel de abstracción correcto. Si necesitas el detalle del IVA, bajas a calcularTotal. Si no, no te molesta. Esto es lo que se llama una función por nivel de abstracción: dentro de una función, todas las líneas hablan "al mismo nivel". La versión mala mezcla el "qué" (procesar un pedido) con el "cómo" (multiplicar por 1.21) en el mismo sitio, y eso es lo que agota al lector.
Cuidado con el dogma del tamaño
"Funciones pequeñas" no significa "trocea hasta que cada función tenga tres líneas". Partir por partir crea el problema contrario: para entender una operación tienes que saltar por doce funciones minúsculas repartidas por el fichero (a esto se le llama código lasaña, capas y capas que hay que atravesar). El criterio no es el número de líneas: es un propósito, un nivel de abstracción. Si trocear te obliga a inventar nombres artificiales (procesarPedidoParte2), te has pasado.
4. Acoplamiento y cohesión: la pareja que lo gobierna todo¶
Estas dos palabras son, probablemente, las más importantes de toda la ingeniería del software, y casi todo lo demás (SOLID incluido) son consecuencias de ellas. Nombran las dos fuerzas que deciden si un sistema es manejable o un plato de espaguetis.
- Cohesión (dentro de un módulo): cuánto tienen que ver entre sí las cosas que están juntas. Alta cohesión = bueno. Una clase
Facturaque solo tiene cosas de facturas está cohesionada. Una claseUtilscon métodos para formatear fechas, enviar emails y validar DNIs no tiene cohesión: es un cajón de sastre. - Acoplamiento (entre módulos): cuánto depende un módulo de los detalles internos de otro. Bajo acoplamiento = bueno. Si para cambiar
Facturatienes que tocar tambiénPedido,ClienteyInforme, están fuertemente acoplados: no puedes mover una pieza sin mover las de al lado.
La regla de oro, que resume media carrera: alta cohesión, bajo acoplamiento. Cosas relacionadas, juntas; cosas independientes, separadas y comunicándose por interfaces estrechas.
graph TB
subgraph MAL["Alto acoplamiento · todo depende de todo"]
A1["Pedido"] <--> B1["Cliente"]
A1 <--> C1["Factura"]
B1 <--> C1
A1 <--> D1["Stock"]
C1 <--> D1
end
subgraph BIEN["Bajo acoplamiento · dependencias claras y dirigidas"]
A2["Pedido"] --> B2["Cliente"]
A2 --> C2["Factura"]
A2 --> D2["Stock"]
end
Por qué importa tanto, en términos de coste: el acoplamiento es lo que hace que un cambio pequeño se convierta en un cambio grande. En un sistema muy acoplado, tocar una línea te obliga a tocar diez sitios más, y cada uno puede romper algo lejano que ni sabías que dependía de esto. El miedo a tocar código —ese "mejor no lo toco que se rompe"— es acoplamiento no diagnosticado.
Un ejemplo concreto de acoplamiento innecesario y su cura:
// ❌ Pedido conoce los detalles internos de cómo se envía un email (SMTP, plantillas...)
class Pedido {
public function confirmar(): void {
$mailer = new PHPMailer();
$mailer->Host = 'smtp.gmail.com';
$mailer->Port = 587;
$mailer->setFrom('no-reply@tienda.com');
// ...20 líneas de configuración SMTP dentro de Pedido
}
}
// ✅ Pedido solo sabe "quiero notificar"; el CÓMO vive detrás de una interfaz
class Pedido {
public function __construct(private Notificador $notificador) {}
public function confirmar(): void {
$this->notificador->enviar($this->cliente, 'Pedido confirmado');
}
}
En la versión buena, Pedido no sabe nada de SMTP. Si mañana cambias a un servicio de emails distinto, o a SMS, o a una cola de mensajes, Pedido no se entera: solo cambia la implementación de Notificador. Has cortado el acoplamiento. Esta idea —depender de qué necesito (una interfaz) y no de cómo se hace (una implementación concreta)— es el corazón de la inyección de dependencias y volverá, con nombre y apellidos, en SOLID.
5. DRY, KISS, YAGNI: útiles hasta que se convierten en dogma¶
Tres acrónimos que oirás mil veces. Los tres son buenos consejos y los tres se abusan hasta hacer daño. La diferencia entre usarlos bien y mal es entender qué problema resuelve cada uno en lugar de aplicarlos como reflejo.
DRY — Don't Repeat Yourself (no te repitas). La idea real, tal como la formularon sus autores, no es "no escribas dos líneas parecidas": es "cada pieza de conocimiento debe tener una única representación en el sistema". Si la regla "el IVA es el 21%" está copiada en ocho sitios, el día que cambie tendrás que encontrar los ocho, y te dejarás uno. Eso es lo que DRY previene: la duplicación de decisiones, no de caracteres.
El abuso: unificar código que se parece por casualidad pero representa decisiones distintas. Dos funciones que hoy hacen lo mismo pero por razones diferentes no son duplicación: son un acoplamiento esperando a nacer. El día que una tenga que cambiar y la otra no, tu abstracción "DRY" te obliga a añadir un if para separarlas, y otro, y otro. A menudo un poco de duplicación es más barata que la abstracción equivocada.
La regla práctica de DRY: el conocimiento, no el texto
Antes de eliminar una repetición, pregúntate: ¿estas dos cosas cambiarán siempre juntas y por la misma razón? Si sí, es la misma decisión duplicada → únelas. Si es casualidad que hoy se parezcan → déjalas. "Duplicación" en el sentido malo es duplicar conocimiento, no duplicar líneas.
KISS — Keep It Simple, Stupid (hazlo simple). Entre dos soluciones que resuelven el problema, elige la más simple de entender. No la más lista, no la que demuestra que sabes patrones: la que la próxima persona entiende sin esfuerzo. El código "ingenioso" es un placer de escribir y un castigo de mantener. Nadie ha agradecido nunca heredar un one-liner críptico.
YAGNI — You Aren't Gonna Need It (no lo vas a necesitar). No construyas cosas para un futuro hipotético. El gancho de configuración "por si algún día", la capa de abstracción para "cuando soportemos cinco bases de datos", el sistema de plugins para un proyecto que tiene un cliente. Casi siempre ese futuro no llega, o llega distinto de como lo imaginaste, y mientras tanto pagas la complejidad hoy: más código que leer, más que testear, más sitios donde puede fallar.
La tensión honesta entre ellos: YAGNI te dice "no abstraigas de más", DRY te empuja a abstraer. KISS arbitra. La resolución no es un acrónimo ganador, es criterio: abstrae cuando la duplicación ya duele de verdad (tercera vez, regla del tres), no cuando la imaginas doler. Antes de eso, la duplicación es más barata que la abstracción prematura.
6. SOLID por intuición, no como catecismo¶
SOLID son cinco principios de diseño orientado a objetos. El problema de cómo se enseñan es que se recitan como mandamientos y el alumno los memoriza sin entender qué dolor evitan. Vamos al revés: cada uno es una respuesta a un tipo concreto de "y ahora esto es un infierno de cambiar". Todos, en el fondo, empujan hacia lo mismo que ya viste: alta cohesión, bajo acoplamiento.
S — Responsabilidad única (Single Responsibility). Una clase debería tener una sola razón para cambiar. No "hacer una sola cosa" literalmente, sino: si dos motivos de negocio distintos (cambia el cálculo de impuestos / cambia el formato del PDF) te obligan a tocar la misma clase, esa clase tiene dos responsabilidades y las estás acoplando. Sepáralas y cada una cambiará por su lado.
// ❌ Cambia por dos razones: la lógica de la factura Y el formato de salida
class Factura {
public function calcularTotal(): float { /* ... */ }
public function generarPdf(): string { /* maquetación, fuentes, márgenes... */ }
}
// ✅ Cada clase, una razón para cambiar
class Factura { public function calcularTotal(): float { /* ... */ } }
class FacturaPdfGenerator { public function generar(Factura $f): string { /* ... */ } }
O — Abierto/cerrado (Open/Closed). El código debería estar abierto a extensión, cerrado a modificación: poder añadir un caso nuevo sin editar el código existente. El síntoma de que lo violas es el switch/if gigante que crece cada vez que aparece un tipo nuevo, y que hay que abrir y tocar cada vez.
// ❌ Cada método de pago nuevo obliga a abrir y editar este switch
function comision(string $metodo, float $importe): float {
switch ($metodo) {
case 'tarjeta': return $importe * 0.02;
case 'paypal': return $importe * 0.03;
// ...y mañana 'bizum', y hay que volver aquí
}
}
// ✅ Añadir un método nuevo = crear una clase nueva; este código no se toca
interface MetodoPago {
public function comision(float $importe): float;
}
class Tarjeta implements MetodoPago {
public function comision(float $importe): float { return $importe * 0.02; }
}
class Bizum implements MetodoPago {
public function comision(float $importe): float { return 0.0; }
}
L — Sustitución de Liskov (Liskov Substitution). Si algo espera un tipo, cualquier subtipo debe poder usarse sin que el que llama se lleve una sorpresa. El ejemplo canónico: Cuadrado extends Rectangulo parece razonable hasta que alguien hace rect.setAncho(5); rect.setAlto(3) y espera área 15, pero como era un cuadrado por debajo sale 9. El subtipo ha roto una promesa del tipo base. La intuición: heredar no es "se parece a", es "puede sustituir a sin mentir".
I — Segregación de interfaces (Interface Segregation). Mejor varias interfaces pequeñas y específicas que una gorda que obliga a implementar métodos que no te tocan. Si una interfaz Trabajador tiene trabajar() y comer(), un RobotTrabajador se ve forzado a implementar comer() con un throw new Exception() absurdo. La interfaz pedía de más.
D — Inversión de dependencias (Dependency Inversion). Los módulos de alto nivel (tu lógica de negocio) no deben depender de los de bajo nivel (la base de datos concreta, el proveedor de email); ambos deben depender de abstracciones. Es exactamente lo que hicimos con Pedido y Notificador en la sección de acoplamiento: Pedido depende de la interfaz Notificador, no de PHPMailer. Por eso puedes cambiar el detalle sin tocar la lógica.
SOLID no es una checklist
Aplicar SOLID a ciegas produce el mismo daño que ignorarlo: una interfaz por clase "por si acaso", fábricas de fábricas, indirección para todo. SOLID son heurísticas para bajar el acoplamiento cuando el acoplamiento duele, no un examen que hay que aprobar en cada fichero. Si un principio te lleva a más complejidad de la que quita, el principio no aplica ahí. El objetivo nunca es "cumplir SOLID"; es "que esto sea barato de cambiar".
7. Code smells y refactoring¶
Un code smell (mal olor) es una señal en la superficie del código que sugiere un problema más profundo. No es un bug —funciona— pero huele a que mantenerlo va a doler. Aprender a olerlos es aprender a diagnosticar antes de que reviente. Los clásicos:
- Función larga / clase enorme (God class): hace demasiado, viola cohesión.
- Lista de parámetros larga:
crear($a, $b, $c, $d, $e, $f)casi siempre esconde un objeto que quiere nacer. - Código duplicado: la misma decisión en varios sitios (ojo, la duplicación real, la de conocimiento).
- Envidia de características (feature envy): un método que usa más los datos de otra clase que los de la suya → probablemente vive en el sitio equivocado.
- Números y cadenas mágicas:
* 1.21,if ($estado === 3)→ ¿qué es 3? Dale un nombre. - Comentarios que explican qué hace un código enrevesado: el comentario a menudo es una disculpa por no haberlo escrito claro. Refactoriza hasta que sobre.
El refactoring es la técnica para pagar estos olores: cambiar la estructura interna del código sin cambiar su comportamiento observable. Esta última parte es sagrada. Refactorizar no es "reescribir": es una serie de transformaciones pequeñas y seguras —renombrar, extraer una función, mover un método— cada una de las cuales deja el programa haciendo exactamente lo mismo que antes.
graph LR
A["Tests en verde"] --> B["Un cambio de<br/>estructura pequeño"]
B --> C["Tests en verde<br/>otra vez"]
C --> D{"¿Sigue oliendo?"}
D -->|"sí"| B
D -->|"no"| E["Hecho, mismo<br/>comportamiento"]
Por eso el refactoring y los tests son inseparables (y por eso hay una lección entera sobre ellos en esta capa): los tests son la red de seguridad que te dice, después de cada pasito, que no has cambiado el comportamiento. Sin tests, "refactorizar" es en realidad "reescribir con los ojos vendados y rezar". Con tests, es una operación segura y reversible.
Regla de oro operativa: nunca refactorices y añadas funcionalidad a la vez. Son dos sombreros distintos. O cambias la estructura sin cambiar el comportamiento (refactor), o cambias el comportamiento (feature), pero no ambos en el mismo movimiento, porque si algo se rompe no sabrás cuál de las dos cosas fue.
8. Control de versiones: commits atómicos y con sentido¶
Git no es un botón de "guardar en la nube". Es la historia razonada de por qué el código es como es, y esa historia se lee tanto como el código. Cuando dentro de un año te preguntes "¿por qué demonios esto se hace así?", la respuesta debería estar en git blame → el commit → su mensaje. Si el mensaje dice "cambios" o "fix", la historia es basura y estás solo.
Un buen commit es atómico: contiene un cambio lógico completo y coherente, ni más ni menos. No medio cambio (que deja el repo roto entre commits), ni cinco cambios sin relación mezclados (imposibles de entender, de revisar y de revertir por separado).
Por qué importa la atomicidad, en concreto:
git revertfunciona: si un commit hace una sola cosa, puedes deshacer esa cosa sin arrastrar otras cinco.git bisectencuentra el bug: si cada commit es coherente y funciona, puedes bisecar la historia para localizar exactamente qué cambio introdujo un fallo. Con commits gigantes que mezclan todo, bisect te señala un mazacote y no has ganado nada.- La review es posible: revisar un commit con un propósito es factible; revisar "he tocado 40 ficheros por tres razones distintas" no.
El mensaje es la mitad del valor. La convención que ha ganado: una primera línea imperativa y corta (el qué), y si hace falta, un cuerpo que explique el porqué —que es lo que el código no puede contar por sí mismo.
Corrige el cálculo del IVA en pedidos con descuento
El descuento se aplicaba después del IVA, inflando el total.
Ahora se aplica sobre la base imponible, antes de impuestos,
como exige la normativa. Afecta solo a pedidos con cupón.
El código te dice qué se hizo. El mensaje te dice por qué, y el porqué es lo que no puedes recuperar leyendo el diff. Un diff sin porqué es un jeroglífico dentro de un año.
El truco mental del commit atómico
Si al escribir el mensaje necesitas la palabra "y" ("arregla el IVA y cambia el logo y actualiza dependencias"), probablemente son varios commits. Sepáralos. La misma señal del "y" que delataba funciones que hacen demasiado.
9. Code review como práctica de equipo¶
La revisión de código es que otra persona lea tu cambio antes de que entre. Su valor real no es "cazar bugs" (que también, pero los tests cazan más): es que el conocimiento del código no se quede en una sola cabeza, que el estándar de calidad sea del equipo y no de cada individuo, y que la próxima persona que lea ese código ya sea, como mínimo, dos.
Lo que una buena review persigue, en orden de importancia:
- ¿Es correcto? ¿Hace lo que dice? ¿Contempla los casos raros?
- ¿Se entiende? Si el revisor no lo entiende leyéndolo, la próxima persona tampoco. Eso es un problema, no una cuestión de gustos.
- ¿Encaja con el resto? ¿Sigue las convenciones del proyecto, no reinventa lo que ya existe?
- ¿Tiene tests y cubren lo que importa?
Y la parte que casi nadie enseña: cómo se hace sin envenenar al equipo. La review es sobre el código, nunca sobre la persona. "Este nombre me costó entenderlo, ¿filtrarPagados sería más claro?" construye; "¿por qué has puesto esto así?" en tono de fiscal, destruye. Como autor, recuerda que tú no eres tu código: una crítica al código no es una crítica a ti, y separar eso es lo que hace que un equipo mejore en vez de ponerse a la defensiva. Distingue también lo que bloquea (un bug, un fallo de seguridad) de lo que es preferencia (dónde va una llave); marca lo segundo como opcional y no lo conviertas en un muro.
10. Deuda técnica: cuándo pedirla y cuándo pagarla¶
La deuda técnica es la metáfora más útil y más malinterpretada de la profesión. La acuñó Ward Cunningham, y su significado original es preciso: elegir a conciencia una solución imperfecta pero rápida para entregar ya, entendiendo que después habrá que volver a "pagarla" con trabajo de limpieza —igual que un préstamo: dinero ahora, con intereses después.
Las dos palabras clave son a conciencia. La deuda técnica buena es una decisión deliberada: "sé que esto no es la solución limpia, pero necesitamos salir el lunes; lo hacemos rápido y anotamos que hay que volver". Eso es una herramienta de negocio legítima: a veces llegar al mercado hoy vale más que el código perfecto de dentro de un mes.
Lo que no es deuda técnica es el código malo por ignorancia o por dejadez. Eso no es un préstamo que decidiste pedir; es un destrozo que ni sabías que hacías. Cunningham era tajante: la metáfora solo funciona si sabes que estás endeudándote y por qué.
graph TB
A["¿El código imperfecto<br/>fue una decisión consciente<br/>para entregar antes?"] -->|"sí"| B["Deuda técnica legítima<br/>anótala y ponle fecha de pago"]
A -->|"no, es que no supimos<br/>o no nos importó"| C["No es deuda, es un<br/>destrozo · arréglalo y aprende"]
B --> D["¿Los intereses<br/>ya duelen?"]
D -->|"cada cambio aquí<br/>cuesta el triple"| E["Págala ahora"]
D -->|"esta zona no se toca<br/>casi nunca"| F["Déjala, no toda deuda<br/>hay que pagarla"]
El interés de la deuda es lo que pagas cada vez que trabajas sobre esa zona chapucera: cada cambio cuesta más, cada bug es más difícil de encontrar. Y aquí está la clave económica que casi nadie aplica: no toda la deuda hay que pagarla. Deuda en un módulo que no vas a tocar nunca más es como una hipoteca de un piso que ya no usas: técnicamente existe, pero pagarla no te da nada. La deuda que hay que pagar es la que tiene intereses altos: la que está en las zonas que tocas a menudo, donde el "cuesta el triple" se multiplica por la frecuencia. Refactorizar es más rentable donde más pasas.
La decisión, entonces, no es "código limpio siempre" (idealismo caro) ni "ya lo arreglaremos" (nunca llega). Es económica y explícita: ¿cuánto me está costando esta deuda en las zonas donde de verdad trabajo, y cuánto cuesta pagarla? Se decide con esa cuenta, no con la culpa.
Cómo se ve en la práctica¶
Un caso real que le pasa a todo equipo Symfony: un controlador que empezó siendo dos líneas y a base de "un añadido rápido más" acabó siendo esto.
// ❌ El controlador que se comió toda la lógica
#[Route('/pedido/{id}/pagar', methods: ['POST'])]
public function pagar(int $id, Request $request): Response {
$pedido = $this->em->getRepository(Pedido::class)->find($id);
if (!$pedido) { throw new NotFoundHttpException(); }
// validación mezclada con acceso a datos mezclada con lógica de negocio
if ($pedido->getEstado() === 3) { return new Response('Ya pagado', 400); }
$total = 0;
foreach ($pedido->getLineas() as $l) { $total += $l->getPrecio() * $l->getCantidad(); }
$total = $total * 1.21; // IVA... otra vez a mano, otro número mágico
// pasarela de pago acoplada directamente aquí
$stripe = new \Stripe\StripeClient('sk_live_...');
$stripe->charges->create(['amount' => $total * 100, 'currency' => 'eur']);
$pedido->setEstado(3);
$this->em->flush();
mail($pedido->getCliente()->getEmail(), 'Pagado', '...');
return new Response('OK');
}
Cuenta los problemas: número mágico 3, IVA a mano (que ya estaba en otros cinco sitios), Stripe acoplado directamente, cuatro responsabilidades en un método, imposible de testear sin tocar la base de datos real y cobrar de verdad. Cada "arreglo rápido" añadió una capa. Ahora nadie quiere tocarlo.
// ✅ El controlador orquesta; cada pieza tiene su sitio y su nombre
#[Route('/pedido/{id}/pagar', methods: ['POST'])]
public function pagar(int $id, PagarPedido $pagarPedido): Response {
try {
$pagarPedido->ejecutar($id);
} catch (PedidoNoEncontrado) {
throw new NotFoundHttpException();
} catch (PedidoYaPagado $e) {
return new Response($e->getMessage(), 400);
}
return new Response('OK');
}
// La lógica vive en un servicio con una responsabilidad, testeable en aislamiento
class PagarPedido {
public function __construct(
private PedidoRepository $pedidos,
private PasarelaPago $pasarela, // interfaz, no Stripe directo
private Notificador $notificador, // interfaz, no mail()
) {}
public function ejecutar(int $id): void {
$pedido = $this->pedidos->obtener($id); // lanza PedidoNoEncontrado
$pedido->confirmarPago($this->pasarela); // el pedido sabe pagarse; usa EstadoPedido, no "3"
$this->pedidos->guardar($pedido);
$this->notificador->enviar($pedido->cliente(), 'Pedido pagado');
}
}
Las dos versiones hacen lo mismo a ojos del usuario. La diferencia es puro coste futuro: la segunda se testea sin cobrar de verdad (inyectas una PasarelaPago falsa), cambia de proveedor de pago sin tocar la lógica, no repite el IVA, y el controlador se lee como un índice. No es más "bonita": es más barata de cambiar, que es lo único que mide la mantenibilidad. Y no se llegó ahí de golpe, sino refactorizando en pasos pequeños con los tests en verde.
Lo que sacrificas / errores comunes¶
- Sobre-ingeniería en nombre de la "limpieza": aplicar SOLID, patrones e interfaces a un CRUD de cuatro campos. La complejidad que añades hoy es real; el beneficio que persigues es hipotético. YAGNI existe precisamente para frenar esto. Código limpio no es código elaborado: a menudo es código más simple.
- Confundir mantenible con perfecto: perseguir la elegancia absoluta tiene rendimientos decrecientes y a veces negativos. El objetivo es "barato de cambiar para lo que va a pasar", no "irreprochable en una revisión académica".
- Refactorizar sin red: cambiar estructura sin tests es reescribir a ciegas. Si no hay tests, el primer paso del refactor es escribir el test que fija el comportamiento actual, no lanzarse a mover cosas.
- DRY dogmático: unificar cosas que se parecen por casualidad crea acoplamiento entre partes que deberían evolucionar por separado. La abstracción equivocada cuesta más que la duplicación que evitaba.
- Tratar la deuda técnica como excusa o como pecado: ni "ya lo arreglaremos" universal (que nunca llega) ni "todo tiene que estar impecable" (que no sale rentable). Es una decisión de coste, tomada a conciencia y por escrito.
- Reviews que atacan a la persona o que se pierden en preferencias: convierten una práctica de equipo en un campo de minas. Sobre el código, no sobre quien lo escribió; y separa lo que bloquea de lo que es gusto.
Resumen¶
- El código se lee mucho más de lo que se escribe. Optimizar la escritura a costa de la lectura es un mal negocio: la mantenibilidad es economía —el coste del próximo cambio—, no estética.
- Las herramientas base de la legibilidad son concretas: nombres que revelan intención, funciones con un solo propósito a un mismo nivel de abstracción, y sobre todo alta cohesión y bajo acoplamiento, la pareja de la que casi todo lo demás se deriva.
- DRY, KISS, YAGNI y SOLID son heurísticas, no dogmas. Empujan hacia bajo acoplamiento y simplicidad; aplicados a ciegas producen el daño contrario (abstracción prematura, indirección inútil). El criterio manda: abstrae cuando la duplicación ya duele.
- Refactorizar es mejorar la estructura sin cambiar el comportamiento, en pasos pequeños con tests como red. Commits atómicos con el porqué en el mensaje, y code review sobre el código y no la persona, mantienen la historia y el conocimiento vivos en el equipo.
- La deuda técnica legítima es una decisión consciente para entregar antes; se paga donde los intereses son altos (las zonas que tocas a menudo) y se puede dejar donde no. Todo se decide con la cuenta del coste, no con la culpa.
Ejercicios socráticos¶
No busques la respuesta fuera. Razónala con lo de esta lección. Si te atascas, la pista está en el texto.
- Un compañero justifica un método de 300 líneas diciendo "pero funciona y pasa los tests". Con el marco de esta lección, ¿qué le respondes? ¿Qué coste está ignorando y por qué el test no lo mide?
- Tienes dos funciones de 10 líneas casi idénticas en dos módulos distintos. ¿Qué una sola pregunta decide si debes unirlas (DRY) o dejarlas duplicadas? Da un ejemplo de cada respuesta.
- Te piden "aplica SOLID a esta clase". ¿Por qué esa petición está mal planteada? Reformúlala en términos de lo que SOLID persigue de verdad.
- Tu equipo tiene un módulo horrible de código, pero llevas dos años sin tocarlo y no hay planes de hacerlo. ¿Es deuda técnica que hay que pagar? Justifícalo con el concepto de intereses.
- Vas a refactorizar una función para dividirla y, ya de paso, arreglar un bug que has visto. ¿Por qué deberías hacerlo en dos commits (o dos movimientos) separados? ¿Qué pierdes si lo mezclas?
Repaso espaciado¶
Pasa estas a Anki (
repasos.md). Formato: pregunta que obligue a razonar, no a reconocer.
- [ ] ¿Por qué se dice que la mantenibilidad es economía y no estética? Explica el coste que se optimiza mal cuando solo miras "ya funciona".
- [ ] Define cohesión y acoplamiento con un ejemplo de cada uno, y explica por qué "alta cohesión, bajo acoplamiento" reduce el coste de un cambio.
- [ ] ¿Cuándo NO aplicar DRY? Da la regla que distingue duplicación real (de conocimiento) de coincidencia.
- [ ] ¿Qué significa exactamente que un refactoring "no cambia el comportamiento" y por qué eso obliga a tener tests? ¿Por qué no mezclarlo con añadir features?
- [ ] ¿Qué hace que una deuda técnica sea legítima y cuándo decides pagarla en lugar de dejarla? (Habla de conciencia e intereses.)
Para seguir tirando del hilo¶
- Clean Code (Robert C. Martin) — la referencia clásica de nombres, funciones y smells. Léelo con criterio: sus ejemplos son valiosos, su tono dogmático conviene filtrarlo con lo de esta lección.
- Refactoring (Martin Fowler) — el catálogo de olores y las transformaciones seguras para pagarlos; el libro que fundó la disciplina.
- A Philosophy of Software Design (John Ousterhout) — corto y afilado, sobre complejidad, profundidad de módulos y por qué "más pequeño" no siempre es "mejor". El mejor contrapeso a Clean Code.
- The Pragmatic Programmer (Hunt & Thomas) — origen de DRY tal como se pensó de verdad; mucho criterio de oficio.
- Charla: "Simple Made Easy" de Rich Hickey — demoledora sobre la diferencia entre simple y fácil, que es el corazón de KISS.
- Experimento: coge una función tuya de hace seis meses que no recuerdes. Cronométrate cuánto tardas en entender qué hace antes de tocarla. Después refactorízala (nombres, extraer, un propósito) y pídele a un compañero que la lea sin contexto. Su cara y su tiempo son la métrica real de mantenibilidad; el linter no la mide.