Clean CodeArquitecturaTypeScriptBest Practices

Clean Code: El Arte de Escribir Código que Otros Agradecerán Leer

Lewis Lopez
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

Compartir este artículo:

¿Te gustó este artículo?

Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.