Clean Code: El Arte de Escribir Código que Otros Agradecerán Leer
Después de revisar miles de líneas de código en proyectos bancarios y corporativos, he aprendido una verdad universal: el código se lee 10 veces más de lo que se escribe.
Clean Code no es sobre ser elegante. Es sobre respeto: respeto por tu yo futuro, por tu equipo, y por el negocio que depende de ese código.
El Costo Real del Código Sucio
En Best Information Technologies, heredamos un proyecto de una fintech con estas características:
- 📊 200K líneas de código
- 🕐 8 horas para entender un módulo
- 💰 $50K USD perdidos por bugs evitables
- 😰 3 desarrolladores senior renunciaron
El problema: Nadie quería tocar el código. Era más fácil reescribirlo.
Señales de Código Problemático
// ❌ ANTES: Nadie entiende qué hace esto
function p(d: any) {
const r = [];
for (let i = 0; i < d.length; i++) {
if (d[i].s === 'A' && d[i].t > 1000) {
r.push(d[i]);
}
}
return r;
}
Problemas:
- Nombres crípticos (
p,d,r) - Tipo
any(pérdida de seguridad) - Lógica mezclada con iteración
- Sin documentación del propósito
Los 7 Principios de Clean Code
1. Nombres Reveladores de Intención
Tu código debe leerse como prosa en español.
// ✅ DESPUÉS: Autoexplicativo
interface PaymentTransaction {
status: 'ACTIVE' | 'PENDING' | 'REJECTED';
amount: number;
timestamp: Date;
}
function filterActiveHighValueTransactions(
transactions: PaymentTransaction[]
): PaymentTransaction[] {
const HIGH_VALUE_THRESHOLD = 1000;
return transactions.filter(
transaction =>
transaction.status === 'ACTIVE' &&
transaction.amount > HIGH_VALUE_THRESHOLD
);
}
Beneficios:
- ✅ Cero comentarios necesarios
- ✅ El propósito es obvio
- ✅ Los tipos previenen errores
- ✅ Fácil de testear
2. Funciones Pequeñas y Enfocadas
Una función = Una responsabilidad.
// ❌ ANTES: Función gigante que hace de todo
async function processOrder(orderId: string) {
// 150 líneas de validación, cálculo, persistencia, notificación...
}
// ✅ DESPUÉS: Funciones especializadas
async function processOrder(orderId: string): Promise<void> {
const order = await validateOrder(orderId);
const total = calculateOrderTotal(order);
const payment = await processPayment(order, total);
await persistOrderAndPayment(order, payment);
await notifyCustomer(order, payment);
}
async function validateOrder(orderId: string): Promise<Order> {
const order = await orderRepository.findById(orderId);
if (!order) {
throw new OrderNotFoundException(orderId);
}
if (order.items.length === 0) {
throw new EmptyOrderException();
}
return order;
}
Ventajas:
- 📖 Cada función es autoexplicativa
- 🧪 Tests unitarios simples
- 🔄 Reutilización natural
- 🐛 Debugging más rápido
3. Evita Comentarios Redundantes
El código limpio se comenta a sí mismo.
// ❌ MAL: Comentarios que repiten el código
// Incrementar el contador
counter++;
// Verificar si el usuario es admin
if (user.role === 'admin') {
// ...
}
// ✅ BIEN: Código autoexplicativo
incrementCartItemQuantity();
if (userHasAdministratorPrivileges()) {
// ...
}
// ✅ BIEN: Comentarios que añaden valor
// Workaround: API externa retorna null en vez de array vacío
// TODO: Remover cuando migren a v2 (Q2 2025)
const items = response.items ?? [];
Cuándo SÍ comentar:
- ⚠️ Decisiones arquitectónicas no obvias
- 🐛 Workarounds temporales con fecha límite
- 📚 Algoritmos complejos (con referencia)
- ⚡ Optimizaciones de performance
4. Manejo de Errores Explícito
Los errores son casos de uso, no excepciones.
// ❌ ANTES: Errores genéricos
try {
await paymentService.charge(amount);
} catch (error) {
console.log('Error');
return null; // ¿Qué hago con esto?
}
// ✅ DESPUÉS: Errores específicos y accionables
class InsufficientFundsError extends Error {
constructor(
public required: number,
public available: number
) {
super(`Fondos insuficientes: requiere ${required}, disponible ${available}`);
this.name = 'InsufficientFundsError';
}
}
class PaymentGatewayTimeoutError extends Error {
constructor(public attemptNumber: number) {
super(`Gateway timeout en intento ${attemptNumber}`);
this.name = 'PaymentGatewayTimeoutError';
}
}
// Uso
try {
await paymentService.charge(amount);
} catch (error) {
if (error instanceof InsufficientFundsError) {
await notifyUserInsufficientFunds(error.required, error.available);
} else if (error instanceof PaymentGatewayTimeoutError) {
await retryPaymentWithExponentialBackoff(error.attemptNumber);
} else {
await logUnexpectedError(error);
throw error;
}
}
5. Ley de Demeter (Don’t Talk to Strangers)
Un objeto solo debe conocer a sus vecinos inmediatos.
// ❌ ANTES: Cadena de dependencias
const city = user.address.location.city; // ¿Qué pasa si address es null?
// ✅ DESPUÉS: Encapsulación correcta
class User {
getUserCity(): string | null {
return this.address?.location?.city ?? null;
}
}
const city = user.getUserCity();
6. DRY (Don’t Repeat Yourself)
La duplicación es el enemigo del mantenimiento.
// ❌ ANTES: Lógica duplicada
function validateUserEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validateContactEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// ✅ DESPUÉS: Lógica centralizada
class EmailValidator {
private static readonly EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
static isValid(email: string): boolean {
return this.EMAIL_REGEX.test(email);
}
}
// Uso
EmailValidator.isValid(user.email);
EmailValidator.isValid(contact.email);
7. Testing como Primera Clase
El código sin tests es código legacy desde el día 1.
// ✅ Test que documenta el comportamiento esperado
describe('OrderService', () => {
describe('processOrder', () => {
it('should reject orders with insufficient stock', async () => {
// Arrange
const order = createTestOrder({ productId: '123', quantity: 10 });
mockInventory.getStock.mockResolvedValue(5);
// Act & Assert
await expect(orderService.processOrder(order))
.rejects
.toThrow(InsufficientStockError);
});
it('should notify customer after successful payment', async () => {
// Arrange
const order = createTestOrder();
mockPaymentGateway.charge.mockResolvedValue({ success: true });
// Act
await orderService.processOrder(order);
// Assert
expect(mockNotificationService.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: order.customerEmail,
subject: 'Orden confirmada',
})
);
});
});
});
Caso Real: Refactoring en Banco Estado Chile
En GENESYS trabajé en APIs críticas para Banco Estado. El código original:
// ❌ ANTES: 300 líneas en un solo archivo
export async function handleRequest(req: any, res: any) {
// Validación
if (!req.body.rut || !req.body.amount) {
res.status(400).send('Error');
return;
}
// Lógica de negocio mezclada
const client = await db.query('SELECT * FROM clients WHERE rut = $1', [req.body.rut]);
if (!client) {
res.status(404).send('Not found');
return;
}
// 250 líneas más...
}
Refactoring aplicado:
// ✅ DESPUÉS: Separación de responsabilidades
// 1. DTOs con validación
export class TransferRequestDto {
@IsRutValid()
rut: string;
@IsPositive()
@Max(1000000)
amount: number;
@IsNotEmpty()
destinationAccount: string;
}
// 2. Controller limpio
@Controller('transfers')
export class TransferController {
constructor(private transferService: TransferService) {}
@Post()
async createTransfer(
@Body() dto: TransferRequestDto
): Promise<TransferResponseDto> {
return this.transferService.processTransfer(dto);
}
}
// 3. Service con lógica de negocio
@Injectable()
export class TransferService {
constructor(
private clientRepository: ClientRepository,
private transferRepository: TransferRepository,
private fraudDetectionService: FraudDetectionService
) {}
async processTransfer(dto: TransferRequestDto): Promise<TransferResponseDto> {
const client = await this.validateClient(dto.rut);
await this.checkSufficientFunds(client, dto.amount);
await this.detectFraud(client, dto);
return this.executeTransfer(client, dto);
}
private async validateClient(rut: string): Promise<Client> {
const client = await this.clientRepository.findByRut(rut);
if (!client) {
throw new ClientNotFoundException(rut);
}
return client;
}
// Métodos privados especializados...
}
Resultados:
- ⚡ 80% menos bugs en producción
- 🧪 Cobertura de tests: 0% → 95%
- 👥 Onboarding: 2 semanas → 3 días
- 📈 Velocidad de features: +40%
Checklist de Clean Code
Antes de hacer commit, pregúntate:
📝 Nombres
- ¿Los nombres son descriptivos sin comentarios?
- ¿Evito abreviaturas crípticas?
- ¿Las variables booleanas empiezan con
is,has,should?
🔧 Funciones
- ¿Cada función hace UNA cosa?
- ¿Tiene menos de 20 líneas?
- ¿Los parámetros son menos de 3?
- ¿Evito flags booleanos?
🎯 Lógica
- ¿Evito anidación mayor a 2 niveles?
- ¿Uso guard clauses para retornos tempranos?
- ¿Las condiciones complejas están extraídas en funciones?
🧪 Tests
- ¿Cada función tiene al menos un test?
- ¿Los tests son legibles y descriptivos?
- ¿Cubren casos edge?
📚 Documentación
- ¿El código se explica solo?
- ¿Los comentarios añaden valor (no repiten)?
- ¿Las decisiones no obvias están documentadas?
Herramientas para Mantener Calidad
{
"devDependencies": {
"eslint": "^8.50.0",
"prettier": "^3.0.0",
"husky": "^8.0.0",
"lint-staged": "^14.0.0",
"jest": "^29.7.0",
"sonarqube": "^2.0.0"
},
"scripts": {
"lint": "eslint . --fix",
"format": "prettier --write .",
"test": "jest --coverage",
"quality": "sonar-scanner"
}
}
Configuración de Pre-commit
// .husky/pre-commit
#!/bin/sh
npm run lint
npm run format
npm test -- --onlyChanged
Recursos Recomendados
📚 Libros Esenciales:
- Clean Code - Robert C. Martin
- Refactoring - Martin Fowler
- The Pragmatic Programmer - Hunt & Thomas
🎓 Mis Cursos (lewislopez.io/productos):
Incluye:
- ✅ Ejercicios prácticos de refactoring
- ✅ Code reviews en vivo
- ✅ Proyectos reales con Clean Code aplicado
- ✅ Certificación profesional
Conclusión: El Código es para Humanos
Recuerda: las máquinas ejecutan bytecode, los humanos leen código fuente.
Clean Code no es perfeccionismo. Es pragmatismo profesional:
- 💼 Para el negocio: Menos bugs, entregas más rápidas
- 👥 Para el equipo: Colaboración fluida, menor rotación
- 🚀 Para tu carrera: Code reviews positivos, liderazgo técnico
El mejor momento para empezar es ahora. El segundo mejor, en tu próximo commit.
Lewis Lopez - Arquitecto de Software
10+ años en banca y sistemas críticos
Fundador de MATIAS IMPULSO
¿Preguntas sobre Clean Code? Escríbeme: contacto@lewislopez.io
Temas relacionados:
¿Te gustó este artículo?
Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.