SOLID y Clean Code: escribir código que se entiende
Principios de diseño de software y prácticas de código limpio que hacen tu código mantenible, testeable y profesional.
Escribir código que funciona es fácil. Escribir código que otro programador (o tu yo de dentro de 6 meses) pueda entender, modificar y extender sin romper todo — eso es el arte.
SOLID son cinco principios de diseño de software que guían cómo estructurar clases y módulos. Clean Code es un conjunto de prácticas para escribir código legible. Juntos forman la base de lo que separa código amateur de código profesional.
En esta guía vas a aprender cada principio con ejemplos prácticos en TypeScript.
Clean Code: lo básico antes de SOLID
Nombres que significan algo
// ❌ Malo
function d(a, b) { return a * b; }
const x = 86400;
// ✅ Bueno
function calcularDistancia(kilometros, millas) { return kilometros * millas; }
const SEGUNDOS_POR_DIA = 86400;
Los nombres deben responder: ¿qué es? ¿para qué sirve? Si necesitas un comentario para explicar qué hace una función, el nombre es malo.
Funciones que hacen una cosa
// ❌ Hace tres cosas
function procesarUsuario(usuario) {
validarEmail(usuario.email);
const hash = await hashPassword(usuario.password);
await db.save({ ...usuario, password: hash });
await enviarEmailBienvenida(usuario.email);
}
// ✅ Cada función hace una cosa
function validarUsuario(usuario) { /* ... */ }
function crearUsuario(usuario) { /* ... */ }
function notificarUsuario(usuario) { /* ... */ }
// Composición
async function registrarUsuario(usuario) {
validarUsuario(usuario);
await crearUsuario(usuario);
await notificarUsuario(usuario);
}
Early return: evita nesting innecesario
// ❌ Pyramid of doom
function procesarPago(pago) {
if (pago.monto > 0) {
if (pago.tarjeta) {
if (pago.tarjeta.valida) {
// lógica de pago
}
}
}
}
// ✅ Early returns
function procesarPago(pago) {
if (pago.monto <= 0) throw new Error("Monto inválido");
if (!pago.tarjeta) throw new Error("Tarjeta requerida");
if (!pago.tarjeta.valida) throw new Error("Tarjeta inválida");
// lógica de pago, sin nesting
}
SOLID
S — Single Responsibility Principle
Una clase o módulo debe tener una sola razón para cambiar.
// ❌ Hace todo: valida, guarda y envía emails
class UsuarioService {
validar(usuario) { /* ... */ }
guardar(usuario) { /* ... */ }
enviarEmailBienvenida(usuario) { /* ... */ }
}
// ✅ Cada clase tiene una responsabilidad
class UsuarioValidator {
validar(usuario) { /* ... */ }
}
class UsuarioRepository {
guardar(usuario) { /* ... */ }
}
class EmailService {
enviarBienvenida(usuario) { /* ... */ }
}
Si cambia la validación, solo toca UsuarioValidator. Si cambia la BD, solo UsuarioRepository.
O — Open/Closed Principle
Abierto para extensión, cerrado para modificación.
// ❌ Modificar para agregar cada nuevo tipo de pago
class ProcesadorPago {
procesar(tipo, monto) {
if (tipo === "tarjeta") { /* ... */ }
else if (tipo === "paypal") { /* ... */ }
else if (tipo === "crypto") { /* ... */ }
// Cada nuevo tipo requiere modificar esta clase
}
}
// ✅ Extender sin modificar
interface MetodoPago {
procesar(monto: number): void;
}
class PagoTarjeta implements MetodoPago {
procesar(monto: number) { /* ... */ }
}
class PagoPaypal implements MetodoPago {
procesar(monto: number) { /* ... */ }
}
class PagoCrypto implements MetodoPago {
procesar(monto: number) { /* ... */ }
}
// Agregar un nuevo método no requiere cambiar ProcesadorPago
class ProcesadorPago {
constructor(private metodo: MetodoPago) {}
procesar(monto: number) {
this.metodo.procesar(monto);
}
}
L — Liskov Substitution Principle
Las subclases deben poder reemplazar a sus clases base sin cambiar el comportamiento correcto del programa.
// ❌ RectanguloCuadrado viola LSP
class Rectangulo {
constructor(public ancho: number, public alto: number) {}
area() { return this.ancho * this.alto; }
}
class Cuadrado extends Rectangulo {
constructor(lado: number) {
super(lado, lado);
}
// Si alguien cambia ancho o alto, el cuadrado se rompe
}
function redimensionar(rect: Rectangulo) {
rect.ancho = 5;
rect.alto = 10;
console.log(rect.area()); // Espera 50
}
redimensionar(new Cuadrado(4)); // 50? No, porque Cuadrado fuerza ancho === alto
La solución: no heredar cuando la subclase cambia el comportamiento fundamental. Composición sobre herencia.
I — Interface Segregation Principle
Es mejor muchas interfaces específicas que una interfaz general.
// ❌ Una interfaz gigante
interface Trabajador {
trabajar(): void;
comer(): void;
dormir(): void;
}
class Humano implements Trabajador {
trabajar() { /* ... */ }
comer() { /* ... */ }
dormir() { /* ... */ }
}
class Robot implements Trabajador {
trabajar() { /* ... */ }
comer() { /* No come */ throw new Error("No aplica"); }
dormir() { /* No duerme */ throw new Error("No aplica"); }
}
// ✅ Interfaces segregadas
interface Trabajable {
trabajar(): void;
}
interface Alimentable {
comer(): void;
}
interface Descansable {
dormir(): void;
}
class Humano implements Trabajable, Alimentable, Descansable { /* ... */ }
class Robot implements Trabajable { /* solo trabajar */ }
D — Dependency Inversion Principle
Depender de abstracciones, no de concreciones.
// ❌ Depende de implementación concreta
class PedidoService {
private db = new MySQLDatabase(); // Hardcodeado
crear(pedido) {
this.db.save(pedido);
}
}
// ✅ Depende de abstracción
interface Database {
save(data: any): void;
}
class PedidoService {
constructor(private db: Database) {} // Inyectado
crear(pedido) {
this.db.save(pedido);
}
}
// Puedes usar MySQL, PostgreSQL, SQLite, o un mock para tests
const service = new PedidoService(new PostgreSQLDatabase());
const testService = new PedidoService(new MockDatabase());
Clean Code en la práctica
Evitar comentarios que repiten el código
// ❌ El comentario repite lo obvio
// Incrementar contador en 1
contador++;
// ✅ Mejor: el código se explica solo
intentosRestantes--;
// ✅ Comentario útil: explica el POR QUÉ, no el QUÉ
// Esperar 100ms porque la API rate-limited rechaza peticiones muy rápidas
await sleep(100);
Código consistente
// ❌ Inconsistente
function getUser() { }
function crear_pedido() { }
const EmailValido = true;
// ✅ Consistente (camelCase para funciones/variables)
function getUser() { }
function crearPedido() { }
const emailValido = true;
Manejo de errores explícito
// ❌ Silenciar errores
try {
await guardarDatos();
} catch (e) {
// Ignorar
}
// ✅ Manejar explícitamente
try {
await guardarDatos();
} catch (error) {
logger.error("Error al guardar datos", { error, datos });
throw new Error("No se pudieron guardar los datos");
}
Por qué importa
SOLID y Clean Code no son reglas religiosas — son herramientas pragmáticas:
- Código legible se mantiene más barato y se debuggea más rápido.
- Principios SOLID hacen tu código testeable y extensible.
- Nombres claros eliminan la necesidad de documentación externa.
- Funciones pequeñas son más fáciles de testear y reutilizar.
La IA y Clean Code
Lo bueno
- Sugerir mejores nombres: muéstrale una función y la IA sugiere nombres más descriptivos.
- Refactorizar: la IA extrae funciones, aplica SOLID, simplifica condiciones.
- Revisar código: la IA identifica violaciones de principios y sugiere mejoras.
Lo que no debes hacer
- No apliques SOLID dogmáticamente. No todo necesita 15 clases. A veces un archivo simple es mejor.
- No aceptes refactorings que compliquen sin necesidad. La sobre-abstracción es tan mala como el código espagueti.
- No uses nombres en inglés si tu equipo trabaja en español. La consistencia con tu equipo importa más que el idioma.
Desafío: refactoriza código sucio
Objetivo: aplicar Clean Code y SOLID a código problemático.
Tu tarea:
Refactoriza este código aplicando los principios que aprendiste:
class UserManager {
async doStuff(u, p, e, a) {
if (u != null && u.length > 0 && p != null && p.length >= 8 && e != null && e.includes("@")) {
let x = await require("crypto").randomBytes(16).toString("hex");
let d = require("pg").Pool;
let pool = new d({ connectionString: process.env.DB });
let c = await pool.connect();
try {
await c.query("INSERT INTO users (name, pass, email, age) VALUES ($1, $2, $3, $4)", [u, x, e, a]);
await require("nodemailer").createTransport({ host: "smtp.ejemplo.com" }).sendMail({
from: "noreply@ejemplo.com", to: e, subject: "Bienvenido", text: `Hola ${u}`
});
} finally { c.release(); }
}
}
}
Bonus: escribe tests unitarios para el código refactorizado.
Para seguir explorando
- Clean Code — libro de Robert C. Martin.
- Agile Software Development: Principles, Patterns, and Practices — SOLID explicado por Uncle Bob.
- Refactoring Guru — patrones de diseño y refactoring visual.
Resumen
- Clean Code: nombres significativos, funciones que hacen una cosa, early returns, consistencia.
- S — Single Responsibility: una clase, una razón para cambiar.
- O — Open/Closed: extender sin modificar.
- L — Liskov Substitution: las subclases deben reemplazar a sus bases sin cambiar comportamiento.
- I — Interface Segregation: interfaces pequeñas y específicas.
- D — Dependency Inversion: depender de abstracciones, no de concreciones.
- Los principios son guías, no dogmas. Aplícalos con criterio.
En la próxima guía vamos a estilizar con propósito: Tailwind CSS: estilos utilitarios en la práctica — CSS moderno sin archivos separados.