Teoría — Funciones, comentarios y la red de seguridad
La sección anterior terminaba con una advertencia: en código sin tests, hasta renombrar da miedo. La historia acaba de mostrar el mecanismo completo del miedo — Júlia arregla un bug real y rompe un informe que no sabía que existía — y también la salida. Este capítulo ordena esa salida en tres partes: qué es de verdad el código legacy y cómo se toca sin morir (tests de caracterización), cómo deben ser las funciones para que mentir sea difícil, y qué papel juegan — y cuál no — los comentarios.
Legacy: la definición de Feathers
Código legacy no significa «código viejo». La definición operativa la dio Michael Feathers en Working Effectively with Legacy Code (2004), el libro que Gabriel le presta a Júlia, y es deliberadamente brutal:
«Para mí, código legacy es simplemente código sin tests.» (traducción propia del prefacio)
La definición es útil porque señala el síntoma que importa: el miedo a cambiarlo. Un código sin tests solo puede modificarse «editando y rezando» — el método oficial de Meridian durante quince años. Y el miedo tiene una dinámica propia que el cuaderno de faro describe con precisión clínica: el miedo produce copias, las copias divergen, la divergencia produce más miedo. El informe de descuentos duplicado no fue un accidente: fue la consecuencia lógica de un sistema donde tocar el original era más peligroso que copiarlo. Cuando veas código duplicado en un sistema viejo, no preguntes «¿quién fue el vago?»; pregunta «¿qué daba tanto miedo?».
Tests de caracterización: fotografiar al dragón
Un test normal codifica lo que el sistema debería hacer. Pero para escribirlo hay que saber qué debería hacer — y en un sistema legacy, muchas veces, nadie lo sabe ya. Feathers propone para estos casos el test de caracterización (characterization test): un test que codifica lo que el sistema hace hoy, sea correcto o no.
El procedimiento, tal como lo aplicaron Júlia y Denís:
- Genera entradas variadas que cubran los caminos del código: casos normales, extremos, raros (las cuarenta y una facturas del tipo 7). No hace falta entender cada camino; hace falta ejercitarlo.
- Captura las salidas exactas que produce el sistema actual y guárdalas como referencia. Cuando la salida es voluminosa (una remesa entera de facturas), a esta variante se la llama golden master: el fichero de referencia contra el que se comparará todo lo posterior.
- Convierte cada par entrada→salida en un test automático. Desde este momento, cualquier cambio que altere cualquier comportamiento enciende una alarma.
- Ahora — y solo ahora — cambia el código. Si tu cambio pretendía alterar 12 comportamientos y se encienden 12 alarmas, perfecto: revísalas y actualiza las referencias. Si se encienden 13, la número 13 acaba de ahorrarte una llamada del cliente.
Un esqueleto en Python del arnés de Júlia, simplificado:
import json
from pathlib import Path
from motor_calculo import calcular_factura # el código legacy, tal cual está
CASOS = Path("casos") # entradas generadas: caso_001.json, caso_002.json...
REFERENCIAS = Path("referencias") # salidas capturadas: caso_001.esperado.json...
def test_caracterizacion():
for caso in sorted(CASOS.glob("caso_*.json")):
entrada = json.loads(caso.read_text())
resultado = calcular_factura(entrada)
referencia = json.loads(
(REFERENCIAS / f"{caso.stem}.esperado.json").read_text()
)
assert resultado == referencia, f"El comportamiento cambió en {caso.name}"
Y la misma idea en JavaScript, con cualquier runner de tests moderno:
import { readFileSync, readdirSync } from "node:fs";
import { calcularFactura } from "./motorCalculo.js"; // el legacy, sin tocar
for (const fichero of readdirSync("casos")) {
test(`caracterización: ${fichero}`, () => {
const entrada = JSON.parse(readFileSync(`casos/${fichero}`, "utf8"));
const referencia = JSON.parse(
readFileSync(`referencias/${fichero.replace(".json", ".esperado.json")}`, "utf8")
);
expect(calcularFactura(entrada)).toEqual(referencia);
});
}
Tres matices que separan al que ha leído un tutorial del que lo ha hecho de verdad:
- Los tests de caracterización consagran bugs, y eso está bien. Su promesa no es «esto es correcto» sino «esto no cambiará sin que lo sepas». Cuando arreglas un bug conocido, esperas que fallen los casos afectados: esa lista de fallos es información valiosísima — es el mapa de todo lo que dependía del comportamiento viejo.
- El determinismo es un prerrequisito. Si el código legacy usa la fecha actual, números aleatorios o el orden de un diccionario, las referencias no serán estables. Parte del trabajo del arnés es fijar esas fuentes de variación (inyectar una fecha fija, una semilla). Guarda esta idea: «las dependencias ocultas de la función son el enemigo» reaparecerá en todo el curso.
- No busques cobertura perfecta; busca cobertura de lo que vas a tocar. Feathers insiste: el objetivo no es testear el sistema entero (imposible en un legacy grande), sino tender red bajo la zona de la obra.
Funciones: pequeñas, una cosa, un nivel
Con la red puesta, empieza la mejora. La unidad de mejora es la función, y el criterio cabe en la frase del cuaderno de faro, que es también la formulación clásica de Clean Code (R. C. Martin, 2008, cap. 3):
Una función debería hacer una cosa, hacerla bien, y no hacer nada más.
«Una cosa» tiene una definición menos vaga de lo que parece: una función hace una cosa cuando todas sus líneas trabajan en el mismo nivel de abstracción. Mira la diferencia en JavaScript:
// Mezcla niveles: política de negocio y aritmética de céntimos en la misma vista
function emitirFactura(pedido, cliente) {
let total = 0;
for (const linea of pedido.lineas) {
let importe = linea.cantidad * linea.precioUnitario;
if (cliente.descuento > 0) {
importe = importe - importe * cliente.descuento;
importe = Math.round(importe * 100) / 100;
}
total += importe;
}
// ...30 líneas más de impuestos, numeración y persistencia
}
// Un nivel por función: cada una se lee como una frase
function emitirFactura(pedido, cliente) {
const lineas = calcularLineas(pedido.lineas, cliente);
const totales = calcularTotales(lineas);
return construirFactura(cliente, lineas, totales);
}
function calcularLineas(lineas, cliente) {
return lineas.map((linea) => calcularImporteDeLinea(linea, cliente));
}
function calcularImporteDeLinea(linea, cliente) {
const bruto = linea.cantidad * linea.precioUnitario;
const conDescuento = aplicarDescuento(bruto, cliente.descuento);
return redondearImporteDeLinea(conDescuento);
}
La versión buena no es más corta — suele ser algo más larga — pero cada función puede leerse, entenderse y testearse por separado, y el nivel alto (emitirFactura) se lee como el índice de un libro. A esto se le llama a veces la regla descendente: el código debería poder leerse de arriba abajo como una narración en la que cada función introduce las del nivel siguiente.
Señales de que una función pide división — todas aparecieron en calc2():
- No puedes nombrarla sin «y» (
validarYCalcularYGuardar). - Tiene secciones separadas por comentarios («// ahora los impuestos») — cada sección es una función que quiere nacer, y el comentario es su nombre provisional.
- Sus variables locales cambian de significado por zonas (
$aux3). - Tiene muchos parámetros (más de tres es sospechoso; grupos de parámetros que siempre viajan juntos —
importe, moneda— son un objeto queriendo nacer, como verás en la sección 5). - Banderas booleanas:
calcular(pedido, true)obliga al lector a memorizar qué diablos significatrue, y confiesa que la función hace dos cosas (una por rama). Dos funciones con nombre honesto casi siempre lo resuelven.
Y el matiz que los seniors discuten con razón: pequeña es consecuencia, no objetivo. El fetichismo de «ninguna función de más de N líneas» produce a veces indirección laberíntica — cuarenta funciones de dos líneas que se llaman en cadena. El criterio rector es el nivel de abstracción único y el nombre honesto; el tamaño baja solo.
Comentarios: la verdad incómoda
La posición clásica de Clean Code escandaliza la primera vez que se oye: todo comentario es, en cierto sentido, un fracaso — el fracaso de no haber podido expresarlo en el código. Es deliberadamente provocadora, pero su núcleo es sólido y la historia de Meridian lo ilustra: los comentarios mienten con el tiempo. El código se mantiene porque se ejecuta; el comentario no se ejecuta, así que nadie nota cuando queda obsoleto. El bloque de cabecera de facturacion.php — «mantenido por: [ver wiki]», con la wiki borrada — es un fósil típico.
La política práctica:
- Antes de comentar, intenta expresarlo en código.
// comprueba si puede facturarsesobre unifcríptico se convierte en una funciónpuedeFacturarse()y el comentario muere de éxito. La mayoría de los comentarios explicativos son nombres que no encontraron su sitio. - Los comentarios valiosos explican el porqué, nunca el qué. El qué ya lo dice el código. Un buen comentario documenta la decisión invisible:
// Redondeamos al total, no por línea: acta de la reunión con Vesta, 2026-09-14. Ese comentario no puede sustituirse por código, porque su contenido no está en el código: está en el mundo. - Comentarios legítimos: advertencias de consecuencias («esto tarda 40 minutos con datos reales»), TODO honestos y fechados, aclaración de un algoritmo genuinamente complejo, y documentación de API pública (docstrings/JSDoc) — que es interfaz, no implementación.
- Comentarios a eliminar sin piedad: código comentado (para eso está el control de versiones; el código comentado es basura que nadie se atreve a tirar), diarios de cambios en cabecera (para eso está
git log), y el ruido (i++; // incrementa i).
Las cuatro reglas del diseño simple
Cierra el capítulo la síntesis más compacta que existe de todo lo anterior. Kent Beck las formuló en los 90 (Extreme Programming); Martin Fowler las documenta en su bliki (entrada BeckDesignRules, martinfowler.com). Un diseño es simple cuando, por este orden de prioridad:
- Pasa todos los tests. Primero funciona — y se sabe que funciona. Sin esto, lo demás es decoración.
- Revela la intención. Cualquier lector entiende qué pretende.
- No se repite (cada pieza de conocimiento vive en un solo lugar — la copia del cálculo de descuentos violaba exactamente esto).
- Tiene el mínimo de elementos. Nada de estructura especulativa «por si acaso».
Fíjate en que el orden zanja el debate de la historia: la red de tests va antes que la limpieza, porque es la regla 1; y la duplicación (regla 3) se elimina después de tener la red, no antes. Júlia y Denís, sin saberlo, siguieron las cuatro reglas en orden.
Para llevar
- Legacy = código sin tests (Feathers). El síntoma que importa es el miedo; el miedo produce duplicación y la duplicación, divergencia silenciosa.
- Test de caracterización: codifica lo que el sistema hace hoy, bugs incluidos. No promete corrección; promete que nada cambiará sin que lo sepas. Golden master: la referencia capturada para salidas voluminosas.
- Red primero, cambio después. La lista de tests que fallan tras un cambio es el mapa exacto de sus efectos.
- Una función, una cosa = todas sus líneas al mismo nivel de abstracción. Señales de división: nombres con «y», secciones comentadas, variables camaleónicas, banderas booleanas, parámetros en manada.
- Comentarios: el qué se expresa en código; el porqué (decisiones, contexto del mundo) es el único contenido que justifica un comentario. El código comentado se borra.
- Cuatro reglas del diseño simple (Beck), en orden: pasa los tests, revela intención, no se repite, mínimo de elementos.
Para profundizar
- Michael Feathers, Working Effectively with Legacy Code (2004) — el manual definitivo para tocar sistemas sin red. De pago; su influencia justifica cada euro.
- Martin Fowler, BeckDesignRules y SelfTestingCode en martinfowler.com (gratuitos).
- 97 Things Every Programmer Should Know (CC BY-NC-SA 3.0): los capítulos Comment Only What the Code Cannot Say (Kevlin Henney) y The Golden Rule of API Design complementan este capítulo.