Introducción
Muchas veces confundimos Clean Code con "código corto". No es eso. Un bloque de cinco líneas puede ser dificilísimo de modificar si esconde reglas de negocio, mezcla niveles de abstracción o obliga a recordar qué significa cada boolean.
En este post vamos a refactorizar un caso de uso muy habitual: aplicar un descuento a un pedido. El objetivo no es aplicar una lista de reglas de memoria, sino dejar el código en un estado en el que la siguiente persona pueda responder rápido a tres preguntas: qué hace, cuándo falla y qué datos necesita.
El punto de partida
Imaginemos que recibimos un pedido y un cupón. Esta primera versión funciona, pero empieza a ser incómoda cuando aparecen nuevas reglas:
El problema no es solo la anidación. Al leerlo tenemos que invertir mentalmente varias condiciones para entender el camino feliz. Además, devolver el total original para cualquier error borra información útil: un cupón caducado y un pedido ya pagado no son el mismo problema.
Guard clauses: sacar los casos que no pueden continuar
Las guard clauses son retornos tempranos para los casos que impiden continuar. Dejan el camino principal al mismo nivel de indentación y hacen visibles las reglas de entrada.
Ahora el final de la función cuenta una historia muy simple: si todas las reglas se cumplen, aplica el descuento. Los fallos aparecen donde ocurren y pueden transformarse en mensajes HTTP o de interfaz en la capa adecuada.
Extraer una regla cuando tiene nombre propio
No conviene convertir cada condición en una función privada. Pero si una expresión representa un concepto del dominio, darle nombre reduce carga cognitiva y evita duplicarla.
El caso de uso queda centrado en decisiones, no en detalles de comparación:
Un buen nombre no sustituye a la lógica; la coloca en el vocabulario del problema. isCouponActive comunica más que expiresAt > now, especialmente cuando esa regla termine incorporando una fecha de activación o una zona horaria.
Haz que los estados inválidos sean difíciles de representar
TypeScript no impide por sí solo que alguien cree un cupón con percentage: -20. Pero podemos concentrar esa validación en un constructor y exponer un tipo que solo se obtiene si el valor es válido.
El branding no es magia: en ejecución sigue siendo un number. Pero hace explícita la frontera de validación. A partir de ahí, una función que recibe Percentage puede asumir que el valor ya cumple el contrato.
No hace falta aplicar este patrón a cada string o número. Es especialmente útil en valores que cruzan varias capas y cuyas restricciones son importantes para el negocio: dinero, porcentajes, emails o identificadores.
Tests que describen comportamiento
La refactorización es más segura cuando los tests hablan del comportamiento observado, no de las funciones internas. En este caso los nombres de las pruebas pueden funcionar como una pequeña especificación.
Fíjate en que no comprobamos si se llamó a isCouponActive. Esa es una decisión de implementación que deberíamos poder cambiar sin reescribir los tests del caso de uso.
Una lista práctica antes de cerrar el editor
Cuando una función empieza a crecer, me ayuda revisar estas preguntas:
- ¿Su nombre expresa una acción y su resultado?
- ¿Los casos de salida temprana están al principio?
- ¿Cada bloque trabaja al mismo nivel de abstracción?
- ¿Hay un
booleano un número cuyo significado merezca un tipo o un nombre? - ¿Los tests describen reglas del negocio y no la estructura interna?
Conclusión
Clean Code no busca un estilo rígido ni funciones minúsculas por deporte. Busca que modificar una regla sea barato y que el código explique por qué toma una decisión.
Empezar por guard clauses, nombres de dominio y tipos en las fronteras suele dar un retorno inmediato. Después, si el caso de uso crece, será mucho más sencillo llevar esas reglas a entidades y casos de uso dentro de una arquitectura limpia.