Lectura de código real¶
De un vistazo
Materia: Transversal (aplica en todas las capas) · Tiempo de lectura: ~28 min · Requisitos previos: ninguno formal; se enriquece con todo lo demás.
En una frase: leer código ajeno es una habilidad distinta de escribirlo —es investigación con hipótesis, no lectura lineal— y vas a aprender a entrar en una base de código desconocida sin ahogarte: encontrar el punto de entrada, seguir un solo flujo de principio a fin e ignorar deliberadamente todo lo demás.
Por qué esto importa¶
Piensa en tu última semana de trabajo. ¿Cuánto tiempo pasaste tecleando código nuevo y cuánto leyendo código que ya existía —el tuyo de hace tres meses, el de un compañero, el del framework, el de una librería para saber por qué narices devuelve eso? Para casi todo el mundo la proporción está entre 1 a 5 y 1 a 10 a favor de la lectura. Y sin embargo, ¿cuántas horas has dedicado conscientemente a mejorar cómo lees, comparado con las que has dedicado a mejorar cómo escribes?
Ese desajuste es el problema. Escribir se practica sin querer; leer bien, no. Y leer bien es lo que separa a quien tarda dos días en entender por qué un pago se duplica de quien tarda veinte minutos.
Los tres momentos donde esto te la juega:
- Entrar en un proyecto ajeno. Empiezas en un curro nuevo, o heredas el código de alguien que se fue. Cientos de miles de líneas que nadie te va a explicar. Tu velocidad las primeras semanas es tu capacidad de leer código.
- Depurar. El 90% de arreglar un bug es entender el código lo suficiente para localizar dónde miente. El arreglo en sí suele ser una línea. Lo caro es la lectura previa.
- Aprender de código bueno. Los mejores patrones que vas a interiorizar no salen de un tutorial: salen de leer cómo lo resolvió gente muy buena en una base madura (Symfony, el kernel de Linux, la librería que usas a diario). Pero solo si sabes leerla; si no, es ruido.
Nadie te enseña esto en la carrera. Se asume que "ya sabes leer" porque sabes leer prosa. Pero el código no se lee como una novela, y hacerlo así es la causa número uno de que la gente se sienta perdida y estúpida ante una base de código grande. No eres tú: es el método.
Intuición primero: el detective, no el lector¶
Cuando lees una novela, empiezas por la página 1 y avanzas en orden. El autor controla qué sabes en cada momento y en qué orden. La historia se cuenta a sí misma.
El código no. El código es la escena de un crimen, no un relato. Está todo ahí a la vez —cientos de ficheros, sin principio ni final marcados— y no te cuenta su historia ni su porqué. Te cuenta el qué (qué hace la máquina), pero el por qué se decidió así y el cómo encaja con el resto no están escritos en ninguna parte: vivían en la cabeza de quien lo escribió, y esa cabeza ya no está.
graph LR
N["Novela<br/>(prosa)"] -->|"se lee"| NL["en orden, lineal<br/>el autor te guía"]
C["Código<br/>(escena)"] -->|"se lee"| CL["por hipótesis<br/>tú decides dónde mirar<br/>e ignoras el 95%"]
Un detective que llega a la escena no examina cada grano de polvo del edificio entero. Se haría viejo. Forma una hipótesis ("entraron por la ventana"), busca la evidencia concreta que la confirma o la tumba, y descarta agresivamente todo lo que no viene al caso. Avanza por preguntas, no por barrido exhaustivo.
Leer código es exactamente eso. La pregunta correcta no es "¿qué hace todo esto?" (imposible y agotador), sino "¿qué hace esta cosa concreta que me importa ahora, y por dónde pasa?". Guarda esta imagen —el detective siguiendo una pista, no el lector devorando páginas— porque toda la lección cuelga de ella.
El detalle¶
1. Por qué leer cuesta tanto (y no es culpa tuya)¶
Antes de las técnicas, conviene entender por qué una base de código desconocida se siente como un muro. Si sabes qué te falta, sabes qué ir a buscar.
El código no cuenta su porqué. Ves if ($user->getCreatedAt() > $threshold). El qué está clarísimo. Pero ¿por qué esa fecha? ¿qué bug de producción de 2019 obligó a meter esa condición? Eso no está en el código. La intención y la historia se pierden en cuanto el autor pulsa guardar. Por eso el historial de git (sección 6) es oro: es lo más cercano a que el código te cuente su porqué.
No hay orden de lectura. Una función llama a otra, que llama a otra, que dispara un evento que escucha algo en otro paquete. El grafo de dependencias no es lineal ni jerárquico limpio: es una red. No existe "la página 1".
La distancia entre el nombre y la realidad. Un método se llama process(). ¿Qué procesa? ¿Cómo? El nombre es una promesa que puede cumplirse o no. En código malo, los nombres mienten. Tienes que verificar, no fiarte.
El volumen. Symfony son cientos de miles de líneas. Ningún cerebro cabe eso. La sensación de "no puedo con esto" viene de intentar, inconscientemente, abarcarlo todo. La cura no es leer más rápido: es leer mucho menos, a propósito.
El error que lo envenena todo: leer pasivamente sin objetivo
Abrir un fichero y ponerte a leerlo de arriba abajo "para enterarte de cómo funciona esto" es la trampa más común y la más estéril. Sin una pregunta concreta, tu cerebro no tiene criterio para decidir qué es importante y qué es ruido, así que lo trata todo como importante, se satura y no retiene nada. Sales de la sesión cansado y con la vaga sensación de haber visto muchas cosas, pero incapaz de responder ninguna pregunta útil. Leer sin hipótesis no es leer: es hacer scroll.
2. La regla de oro: no leas la base entera. Nunca.¶
Que quede grabado porque es contraintuitivo: el objetivo jamás es "entender toda la base de código". Ni el que la escribió la entiende entera a la vez. La tienen troceada en modelos mentales que activan según lo que tocan.
Tu objetivo siempre es más pequeño y más concreto: entender lo justo para responder la pregunta que tienes ahora mismo. "¿Cómo se valida el email en el registro?" "¿Dónde se decide el precio con descuento?" "¿Por qué este endpoint devuelve 403?" Una pregunta con respuesta localizable, no un océano.
graph TB
subgraph MAL["Enfoque que agota"]
direction TB
m["Abro el proyecto<br/>y leo ficheros<br/>hasta 'entenderlo'"]
end
subgraph BIEN["Enfoque de detective"]
direction TB
b1["1 · Pregunta concreta"] --> b2["2 · Punto de entrada"]
b2 --> b3["3 · Sigo UN flujo<br/>e ignoro el resto"]
b3 --> b4["4 · Respondida →<br/>paro"]
end
De aquí sale todo el método de las secciones siguientes. Y el permiso más liberador que te vas a dar: puedes ignorar el 95% del código. De hecho debes. La habilidad no es leer mucho; es decidir qué NO leer y no sentirte culpable por ello.
3. Encontrar el punto de entrada¶
Un flujo hay que empezarlo por algún sitio. El "punto de entrada" es dónde el mundo exterior toca tu código: dónde empieza a ejecutarse lo que te interesa. Localizarlo es el primer movimiento siempre.
Los puntos de entrada típicos, según qué persigas:
- Una petición web → el enrutado. En Symfony, una ruta (
#[Route('/checkout')]) apunta a un método de un controlador. Ese controlador es tu entrada. - Un comando de consola → la clase del comando (en Symfony, la que extiende
Commandy define suname). - Un evento / un job en cola → el listener o el handler que lo procesa.
- Un test que falla → el propio test es un punto de entrada de lujo: te dice exactamente qué función se llama y con qué datos.
- Un mensaje de error → el texto literal del error. Búscalo (sección 4) y aterrizas justo en la línea que lo lanza.
¿Cómo lo encuentras si no lo sabes de memoria? Piensa de fuera hacia dentro: ¿qué acción del usuario o del sistema dispara esto? Una URL, un clic, un cron, un webhook. Eso tiene un nombre buscable (la ruta /checkout, el texto del botón, el nombre del comando). Empiezas por ahí y tiras del hilo hacia adentro.
Truco: haz que el código te diga dónde estás
Si te pierdes sobre qué código se ejecuta de verdad, mete un dump() / var_dump() / un log con un texto único ("AQUI-1") en un sitio sospechoso y dispara la acción. Si aparece, ese código se ejecuta y estás en el flujo. Si no aparece, estás mirando código muerto o la rama equivocada. Verificar en vez de suponer. Esto enlaza con el método de ejecución: solo corre lo que la CPU llega a alcanzar.
4. Seguir UN flujo de principio a fin¶
Este es el corazón de la técnica. Tienes el punto de entrada. Ahora sigue la ejecución paso a paso, como si fueras la CPU, entrando en cada llamada relevante y —esto es lo difícil— saltándote lo que no toca.
El movimiento fundamental es "ir a definición" (go to definition): en tu editor, Ctrl+clic o F12 sobre el nombre de un método te lleva a dónde está definido. Es teletransporte por el grafo de llamadas. Sigues la pista de una función a la siguiente sin perder el hilo. El inverso, "buscar usos / referencias", hace lo contrario: desde una función, ¿quién la llama? Útil para ir hacia atrás ("¿de dónde viene este valor?").
La disciplina está en decidir en cada llamada: ¿entro o la trato como caja negra? No puedes entrar en todo o acabas 40 niveles abajo en las tripas del ORM sin recordar qué buscabas. Criterio:
- Entra si la llamada es parte del flujo que investigas (
$this->calcularPrecio($carrito)cuando buscas cómo se calcula el precio). - No entres —fíate del nombre— si es infraestructura genérica de la que solo te importa el resultado (
$logger->info(...),$this->em->flush(), un getter). Anota mentalmente "esto guarda en BD" y sigue. Si luego resulta relevante, ya entrarás.
graph TB
E["Route /checkout<br/>→ CheckoutController::pay()"] --> A["¿llama a algo?"]
A -->|"calcularTotal() · RELEVANTE"| B["entro → PriceCalculator"]
A -->|"logger->info() · ruido"| X["NO entro · caja negra"]
B --> C["aplicarDescuento()<br/>← aquí vive la respuesta"]
C --> D["respondida la pregunta<br/>→ PARO, ignoro el resto"]
Grep / búsqueda de texto es tu otra herramienta central, sobre todo cuando no tienes un punto de entrada claro o el flujo salta de forma no rastreable con "ir a definición" (eventos, inyección de dependencias, cadenas de texto). Buscas un literal único —un texto de error, una clave de configuración, el nombre de una ruta— y aterrizas donde se usa. Ejemplos reales sobre Symfony:
# ¿Dónde se lanza este error que veo en producción?
grep -rn "Insufficient funds" src/
# ¿Quién escucha el evento de "pedido pagado"?
grep -rn "OrderPaidEvent" src/
# ¿Dónde se define y se consume esta clave de config?
grep -rn "payment_gateway" config/ src/
Combinas las dos: grep para aterrizar de un salto, ir a definición para caminar el flujo una vez has aterrizado. Con estas dos y la disciplina de ignorar, entras en cualquier base de código.
5. Leer los tests como documentación del comportamiento¶
Cuando quieres saber qué se supone que hace una clase —no cómo lo hace, sino qué promete—, su test suele ser la mejor documentación que existe, y por una razón sólida: está verificado. Un comentario puede estar desactualizado y mentir; un test que pasa describe el comportamiento real, o la suite estaría en rojo.
Un test bien escrito te da, gratis:
- Cómo se construye el objeto y qué necesita (el arrange): sus dependencias reales.
- Qué entradas son interesantes, incluidos los casos límite que a ti no se te habrían ocurrido (¿qué pasa con carrito vacío? ¿con cantidad negativa?).
- Qué salida se espera de cada entrada (el assert): el contrato, en ejemplos concretos.
// tests/PriceCalculatorTest.php — leído como documentación
public function testAplicaDescuentoVipEncimaDe100(): void
{
$calc = new PriceCalculator(new VipDiscountPolicy());
$total = $calc->total(new Cart([new Item(120, 1)]), $vipUser);
// ← ESTO es el contrato: VIP + >100€ → 10% de descuento
self::assertSame(108.0, $total);
}
En diez líneas has aprendido qué hace PriceCalculator, cómo se instancia, qué política inyecta y una regla de negocio exacta —sin leer una sola línea de su implementación. Cuando aterrices en una clase que no entiendes, busca su test antes de leer su código. A menudo con eso te basta. (Esto conecta con Testing: los tests no solo previenen regresiones, también son el mejor mapa de intenciones de una base de código.)
6. El historial de git: dónde vive el porqué¶
El código te dice el qué. El porqué —la decisión, el bug que lo forzó, la discusión— vive en la historia. Dos herramientas:
git blamesobre una línea rara ("¿por qué esta condición absurda?") te da el commit que la introdujo. El mensaje de ese commit y, si lo enlaza, la PR/issue asociada suelen contener la explicación que el código no da: "fix: no cobrar dos veces cuando el webhook de Stripe llega duplicado (#4821)". Ahí está el porqué que buscabas.git log -p archivo(ogit log -Lsobre un rango de líneas) te da la evolución: cómo llegó ese fichero a ser lo que es. Muchas veces un trozo confuso se entiende al ver que es la cicatriz de tres parches sucesivos.
# ¿Por qué existe esta línea rara? ¿Quién y por qué la puso?
git blame -L 40,55 src/Payment/Charger.php
# La biografía de un fichero: cómo evolucionó
git log --oneline -- src/Payment/Charger.php
Un buen historial es un buen profesor
Esto también explica por qué merecen la pena los mensajes de commit cuidados y los commits pequeños y atómicos: no son burocracia, son documentación viva del porqué para el próximo detective —que muchas veces serás tú dentro de seis meses, sin recordar nada. Un git blame que aterriza en un commit "wip fixes" no ayuda a nadie. Materia de control de versiones.
7. Leer para aprender ≠ leer para depurar¶
Son dos modos con objetivos distintos. Confundirlos hace que hagas mal los dos.
Leer para aprender patrones. Objetivo: robar buenas ideas. Aquí eliges código maduro y respetado —Symfony es un candidato ideal: años de revisión, patrones consolidados, gente muy buena— y estudias cómo resolvieron un problema concreto que a ti también se te presenta: cómo desacoplan con eventos, cómo estructuran un value object, cómo diseñan una interfaz. Sigues siendo detective (eliges un componente pequeño, no "leo Symfony entero"), pero tu pregunta es de diseño: "¿por qué lo montaron así y qué gano copiándolo?". No leas código malo para aprender: aprenderás malos hábitos.
Leer para depurar. Objetivo: encontrar dónde la realidad se desvía de lo esperado. Aquí no admiras nada; cazas. El método es el científico, no el turístico:
graph LR
H["1 · HIPÓTESIS<br/>'creo que falla<br/>en el cálculo del IVA'"] --> V["2 · VERIFICAR<br/>dump/log/test/breakpoint<br/>justo ahí"]
V -->|"confirmada"| A["3 · ACOTAR<br/>bajo un nivel,<br/>hipótesis más fina"]
V -->|"refutada"| N["descarto esa zona<br/>entera y pruebo otra"]
A --> H
- Hipótesis: una afirmación falsable sobre dónde y por qué falla. "El total sale mal porque el descuento se aplica dos veces." No "algo va mal en pagos" (no es falsable, no sirve).
- Verificar: ve a ese punto exacto y mira la realidad —un
dump, un log, un test que reproduzca el fallo, un breakpoint. No supongas: comprueba. - Acotar: cada verificación parte el problema en dos. Si la hipótesis se confirma, bajas un nivel con una hipótesis más fina. Si se refuta, descartas esa zona entera y pruebas otra región. Es búsqueda binaria sobre el código: en pocos pasos pasas de "falla en algún lugar de 200.000 líneas" a "falla en esta línea".
La diferencia práctica: depurando no lees por leer, lees solo el camino que tu hipótesis actual señala. La hipótesis es lo que te dice qué ignorar. Sin ella, vuelves al scroll pasivo de la sección 1.
Cómo se ve en la práctica¶
Pongámoslo junto con la práctica recomendada: trazar cómo Symfony resuelve una ruta hasta llamar a tu controlador. No para memorizar Symfony, sino para vivir el método.
Pregunta concreta: "cuando llega una petición a /checkout, ¿cómo acaba ejecutándose CheckoutController::pay()?"
- Punto de entrada. Toda petición web entra por un sitio: el front controller
public/index.php. Ahí se crea elKernely se llama a$kernel->handle($request). Entrada localizada en 30 segundos. - Sigo un flujo, ignorando el 95%. "Ir a definición" sobre
handle()me lleva alHttpKernel. Veo que dispara un evento y llama a algo llamado controller resolver. No entro en el sistema de eventos entero (ruido para mi pregunta); sí entro en lo que suena a "resolver qué controlador toca". - Grep para saltar el hueco. El enrutado usa inyección de dependencias y no es obvio a golpe de "ir a definición". Busco el concepto:
Aterrizo en cómo el
RequestStacklleva el atributo_controllerque el router dejó en la petición tras hacer match de la URL. - Los tests confirman el comportamiento. En
vendor/symfony/routing/Tests/encuentro tests del matcher que muestran, con ejemplos, cómo una URL se convierte en['_controller' => '...', 'id' => 42]. En cinco asserts entiendo el contrato del enrutado sin leer su implementación completa. - Respondida → paro. Ya tengo la cadena:
index.php→Kernel::handle→ router hace match y anota_controller→ controller resolver lo instancia → se ejecutapay(). Cierro el editor. No he leído ni el 2% de Symfony y respondo con precisión algo que antes era magia.
Compáralo con el enfoque ingenuo: abrir vendor/symfony/ y ponerse a leer. A la media hora, veinte pestañas abiertas, cero preguntas respondidas y la sensación de ser tonto. El método no te hace más listo; te hace hacer las preguntas en el orden que el código puede responder.
Lo que sacrificas / errores comunes¶
- Leer sin pregunta (el pecado capital). Ya insistido, pero es el 80% de los problemas: si no puedes decir en una frase qué buscas, no estás leyendo, estás vagando. Formula la pregunta antes de abrir el primer fichero.
- Entrar en todas las llamadas. Acabas 30 niveles dentro del framework sin recordar qué investigabas. Disciplina de caja negra: fíate de los nombres de la infraestructura genérica; entra solo en lo que toca tu flujo.
- Fiarte de los nombres en código malo. El reverso: en bases descuidadas,
validate()puede que además guarde en BD y mande un email. No asumas por el nombre en código que no respetas; ahí sí verifica. - Confundir "leo" con "entiendo". Pasar los ojos por las líneas no es comprender. La prueba de que has entendido es que puedes predecir qué hará el código con una entrada dada, o explicárselo a alguien. Si no puedes, no lo has leído: lo has ojeado.
- Ignorar tests y git. Ir directo a la implementación cuando el test te daba el qué en diez líneas y el
git blamete daba el porqué en una. Empieza por los baratos. - Querer entenderlo todo antes de tocar nada. Parálisis. No necesitas el mapa completo para arreglar un bug; necesitas el camino de ese bug. El resto del mapa se dibuja solo, flujo a flujo, con el tiempo.
Resumen¶
- Pasas mucho más tiempo leyendo que escribiendo código, y leer es una habilidad distinta que casi nadie entrena. Entrenarla es de las inversiones más rentables que existen.
- El código es una escena de crimen, no una novela: no te cuenta su porqué ni tiene orden de lectura. Se aborda como detective —por hipótesis y evidencia—, no como lector lineal.
- Nunca leas la base entera. Ten una pregunta concreta, encuentra el punto de entrada, sigue un solo flujo e ignora agresivamente el 95% restante. Saber qué NO leer es la habilidad.
- Herramientas: grep para aterrizar de un salto, "ir a definición" para caminar el flujo, los tests como documentación verificada del qué, y git blame/log para el porqué que el código no cuenta.
- Leer para aprender (código maduro, pregunta de diseño) y leer para depurar (método hipótesis → verificar → acotar, como búsqueda binaria) son modos distintos. Ambos mueren si lees pasivamente y sin objetivo.
Ejercicios socráticos¶
No busques la respuesta fuera. Razónala con el método de esta lección. Si te atascas, la pista está en el texto.
- Un compañero te dice: "para arreglar este bug, primero léete todo el módulo de pagos y así lo entiendes". ¿Por qué es un mal consejo aunque suene responsable? ¿Qué le propondrías en su lugar y por qué sería más rápido?
- Estás depurando y tienes la hipótesis "el precio sale mal porque el descuento se aplica dos veces". Describe los tres pasos exactos que darías a continuación. ¿En qué se diferencia esto de "leer el código de precios hasta encontrar el fallo"?
- Encuentras una condición extraña:
if ($order->getCountry() === 'ES' && $date < '2021-07-01'). El código no explica por qué. ¿Qué dos herramientas usarías para descubrir el porqué, y qué esperas encontrar en cada una? - Tienes que entender qué hace la clase
InvoiceGenerator, de 400 líneas. ¿Por dónde empiezas y por qué no es por la línea 1 de la implementación? ¿Qué te daría más información por minuto invertido? - ¿Por qué "ir a definición" en cada llamada que ves te lleva al desastre, mientras que "ir a definición" selectivo es la técnica central? ¿Qué criterio usas para decidir en cada llamada si entras o la tratas como caja negra?
Repaso espaciado¶
Pasa estas a Anki (
repasos.md). Formato: pregunta que obligue a razonar, no a reconocer.
- [ ] ¿Por qué el código se lee como una escena de crimen y no como una novela? Nombra las dos propiedades del código que lo causan.
- [ ] ¿Cuál es el error más común y estéril al leer código, y por qué satura el cerebro? ¿Cuál es su antídoto en una palabra?
- [ ] Para entrar en una base desconocida: ¿cuáles son los cuatro pasos (pregunta → ... → parar) y qué papel juega "ignorar el 95%"?
- [ ] ¿Por qué los tests son mejor documentación del qué que un comentario, y qué te da un test bien escrito en tres cosas concretas?
- [ ] Distingue leer-para-aprender de leer-para-depurar: objetivo de cada uno y qué método sigue el segundo (los tres pasos del ciclo).
Para seguir tirando del hilo¶
- The Programmer's Brain (Felienne Hermans) — cómo funciona tu cabeza al leer código y cómo entrenarla; base cognitiva de esta lección.
- Your Code as a Crime Scene (Adam Tornhill) — usar el historial de git y forense de código para entender bases grandes; extiende la sección 6.
- Working Effectively with Legacy Code (Michael Feathers) — el manual de entrar en código ajeno y sin tests sin morir en el intento.
- Práctica de gimnasio: coge un componente pequeño de Symfony (resolución de rutas, el sistema de eventos, cómo se construye un servicio) y trázalo de entrada a salida con las técnicas de aquí. Escribe en cinco líneas la cadena de llamadas que descubriste. Repetir esto con distintos componentes es cómo se afila esta habilidad.