Diseño de decisiones en JavaScript: reglas claras | Nicolás Garzón
Saber escribir un if es sencillo. Diseñar decisiones que sigan siendo claras cuando aparecen permisos, estados, validaciones y reglas diferentes requiere más cuidado.
El objetivo no es eliminar los condicionales, sino conseguir que cada decisión:
Responda una pregunta concreta.
Aparezca en un orden lógico.
Tenga nombres comprensibles.
Evite caminos ocultos o contradictorios.
Pueda probarse sin depender de demasiadas cosas.
JavaScript
Copiar function processOrder ( order, user ) {
if ( user) {
if ( user. isActive) {
if ( order) {
if ( order. status === "pending" ) {
if ( order. total > 0 ) {
return confirmOrder ( order) ;
}
}
}
}
}
return null ;
} El camino importante está enterrado dentro de cinco niveles. Para entenderlo hay que recordar todas las condiciones exteriores.
Una guard clause comprueba temprano una condición que impide continuar.
JavaScript
Copiar function processOrder ( order, user ) {
if ( ! user) {
return null ;
}
if ( ! user. isActive) {
return null ;
}
if ( ! order) {
return null ;
}
if ( order. status !== "pending" ) {
return null ;
}
if ( order. total <= 0 ) {
return null ;
}
return confirmOrder ( order) ;
} El camino principal queda al mismo nivel visual y cada salida explica una razón concreta para detenerse.
No todas las funciones necesitan muchos retornos, pero los retornos tempranos son útiles para validaciones y precondiciones.
Retornar null para todos los problemas puede esconder información útil.
JavaScript
Copiar function validateOrder ( order, user ) {
if ( ! user) {
return { valid : false , reason : "USER_REQUIRED" } ;
}
if ( ! user. isActive) {
return { valid : false , reason : "USER_INACTIVE" } ;
}
if ( ! order) {
return { valid : false , reason : "ORDER_REQUIRED" } ;
}
if ( order. status !== "pending" ) {
return { valid : false , reason : "INVALID_STATUS" } ;
}
return { valid : true , reason : null } ;
} La estructura del resultado permite mostrar mensajes, registrar métricas o probar cada caso.
Una condición larga obliga a interpretar detalles cada vez que se lee.
JavaScript
Copiar if (
user. isAuthenticated &&
! user. isBlocked &&
order. status === "pending" &&
( user. id === order. ownerId || user. role === "admin" )
) {
showEditForm ( ) ;
} Un nombre puede expresar la intención:
JavaScript
Copiar const ownsOrder = user. id === order. ownerId;
const hasAdministrativeAccess = user. role === "admin" ;
const canManageOrder = ownsOrder || hasAdministrativeAccess;
const canEditOrder =
user. isAuthenticated &&
! user. isBlocked &&
order. status === "pending" &&
canManageOrder;
if ( canEditOrder) {
showEditForm ( ) ;
} No necesitas crear una variable para cada comparación. Hazlo cuando el nombre reduzca esfuerzo mental o evite repetir la regla.
Una función que devuelve un booleano puede encapsular una regla reutilizable.
JavaScript
Copiar function canEditOrder ( user, order ) {
if ( ! user. isAuthenticated || user. isBlocked) {
return false ;
}
if ( order. status !== "pending" ) {
return false ;
}
return user. id === order. ownerId || user. role === "admin" ;
} JavaScript
Copiar if ( canEditOrder ( user, order) ) {
showEditForm ( ) ;
} El nombre debe sonar como una pregunta que puede responderse con sí o no:
JavaScript
Copiar isValidEmail ( email) ;
hasAvailableStock ( product, quantity) ;
canCancelOrder ( user, order) ; Este código decide y ejecuta efectos al mismo tiempo:
JavaScript
Copiar if ( score >= 90 ) {
console. log ( "Excellent" ) ;
sendCertificate ( ) ;
} else if ( score >= 70 ) {
console. log ( "Approved" ) ;
} else {
console. log ( "Failed" ) ;
scheduleRecoveryCourse ( ) ;
} Puede ser más fácil probar primero una función que clasifique:
JavaScript
Copiar function getScoreLevel ( score ) {
if ( score >= 90 ) {
return "excellent" ;
}
if ( score >= 70 ) {
return "approved" ;
}
return "failed" ;
} Después otra parte ejecuta los efectos:
JavaScript
Copiar const level = getScoreLevel ( score) ;
if ( level === "excellent" ) {
sendCertificate ( ) ;
}
if ( level === "failed" ) {
scheduleRecoveryCourse ( ) ;
} La clasificación puede probarse con entradas y salidas simples.
Cuando una clave solo debe convertirse en un valor, una tabla evita condicionales repetitivos.
JavaScript
Copiar function getStatusLabel ( status ) {
const labels = {
pending : "Pendiente" ,
preparing : "En preparación" ,
ready : "Lista" ,
delivered : "Entregada" ,
} ;
return labels[ status] ?? "Desconocida" ;
} JavaScript
Copiar if ( status === "pending" ) {
return "Pendiente" ;
}
if ( status === "preparing" ) {
return "En preparación" ;
} Una tabla es buena para datos. Un switch o funciones separadas son mejores cuando cada caso necesita lógica.
También puede asociarse una acción con una función.
JavaScript
Copiar const actions = {
confirm : confirmOrder,
cancel : cancelOrder,
dispatch : dispatchOrder,
} ;
function executeAction ( action, order ) {
const handler = actions[ action] ;
if ( ! handler) {
throw new Error ( ` Unknown action: ${ action} ` ) ;
}
return handler ( order) ;
} Esta técnica funciona bien cuando los casos comparten la misma forma de entrada y salida.
No debe utilizarse automáticamente. Si cada acción necesita reglas muy diferentes, un switch explícito puede resultar más fácil de seguir.
JavaScript
Copiar createUser ( data, true , false ) ; Sin contexto, no se entiende qué significan esos booleanos.
Un objeto comunica mejor:
JavaScript
Copiar createUser ( data, {
sendWelcomeEmail : true ,
requirePasswordChange : false ,
} ) ; O pueden existir funciones diferentes si el comportamiento cambia de manera importante:
JavaScript
Copiar createInvitedUser ( data) ;
createActiveUser ( data) ; Los booleanos son correctos para datos booleanos. El problema aparece cuando controlan modos ocultos y difíciles de reconocer.
Las validaciones suelen ser más claras desde lo más básico hasta lo más específico.
JavaScript
Copiar function validateQuantity ( value, stock ) {
if ( value === null || value === undefined ) {
return "Quantity is required" ;
}
if ( ! Number. isInteger ( value) ) {
return "Quantity must be an integer" ;
}
if ( value <= 0 ) {
return "Quantity must be positive" ;
}
if ( value > stock) {
return "Quantity exceeds stock" ;
}
return null ;
} No tiene sentido comprobar el stock antes de confirmar que la cantidad existe y es un entero válido.
Los errores aparecen con frecuencia en los bordes.
JavaScript
Copiar if ( age > 18 ) {
return "Adult" ;
} ¿Qué ocurre exactamente con 18?
Tal vez la regla correcta sea:
JavaScript
Copiar if ( age >= 18 ) {
return "Adult" ;
} Para cada decisión por rango, revisa:
El valor justo antes del límite.
El propio límite.
El valor justo después.
JavaScript
Copiar 69 , 70 , 71
89 , 90 , 91 Esto ayuda a detectar huecos y solapamientos.
Cuando una entidad tiene estados, no todas las transiciones deberían permitirse.
JavaScript
Copiar const allowedTransitions = {
pending : [ "confirmed" , "cancelled" ] ,
confirmed : [ "preparing" , "cancelled" ] ,
preparing : [ "ready" ] ,
ready : [ "dispatched" ] ,
dispatched : [ "delivered" ] ,
delivered : [ ] ,
cancelled : [ ] ,
} ;
function canTransition ( from, to ) {
return allowedTransitions[ from] ?. includes ( to) ?? false ;
} JavaScript
Copiar if ( ! canTransition ( order. status, nextStatus) ) {
throw new Error (
` Cannot move order from ${ order. status} to ${ nextStatus} ` ,
) ;
} Una tabla de transiciones hace visible el modelo completo y evita repartir reglas contradictorias por diferentes partes del código.
Cuando existe un conjunto cerrado de casos, conviene decidir qué ocurre con valores desconocidos.
JavaScript
Copiar function getPaymentFee ( method ) {
switch ( method) {
case "cash" :
return 0 ;
case "card" :
return 0.03 ;
case "transfer" :
return 0.01 ;
default :
throw new Error ( ` Unsupported payment method: ${ method} ` ) ;
}
} Retornar silenciosamente un valor por defecto podría esconder un dato nuevo o inválido. La decisión depende del dominio: a veces existe un fallback legítimo; otras veces debe fallar de forma visible.
No basta con probar el camino exitoso.
JavaScript
Copiar function canPurchase ( { active, stock, quantity, balance, price } ) {
return (
active &&
stock >= quantity &&
balance >= price * quantity
) ;
}
Producto activo y compra válida.
Producto inactivo.
Stock exactamente igual a la cantidad.
Stock una unidad por debajo.
Balance exactamente igual al total.
Balance por debajo del total.
Cantidad cero o negativa si esas entradas son posibles.
Las pruebas deben proteger los límites y cada razón importante de rechazo.
JavaScript
Copiar function getCancellationResult ( { order, user, now } ) {
if ( ! order) {
return { allowed : false , reason : "ORDER_REQUIRED" } ;
}
if ( ! user) {
return { allowed : false , reason : "USER_REQUIRED" } ;
}
if ( [ "delivered" , "cancelled" ] . includes ( order. status) ) {
return { allowed : false , reason : "FINAL_STATUS" } ;
}
const ownsOrder = order. customerId === user. id;
const isStaff = [ "admin" , "manager" ] . includes ( user. role) ;
if ( ! ownsOrder && ! isStaff) {
return { allowed : false , reason : "NOT_AUTHORIZED" } ;
}
const elapsedMinutes =
( now. getTime ( ) - order. createdAt. getTime ( ) ) / 60_000 ;
if ( ownsOrder && elapsedMinutes > 10 ) {
return { allowed : false , reason : "CUSTOMER_TIME_LIMIT" } ;
}
return { allowed : true , reason : null } ;
}
Valida primero las entradas.
Rechaza estados finales.
Nombra las reglas de propiedad y rol.
Aplica una regla específica al cliente.
Devuelve un resultado que puede mostrarse, registrarse y probarse.
Anidar condicionales hasta esconder el camino principal.
Escribir condiciones largas sin nombres.
Repetir reglas de negocio en varios lugares.
Utilizar tablas para lógica que necesita pasos diferentes.
Utilizar switch para datos que solo necesitan un mapa.
Controlar modos importantes con booleanos ambiguos.
No definir qué ocurre con estados desconocidos.
Probar solo valores normales y olvidar los límites.
Mezclar clasificación, validación y efectos en el mismo bloque.
Retornar true o false cuando el consumidor necesita conocer la razón.
Las guard clauses mantienen visible el camino principal.
Las condiciones importantes deben tener nombres que expresen intención.
Extraer predicados evita repetir reglas.
Las tablas son buenas para relaciones entre datos.
switch y mapas de funciones son útiles para comportamientos por caso.
Las validaciones deben ordenarse desde lo básico hasta lo específico.
Los límites merecen pruebas propias.
Los estados complejos se benefician de transiciones explícitas.
El mejor diseño no es el que tiene menos if, sino el que hace las reglas fáciles de encontrar y entender.
¿Qué problemas de claridad encuentras aquí?
JavaScript
Copiar function handleOrder ( order, user, force ) {
if ( order) {
if ( user) {
if ( order. status !== "delivered" || force) {
if ( user. id === order. customerId || user. role === "admin" ) {
return cancelOrder ( order) ;
}
}
}
}
return false ;
} Respuesta orientativa
Existe demasiado anidamiento.
force no explica quién puede utilizarlo ni qué regla omite.
La condición status !== "delivered" || force permite forzar sin mostrar la autorización necesaria.
Propiedad y rol aparecen dentro de una condición larga.
false no explica la razón del rechazo.
Validación, autorización y efecto están mezclados.
Una mejora comenzaría con guard clauses, nombres como ownsOrder e isAdmin, y un resultado con una razón concreta antes de ejecutar cancelOrder.
El siguiente bloque estudia scope y ejecución , donde veremos cómo JavaScript organiza variables, funciones y contextos mientras ejecuta el programa.