Módulo 2: Entender y fijar el legacy
Trabajar con código que da miedo y no tiene tests
Descripción
En la lección anterior viste la técnica —el characterization test— y la viste atrapar una regresión de un centavo. Esta lección da un paso atrás para entender por qué esa red es imprescindible, y lo hace desde la definición que ordena todo el oficio de modernizar. Michael Feathers, en Working Effectively with Legacy Code, propone una definición que al principio incomoda: código legacy es, simplemente, código sin tests. No habla de antigüedad, ni de lenguaje viejo, ni de arquitectura anticuada. Un código que escribiste ayer, en el framework más moderno, es legacy si no tiene tests —porque no puedes cambiarlo con confianza—. Y a la inversa: un sistema de veinte años con una buena red de tests no da miedo, porque cada cambio te dice de inmediato si rompiste algo. Lo que hace "legacy" a un código no es su edad; es que cambiarlo es cambiarlo a ciegas.
De esa definición sale la paradoja que hace tan difícil empezar. Para cambiar código con seguridad, necesitas tests que te avisen si rompes algo. Pero para poner tests sobre código legacy, muchas veces necesitas cambiarlo primero —romper una dependencia, extraer una función, abrir una costura para poder inyectar una prueba—. O sea: para testear necesitas cambiar, y para cambiar con seguridad necesitas testear. Es un círculo, y Feathers dedica un libro entero a cómo salir de él sin apostar el sistema. La salida no es "reescribe hasta que sea testeable" —eso es el rewrite del módulo 1—; es una serie de técnicas para insertar la red con el mínimo cambio riesgoso posible. Este módulo es tu versión práctica de esas técnicas, aplicada al catálogo de Mercado.
Conexión con el módulo. La lección 1 te mostró la red funcionando; esta lección explica por qué no puedes modernizar sin ella, midiendo el costo del cambio a ciegas. Es la justificación de todo lo que sigue: la 3 enseña a escribir la red (el characterization test), la 4 a insertarla donde una dependencia lo impide (el seam), la 5 a escalarla (golden master). Aquí instalamos la convicción de que el primer trabajo frente a código legacy no es cambiarlo, es fijarlo. La frontera con el ecosistema de Testing se mantiene: no vamos a discutir la teoría de qué es un test o cómo se organiza una suite; vamos a usar el test como red de seguridad para migrar. Y la mecánica de desviar tráfico —una vez que tienes la red— es el módulo 3.
Una analogía: el trapecista sin red
Mira a dos trapecistas hacer exactamente el mismo número, el mismo salto triple, con la misma técnica impecable. Uno trabaja sobre una red; el otro, sin red. Mientras todo sale bien —y casi siempre sale bien— los dos números se ven idénticos. El público ni nota la diferencia. La red no cambia en nada el salto cuando el salto sale perfecto. Aquí está lo engañoso: la ausencia de red es invisible el 99% del tiempo. Solo el día que el trapecista se resbala —y tarde o temprano alguien se resbala— la red deja de ser un detalle decorativo y se vuelve la diferencia entre "un susto y a seguir" y "el fin de la carrera". La red no hace mejor el salto; hace sobrevivible el error.
Cambiar código sin tests es el trapecista sin red. Mientras tus cambios salgan bien, el código sin tests se ve igual de sano que el código con tests —de hecho, sin la red trabajas más rápido, porque no hay tests que correr ni que mantener—. La diferencia no aparece cuando aciertas; aparece el día que te resbalas: cuando un cambio "obvio" rompe algo que no viste. Con la red, ese resbalón es un test rojo en tu máquina, antes de mergear. Sin la red, es una orden mal cobrada en producción, descubierta semanas después por el equipo de finanzas. El mismo error, dos costos radicalmente distintos. Y como el legacy de Mercado se toca todo el tiempo —el negocio no se detiene—, la pregunta no es si alguien se va a resbalar, sino cuándo. La red decide qué pasa ese día.
Por eso la definición de Feathers pone el dedo en la llaga: no importa qué tan bien escrito esté el código ni qué tan bueno sea el equipo. Sin la red, cada cambio es un salto sin red, y el costo del primer resbalón se paga en producción.
Ejemplo trabajado: un "arreglo" inofensivo que toca el 88% de las órdenes
Vamos a medir el costo del cambio a ciegas con un caso realista y pequeño. Un desarrollador nuevo entra al código del catálogo, ve que el precio redondea hacia abajo en cada paso (_floor_cents) y piensa, con toda razón aparente: "esto está mal, debería redondear al centavo más cercano, como manda cualquier libro de contabilidad". Es un cambio de una línea. Se ve inofensivo. Vamos a soltarlo sobre un lote de 200 órdenes reales del catálogo —primero sin red, después con red— y a contar qué pasa.
import math
import random
TAX_RATE = 0.16
CATEGORY_DISCOUNT = {"electronics": 0.05, "books": 0.10, "toys": 0.08, "grocery": 0.00}
BULK_MIN_QTY, BULK_DISCOUNT, GOLD_LOYALTY_DISCOUNT = 10, 0.05, 0.03
def _floor_cents(a):
return math.floor(a * 100) / 100
def legacy_catalog_price(item, rounder=_floor_cents):
# 'rounder' es el punto que un dev va a "mejorar". Por defecto: el de HOY.
subtotal = rounder(item["unit_price"] * item["quantity"])
price = rounder(subtotal * (1 - CATEGORY_DISCOUNT.get(item["category"], 0.0)))
if item["quantity"] >= BULK_MIN_QTY:
price = rounder(price * (1 - BULK_DISCOUNT))
price = rounder(price * (1 - item.get("coupon_percent", 0.0)))
price = rounder(price * (1 + TAX_RATE))
if item.get("loyalty") == "gold":
price = rounder(price * (1 - GOLD_LOYALTY_DISCOUNT))
return price
def _round_half(a):
# El "arreglo" que parece inofensivo: redondear al centavo mas cercano.
return round(a, 2)
# --- Un lote fijo de la vida real del catalogo (semilla fija = reproducible) ---
random.seed(42)
CATEGORIES = list(CATEGORY_DISCOUNT)
BATCH = []
for i in range(200):
BATCH.append({
"sku": f"SKU-{i:03d}",
"unit_price": round(random.uniform(5, 200), 2),
"quantity": random.randint(1, 15),
"category": random.choice(CATEGORIES),
"coupon_percent": random.choice([0.0, 0.0, 0.10, 0.15]),
"loyalty": random.choice([None, None, "gold"]),
})
# El "como funciona hoy": lo que el legacy cobra en este momento.
TODAY = {it["sku"]: legacy_catalog_price(it) for it in BATCH}
print("=== Escenario A: el dev cambia el redondeo SIN red de seguridad ===")
changed = []
total_delta = 0.0
for it in BATCH:
before = TODAY[it["sku"]]
after = legacy_catalog_price(it, rounder=_round_half) # el "arreglo"
if after != before:
changed.append((it["sku"], before, after))
total_delta += after - before
print(f"Ordenes en el lote: {len(BATCH)}")
print(f"Ordenes cuyo precio CAMBIO: {len(changed)} "
f"({100*len(changed)//len(BATCH)}% del lote)")
print(f"Desviacion total de ingresos: {round(total_delta, 2)} "
f"(por un cambio 'inofensivo')")
print("Primeras 5 ordenes afectadas (nadie las revisa; se van a produccion):")
for sku, before, after in changed[:5]:
print(f" {sku}: {before} -> {after} (diff={round(after-before, 2)})")
print("Sin tests, este cambio se despliega en verde. El error aparece semanas")
print("despues, cuando finanzas no cuadra el corte del mes.\n")
print("=== Escenario B: el MISMO cambio, ahora con red de seguridad ===")
def characterization_suite(price_fn):
fails = 0
for it in BATCH:
if price_fn(it) != TODAY[it["sku"]]:
fails += 1
return fails
fails = characterization_suite(lambda it: legacy_catalog_price(it, rounder=_round_half))
print(f"characterization_test sobre las {len(BATCH)} ordenes del lote:")
print(f" {len(BATCH) - fails} passed, {fails} failed ==> "
f"{'VERDE' if fails == 0 else 'ROJO'} "
f"({'sin cambios' if fails == 0 else f'{fails} precios cambiaron'})")
print("La red convierte un desastre silencioso de semanas en un ROJO inmediato,")
print("antes de que una sola orden real se vea afectada.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
=== Escenario A: el dev cambia el redondeo SIN red de seguridad ===
Ordenes en el lote: 200
Ordenes cuyo precio CAMBIO: 176 (88% del lote)
Desviacion total de ingresos: 3.51 (por un cambio 'inofensivo')
Primeras 5 ordenes afectadas (nadie las revisa; se van a produccion):
SKU-000: 138.39 -> 138.4 (diff=0.01)
SKU-001: 60.33 -> 60.34 (diff=0.01)
SKU-002: 43.76 -> 43.8 (diff=0.04)
SKU-003: 530.57 -> 530.58 (diff=0.01)
SKU-004: 1582.96 -> 1582.99 (diff=0.03)
Sin tests, este cambio se despliega en verde. El error aparece semanas
despues, cuando finanzas no cuadra el corte del mes.
=== Escenario B: el MISMO cambio, ahora con red de seguridad ===
characterization_test sobre las 200 ordenes del lote:
24 passed, 176 failed ==> ROJO (176 precios cambiaron)
La red convierte un desastre silencioso de semanas en un ROJO inmediato,
antes de que una sola orden real se vea afectada.
Lee los dos escenarios lado a lado, porque son el mismo cambio con dos destinos opuestos.
En el escenario A —sin red— el cambio de una línea toca 176 de 200 órdenes: el 88% del lote. Fíjate en el tamaño de cada cambio individual: un centavo, tres centavos, cuatro centavos. Ninguna orden se rompe de forma ruidosa; ningún test truena porque no hay tests; el sistema sigue corriendo. El desarrollador ve todo verde (no hay nada que se ponga rojo) y despliega, convencido de que "arregló" el redondeo. Y ahí empieza el problema, porque el cambio sí alteró el 88% de los precios —de forma diminuta, invisible orden por orden, pero sistemática—. La desviación total de ingresos del lote es de 3.51, y ese lote es solo una muestra: en el volumen real del catálogo, esos centavos se acumulan hasta que el corte contable del mes no cuadra, y alguien en finanzas pasa una semana buscando de dónde salió el descuadre —que resultó ser un cambio de una línea desplegado hace tres semanas por alguien que creía estar arreglando algo—.
En el escenario B —con red— es el mismo cambio, exactamente el mismo—, pero ahora hay un characterization test que fijó los 200 precios de hoy. El resultado: 176 fallos, rojo inmediato. El desarrollador corre la suite antes de mergear, la ve encenderse en rojo, y en ese segundo aprende lo que en el escenario A tardó tres semanas y una investigación de finanzas en descubrir: "mi cambio de redondeo altera el 88% de los precios". La red no le prohíbe el cambio —quizá el negocio sí quiere redondear al centavo más cercano—; le hace visible el impacto, aquí y ahora, para que sea una decisión consciente y no un accidente. Convirtió un desastre silencioso de semanas en un rojo de un segundo.
Ese es el valor de la red, medido: no es que evite que rompas cosas —a veces vas a querer cambiar comportamiento, y está bien—; es que hace imposible cambiar comportamiento sin darte cuenta. Sin ella, el 88% se fue en silencio. Con ella, el 88% te grita.
Profundización: por qué "no truena" es un criterio peligroso
El escenario A revela algo más profundo que un descuadre contable: revela que "correr el sistema y ver si truena" no detecta las regresiones que importan. Truenan las fallas ruidosas —una excepción, un crash, una página en blanco—. Pero las regresiones más caras del legacy son silenciosas: un precio que cambia por un centavo, un caso borde que ahora se calcula distinto, una regla que dejó de aplicarse para cierto segmento. Nada de eso truena. Todo eso se despliega en verde y se descubre tarde, cuando el daño ya se acumuló.
Esto conecta con la paradoja del cambio que abrió la lección. Un equipo sensato dice: "no voy a tocar el catálogo sin tests, primero pongo tests". Bien. Pero al intentar poner un test descubre que la función de precio consulta una global (TAX_RATE), lee el reloj del sistema para la vigencia del cupón, y quizá golpea la base de datos para traer el descuento de categoría. No puede escribir un test determinista sobre eso sin cambiar algo primero —parametrizar la global, inyectar el reloj, romper la dependencia de BD—. Y para cambiar esas cosas con seguridad... necesitaría tests. El círculo de Feathers, en carne y hueso.
La salida —que las lecciones 3 y 4 desarrollan— no es reescribir hasta que sea testeable. Es insertar la red con el mínimo cambio riesgoso posible. Primero fijas lo que sí puedes fijar sin tocar nada (los casos deterministas, como hicimos con el lote de 200). Después, para lo que consulta el reloj o la global, abres una costura —un seam— con un cambio quirúrgico y de bajo riesgo (una parametrización con default que no rompe a los llamadores viejos), y ahí sí pones el test. El orden es siempre el mismo, y es el opuesto al instinto:
Instinto (peligroso) Disciplina de legacy (segura)
────────────────────────── ────────────────────────────────
1. Veo codigo feo 1. Fijo lo que puedo sin tocar nada
2. Lo "arreglo" 2. Abro un seam donde haga falta
3. Corro a ver si truena 3. Fijo el resto con el seam
4. Si no truena, listo 4. AHORA cambio, con la red puesta
5. La red decide si rompi algo
Fíjate en que el instinto pone el cambio primero y la verificación al final (y débil: "si no truena"). La disciplina pone la verificación primero (la red) y el cambio al final, con la red ya puesta para juzgarlo. Todo el módulo es entrenar ese reordenamiento.
Y hay una razón económica para que la red preceda al cambio, no solo una razón de seguridad. El costo de una regresión no es fijo: crece con el tiempo que tarda en descubrirse. Un cambio de comportamiento atrapado por la red antes de mergear cuesta minutos —lees el rojo, entiendes qué tocaste, decides—. El mismo cambio descubierto en el corte contable del mes cuesta días —una investigación de finanzas, el rastreo hasta el commit culpable, la corrección, y a menudo la reparación de los datos ya cobrados mal—. Y si escapa hasta un tercero que concilia (como el socio de lealtad que verás en la lección 6), cuesta semanas y credibilidad. La red no solo evita el error; lo mueve al momento en que es más barato de arreglar. Por eso "poner tests después, cuando tenga tiempo" es una falsa economía: el tiempo que ahorras al no tender la red hoy lo pagas multiplicado el día que una regresión silenciosa se descubre lejos de su origen.
Errores comunes
Confundir "código nuevo" con "código no-legacy". Qué pasa: el equipo trata como legacy solo lo viejo —el módulo escrito hace diez años en el estilo antiguo— y considera "moderno y sano" al código reciente, aunque tampoco tenga tests. Por qué pasa: la palabra "legacy" evoca antigüedad, así que asociamos el riesgo con la fecha del commit. Cómo detectarlo: pregunta "si cambio esta función, ¿algo me avisa de inmediato si rompí su comportamiento?". Si la respuesta es no, es legacy —lo hayas escrito ayer o hace una década—. Cómo corregirlo: adopta la definición de Feathers en la práctica: legacy = sin red de tests. Un microservicio flamante sin characterization tests da exactamente el mismo miedo de tocar que el monolito viejo; la modernidad del stack no compra ninguna seguridad. La seguridad la compra la red, y solo la red.
Creer que "correr el sistema a ver si truena" es una red de seguridad. Qué pasa: en vez de fijar el comportamiento, el equipo cambia el código, levanta el sistema, hace clic en un par de pantallas, no ve errores y despliega. Por qué pasa: es rápido, no requiere escribir nada, y da una sensación de haber "verificado". Cómo detectarlo: si la única verificación tras un cambio es una prueba manual del camino feliz, las regresiones silenciosas están garantizadas a pasar. Cómo corregirlo: recuerda el escenario A —el cambio de redondeo no hizo truenar nada y aun así alteró el 88% de los precios—. "No truena" solo descarta las fallas ruidosas; el legacy hace daño de forma silenciosa. Una red de verdad compara la salida actual contra la fijada, para todos los casos que te importan, y te dice si algún número cambió aunque el sistema no se haya quejado.
Querer poner tests "perfectos" antes de tocar nada, y paralizarse. Qué pasa: el equipo decide que antes de modernizar hay que tener una suite de tests completa y bien diseñada del catálogo, y como eso es enorme, nunca empieza. Por qué pasa: la paradoja del cambio asusta —"para testear bien tendría que refactorizar medio módulo"— y el perfeccionismo se disfraza de prudencia. Cómo detectarlo: si llevas semanas "preparando el terreno para poder testear" y no has fijado ni un solo comportamiento, estás paralizado. Cómo corregirlo: no necesitas la suite perfecta; necesitas la red suficiente para el cambio que vas a hacer. Fija primero lo que puedas sin tocar nada —los casos deterministas—, que ya te da cobertura real, y abre seams solo donde el cambio concreto lo exija. La red crece incrementalmente, igual que la migración: empiezas con la rebanada que vas a tocar, no con el sistema entero. La lección 8 muestra que fijar el catálogo completo no requiere reescribirlo: requiere muestrear sus salidas.
Ejercicios
Ejercicio 1 — ¿Es legacy? Para cada caso, di si es "legacy" según la definición de Feathers (código sin red que te avise al cambiarlo) y justifica en una frase: (a) el módulo payments de Mercado, escrito hace 8 años, sin un solo test; (b) un microservicio de notificaciones que un equipo terminó la semana pasada en el framework más moderno, sin tests; (c) una librería de utilidades de Mercado con 400 tests que cubren todos sus casos; (d) un script de 30 líneas que corre una vez y se borra.
Ver solución
- (a)
paymentsde 8 años sin tests → legacy. El caso de manual: viejo y sin red. Cambiarlo es cambiarlo a ciegas. La antigüedad agrava (nadie recuerda por qué hace lo que hace) pero lo que lo hace legacy es la ausencia de red, no la edad. - (b) Microservicio flamante sin tests → legacy, aunque duela admitirlo. Este es el caso que rompe la intuición. El stack es modernísimo y el código se escribió la semana pasada, pero si no tiene tests, cambiarlo mañana es cambiarlo a ciegas: no hay nada que te avise si rompes su comportamiento. Por la definición de Feathers, es legacy desde el día uno. La modernidad del framework no compró ninguna seguridad.
- (c) Librería con 400 tests que cubren sus casos → no-legacy. Aquí sí hay red. Puedes cambiar la implementación y, si rompes un comportamiento, un test se pone rojo de inmediato. Puede tener código feo por dentro, pero no da miedo tocarlo, que es justo lo que la definición mide.
- (d) Script de 30 líneas de un solo uso → zona gris, tirando a "no importa". Técnicamente no tiene red, así que sería legacy; pero como corre una vez y se borra, no hay futuro en el que alguien lo cambie a ciegas y pague el costo. La definición de Feathers importa porque el legacy se toca en el tiempo; un script desechable no se toca. No vale la pena ponerle red. (Contrasta con el catálogo, que se toca todas las semanas porque el negocio no se detiene.)
Ejercicio 2 — El costo del cambio silencioso. En el escenario A, el cambio de redondeo alteró 176 de 200 órdenes por centavos, y "no tronó" nada. Explica por qué "no tronó" es exactamente lo que hace peligroso a este tipo de regresión, comparado con un cambio que sí hubiera lanzado una excepción visible. ¿Cuál de los dos —el silencioso o el ruidoso— se descubre antes, y por qué eso importa para el costo?
Ver solución
Un cambio ruidoso —uno que lanza una excepción, deja una página en blanco o tira el proceso— es, paradójicamente, el menos peligroso: se descubre casi de inmediato, porque alguien lo ve fallar (un usuario, un monitor, el propio desarrollador al probar). El daño se detecta cerca del momento en que se introdujo, así que es barato de rastrear y arreglar: sabes qué cambiaste hoy.
Un cambio silencioso como el del redondeo es mucho más caro precisamente porque no truena. El sistema sigue corriendo, las páginas cargan, nadie ve un error. El daño —176 precios ligeramente distintos— se acumula invisible durante semanas, hasta que un síntoma lejano y agregado (el corte contable que no cuadra) obliga a una investigación. Y para entonces el rastro está frío: el descuadre no apunta a "el commit de redondeo del 3 de marzo"; apunta a "algo, en algún lado, está cobrando mal desde hace un tiempo". Rastrear un efecto silencioso y acumulado hasta su causa cuesta órdenes de magnitud más que arreglar una excepción del día.
Por eso "no truena" es un criterio peligroso: te da confianza justo en los casos donde deberías desconfiar más. La red de seguridad existe para volver ruidosas las regresiones silenciosas —convertir el descuadre de dentro de tres semanas en un test rojo de ahora mismo—.
Ejercicio 3 — Rompe la paradoja. Quieres poner un characterization test sobre la función de descuento de envío de Mercado, pero descubres que consulta datetime.now() para saber si aplica una promoción de temporada, y por eso su salida cambia según el día en que corras el test. Un compañero propone: "entonces no se puede testear, mejor lo dejamos así". Explica por qué esa conclusión es falsa, y describe en términos generales (sin código todavía) qué cambio mínimo te permitiría fijar el comportamiento. ¿Por qué ese cambio es de bajo riesgo?
Ver solución
La conclusión "no se puede testear" es falsa: confunde "la función tiene una dependencia que la hace no determinista" con "la función es intocable". La dependencia del reloj es exactamente el tipo de obstáculo que la disciplina de legacy sabe rodear —es la paradoja del cambio, y tiene salida—.
El cambio mínimo es abrir una costura (un seam, que la lección 4 desarrolla) alrededor de la dependencia del reloj: en vez de que la función llame a datetime.now() por dentro, hacer que reciba la fecha como un parámetro con un valor por defecto que preserve el comportamiento viejo (algo como def shipping_discount(order, today=None), y si today es None, usa datetime.now()). Con eso, el test puede pasar una fecha fija y el resultado se vuelve determinista y fijable; los llamadores viejos, que no pasan today, siguen comportándose idéntico.
Ese cambio es de bajo riesgo por dos razones. Primero, no altera el comportamiento observable: para todo el que ya la llama sin el parámetro nuevo, la función hace exactamente lo mismo que antes (el default reproduce la llamada al reloj). Segundo, es un cambio pequeño y local —agregar un parámetro opcional—, no una reescritura. Es el "mínimo cambio riesgoso posible" del que hablaba la profundización: lo justo para poder poner la red, sin apostar el sistema. Una vez puesta la red, ya puedes cambiar la lógica interna con confianza. Romper la paradoja no es reescribir; es abrir la costura justa.
Resumen y siguiente paso
En esta lección instalaste la definición que ordena todo el oficio: código legacy es código sin tests, sin importar su edad, porque lo que da miedo no es la antigüedad sino la imposibilidad de cambiarlo sin cambiarlo a ciegas. Viste, con el trapecista, que la ausencia de red es invisible mientras aciertas y decisiva el día que te resbalas. Y lo mediste: un "arreglo" de redondeo de una sola línea alteró el 88% de las órdenes del catálogo sin hacer truenar nada —un desastre silencioso que en producción se paga semanas después—, mientras que el mismo cambio, con una red de characterization tests, se encendió en rojo al instante con 176 fallos. La red no te prohíbe cambiar; te hace imposible cambiar sin darte cuenta.
También enfrentaste la paradoja del cambio —para testear hay que cambiar, para cambiar hay que testear— y viste su salida: insertar la red con el mínimo cambio riesgoso, fijando primero lo determinista y abriendo seams solo donde haga falta. Antes de avanzar deberías poder: aplicar la definición de Feathers para clasificar cualquier código como legacy o no; explicar por qué "no truena" no es una red; y describir por qué las regresiones silenciosas cuestan más que las ruidosas.
La lección 3 baja al detalle de cómo se escribe esa red: el characterization test paso a paso. Vas a ver la trampa que atrapa a casi todos la primera vez —adivinar el valor "correcto" en vez de capturar el valor real del legacy— y el flujo de tres pasos para fijar el comportamiento actual, rarezas incluidas, incluso cuando no entiendes por qué el código hace lo que hace.
Recursos
- Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004), prefacio y cap. 1-2 — el origen de la definición "legacy = código sin tests" y de la paradoja del cambio (el "legacy code dilemma"). La base directa de esta lección. En inglés.
- Martin Fowler, "Legacy Seam" — martinfowler.com/bliki/LegacySeam.html. Cómo abrir una costura para insertar tests en código que no fue diseñado para ser testeable: la salida a la paradoja que esta lección plantea y que la lección 4 desarrolla. En inglés.
- Michael Feathers, "The Deep Synergy Between Testability and Good Design" — charla/ensayo sobre por qué el código difícil de testear suele ser también difícil de cambiar, y por qué la red y el buen diseño van de la mano. Buen complemento conceptual. En inglés.
- Nicolas Carlo, Understanding Legacy Code — understandlegacycode.com. Blog práctico y moderno sobre técnicas para domar código legacy sin tests, muy en la línea de Feathers, con ejemplos concretos de cómo empezar. En inglés.