🎯 Meta: escribir código que se pueda leer, cambiar y mantener meses después sin maldecir. El código se escribe una vez pero se lee cientos. La diferencia entre un junior y un senior no es que el senior sepa más trucos, sino que escribe código simple y claro.
10.1 · Clean Code: los fundamentos
Nombres que dicen la verdad
# ❌ Malo
d = 30 # ¿qué es d?
def calc(x, y): ... # ¿calcula qué?
lst = get() # ¿lista de qué?
# ✅ Bueno
dias_de_gracia = 30
def calcular_total_con_igv(subtotal, tasa_igv): ...
usuarios_activos = obtener_usuarios_activos()🧠 Regla: el nombre debe responder qué es o qué hace sin tener que leer el código. Si necesitas un comentario para explicar una variable, probablemente el nombre está mal.
Funciones pequeñas que hacen UNA cosa
# ❌ Una función que hace de todo (300 líneas, imposible de testear)
def procesar_pedido(pedido):
# valida... calcula impuestos... aplica descuentos...
# guarda en BD... envía email... genera factura... registra log...
# ✅ Cada paso, su función
def procesar_pedido(pedido):
validar(pedido)
total = calcular_total(pedido)
guardar(pedido, total)
notificar(pedido)Señales de que una función es demasiado grande: no cabe en la pantalla, tiene muchos if anidados, cuesta ponerle un nombre (porque hace varias cosas), o no sabes cómo testearla.
Otras reglas de oro
| Regla | Por qué |
|---|---|
| DRY (Don't Repeat Yourself) | Duplicar código = duplicar bugs. Extrae lo repetido. |
| KISS (Keep It Simple) | La solución simple casi siempre gana. No sobre-ingenierices. |
| YAGNI (You Aren't Gonna Need It) | No construyas para un futuro imaginario. Resuelve el problema de hoy. |
| Boy Scout Rule | Deja el código más limpio de como lo encontraste. |
| Fail Fast | Valida y falla pronto, con mensajes claros, no dejes que el error se propague. |
⚠️ Cuidado con el DRY mal entendido: dos trozos de código que casualmente se parecen hoy pero cambian por razones distintas no deben unirse. "Duplicación" real es la misma decisión repetida, no las mismas líneas. Un abuso del DRY crea abstracciones prematuras peores que la duplicación.
Comentarios: los justos
# ❌ Comentario que repite el código (ruido)
i = i + 1 # suma 1 a i
# ❌ Comentario que compensa un mal nombre
d = 30 # días de gracia antes de suspender
# ✅ Comentario que explica el PORQUÉ (lo que el código no puede decir)
# El proveedor de pagos exige reintentar máximo 3 veces por su política anti-fraude.
MAX_REINTENTOS = 3🧠 El mejor comentario es el que no necesitas porque el código se explica solo. Comenta el porqué (decisiones, restricciones externas, workarounds), nunca el qué (eso lo dice el código).
10.2 · SOLID: los 5 principios
SOLID son 5 principios de diseño orientado a objetos que hacen el código flexible y mantenible. Los verás con ejemplos en varios lenguajes del libro.
S — Single Responsibility Una clase, una razón para cambiar
O — Open/Closed Abierto a extensión, cerrado a modificación
L — Liskov Substitution Un hijo debe poder sustituir a su padre sin romper
I — Interface Segregation Interfaces pequeñas y específicas
D — Dependency Inversion Depende de abstracciones, no de concrecionesS — Single Responsibility Principle (Responsabilidad única)
Una clase debe tener una sola razón para cambiar.
// ❌ Esta clase hace 3 cosas: lógica, persistencia y notificación
class Usuario {
registrar() {
// valida datos
// guarda en la base de datos
// envía email de bienvenida
}
}
// ✅ Cada responsabilidad, su clase
class RegistroUsuario { // orquesta
constructor(
private repo: UsuarioRepositorio, // persistencia
private mailer: ServicioEmail, // notificación
) {}
registrar(datos: DatosRegistro) {
const usuario = Usuario.crear(datos); // lógica de dominio
this.repo.guardar(usuario);
this.mailer.enviarBienvenida(usuario);
}
}Por qué importa: si cambia el proveedor de email, tocas solo ServicioEmail. Si cambia la BD, solo el repositorio. Cambios aislados = menos riesgo.
🔗 Esto es lo que ya hiciste al separar controller/service/repository en NestJS, FastAPI y Go. Ya practicabas SRP sin saberlo.
O — Open/Closed Principle (Abierto/Cerrado)
El código debe estar abierto a extensión pero cerrado a modificación. Añades funcionalidad sin tocar lo que ya funciona.
# ❌ Cada nuevo método de pago obliga a modificar esta función (y arriesgar romperla)
def procesar_pago(tipo, monto):
if tipo == "tarjeta":
# ...
elif tipo == "paypal":
# ...
elif tipo == "yape": # y mañana otro, y otro... el if crece sin fin
# ...
# ✅ Una abstracción; cada método es una clase nueva que NO toca las demás
from abc import ABC, abstractmethod
class MetodoPago(ABC):
@abstractmethod
def pagar(self, monto: float) -> bool: ...
class Tarjeta(MetodoPago):
def pagar(self, monto): ...
class Yape(MetodoPago): # añadir esto NO modifica nada existente
def pagar(self, monto): ...
def procesar_pago(metodo: MetodoPago, monto: float):
return metodo.pagar(monto) # no cambia nuncaL — Liskov Substitution Principle (Sustitución de Liskov)
Si algo funciona con una clase padre, debe funcionar con cualquier hijo sin sorpresas.
# ❌ Viola Liskov: un Pingüino ES un Ave pero no puede volar → rompe el contrato
class Ave:
def volar(self): ...
class Pinguino(Ave):
def volar(self):
raise Exception("¡No puedo volar!") # sorpresa que rompe el código
# ✅ Modela bien la jerarquía
class Ave: ...
class AveVoladora(Ave):
def volar(self): ...
class Pinguino(Ave): # simplemente no hereda "volar"
def nadar(self): ...Regla práctica: un subtipo no debe fortalecer precondiciones ni debilitar postcondiciones. Si para usar el hijo tienes que comprobar "¿de qué tipo es en realidad?", algo está mal.
I — Interface Segregation Principle (Segregación de interfaces)
Muchas interfaces pequeñas y específicas son mejores que una gigante. Nadie debería depender de métodos que no usa.
// ❌ Una interfaz enorme; quien solo lee está obligado a implementar todo
type Repositorio interface {
Leer(id int) Item
Escribir(item Item)
Borrar(id int)
Exportar() []byte
Respaldar() error
}
// ✅ Interfaces pequeñas por capacidad (idiomático en Go)
type Lector interface { Leer(id int) Item }
type Escritor interface { Escribir(item Item) }
// Un servicio de solo lectura depende únicamente de lo que necesita:
func MostrarItem(l Lector, id int) { ... }🧠 En Go esto es cultura: interfaces de 1-2 métodos (
io.Reader,io.Writer). Cuanto más pequeña la interfaz, más fácil de implementar, mockear y reutilizar.
D — Dependency Inversion Principle (Inversión de dependencias)
Depende de abstracciones (interfaces), no de implementaciones concretas. Los módulos de alto nivel no deben depender de los de bajo nivel; ambos dependen de abstracciones.
// ❌ El servicio depende DIRECTAMENTE de PostgreSQL. Cambiar de BD = reescribir el servicio.
class ServicioPedidos {
private db = new PostgresConexion(); // acoplado a una implementación
crear(pedido) { this.db.query('INSERT...'); }
}
// ✅ El servicio depende de una ABSTRACCIÓN. La implementación se inyecta.
interface PedidoRepositorio {
guardar(pedido: Pedido): Promise<void>;
}
class ServicioPedidos {
constructor(private repo: PedidoRepositorio) {} // ← se inyecta
async crear(pedido: Pedido) { await this.repo.guardar(pedido); }
}
// En producción: new ServicioPedidos(new PostgresRepositorio())
// En tests: new ServicioPedidos(new RepositorioFake())🔗 Este es EL principio que hace tu código testeable. Por eso pudiste inyectar mocks en cada capítulo (repoFake en Go, prismaMock en Nest, dependency_overrides en FastAPI). La inyección de dependencias de los frameworks es DIP automatizado. Ahora entiendes el porqué.
10.3 · Cómo aplicar SOLID sin volverte loco
SOLID mal aplicado genera sobre-ingeniería: 15 interfaces para un CRUD trivial. La sensatez:
- No abstraigas antes de tiempo. Empieza simple. Cuando veas duplicación real o un punto de cambio frecuente, entonces extrae la abstracción. (YAGNI manda.)
- SOLID es una guía, no una religión. El objetivo es código mantenible, no "cumplir SOLID".
- La señal de que necesitas SOLID: cuando algo es difícil de testear, difícil de cambiar sin romper otra cosa, o tiene
if/elifque crecen sin parar. Ahí aplica el principio que resuelva ese dolor.
🧠 Frase para recordar: "Haz que funcione, hazlo correcto, hazlo rápido — en ese orden." Primero resuelve el problema; luego límpialo con estos principios; optimiza solo si medís que hace falta.
10.4 · Code smells (olores del código)
Señales de que algo huele mal y conviene refactorizar:
| Smell | Qué es | Solución |
|---|---|---|
| Función larga | Hace demasiado | Divídela en funciones con nombre |
| Clase Dios | Una clase que lo controla todo | Aplica SRP, reparte responsabilidades |
| Parámetros excesivos | crear(a, b, c, d, e, f) | Agrupa en un objeto/DTO |
| Números mágicos | if edad > 18 | Constante con nombre: MAYORIA_EDAD = 18 |
| Anidamiento profundo | if{ if{ if{ } } } | Early returns (guard clauses) |
| Comentarios excesivos | Explican código confuso | Reescribe el código para que se entienda solo |
| Código muerto | Comentado "por si acaso" | Bórralo. Para eso está git. |
Early return — el refactor más rentable para aplanar anidamiento:
# ❌ Pirámide de la perdición
def procesar(usuario):
if usuario is not None:
if usuario.activo:
if usuario.tiene_saldo():
# ...lógica real, sepultada 3 niveles...
# ✅ Guard clauses: sal pronto de los casos inválidos
def procesar(usuario):
if usuario is None:
return
if not usuario.activo:
return
if not usuario.tiene_saldo():
return
# ...lógica real, al nivel principal, clara...✅ Ejercicio del capítulo
Toma el código de una de tus APIs del blog y refactorízalo aplicando lo aprendido:
1. Renombra al menos 5 variables/funciones para que se expliquen solas.
2. Encuentra la función más larga y pártela en funciones de una sola cosa.
3. Aplica SRP: separa lógica de negocio, persistencia y presentación.
4. Aplica DIP: haz que tu servicio dependa de una interfaz de repositorio
(y aprovecha para escribir un test con un repo falso).
5. Aplica early returns donde haya anidamiento.
6. Elimina números mágicos y código muerto.
7. Anota: ¿qué principio resolvió qué dolor concreto? (no apliques por aplicar).Reflexión final: el buen código no es el más listo, es el más aburrido de leer — porque se entiende a la primera. Tu yo del futuro te lo agradecerá.
💡 Pistas de la solución
- Para encontrar la función más larga sin herramientas: busca la que tenga más niveles de indentación o más de un
ifanidado dentro de otroif. Esa suele ser la candidata. - El test con repositorio falso (paso 4) es la prueba de fuego de si DIP está bien aplicado: si no puedes crear
new ServicioPedidos(repoFake)sin tocar nada de infraestructura real, tu servicio sigue acoplado a una implementación concreta. - No apliques los 5 principios SOLID a la fuerza en un ejercicio pequeño — el punto 7 pide justificar cada uno; si no encuentras un dolor real que resuelva Liskov o Interface Segregation en tu código, está bien omitirlos y explicar por qué.
🧠 Autoevaluación
DRY habla de no repetir la misma decisión de negocio, no las mismas líneas de código. Dos fragmentos que hoy se parecen pero representan reglas distintas (y cambiarán por razones distintas) deben mantenerse separados — unirlos crea una abstracción prematura que se vuelve difícil de modificar cuando una de las dos reglas cambia y la otra no.
Early returns (guard clauses): sales pronto de los casos inválidos al principio de la función en vez de anidar el caso válido dentro de sucesivos if. El resultado es una función plana donde la lógica principal queda al nivel superior, sin sacrificar ninguna validación.
DIP dice que los módulos de alto nivel deben depender de abstracciones, no de implementaciones concretas. La inyección de dependencias automatiza exactamente eso: tu service recibe una interfaz/abstracción por constructor, y el framework decide en runtime qué implementación concreta inyectar — en producción la real, en tests un mock.
Viola Liskov Substitution: cualquier código que reciba un Ave y llame a volar() esperando que funcione se rompe en runtime si recibe un Pinguino. El problema no es solo teórico — cualquier función que use polimorfismo sobre Ave ahora necesita comprobar el tipo real antes de llamar a volar(), lo que anula la ventaja de usar herencia en primer lugar.
Siguiente: 11-arquitectura.md — cómo organizar aplicaciones grandes.