Siete hábitos que facilitan que el próximo desarrollador modifique el código
Cláusulas de guardia, nombres que revelan la intención, límites de API, tipos precisos, funciones de decisión puras, errores estructurados y solicitudes de integración enfocadas, con ejemplos en JavaScript.
Fuentes: 7 Coding Habits That Separate Senior Developers From the Rest
Aprender otro framework eventualmente deja de hacer que seas un ingeniero notablemente mejor. Lo que realmente da resultados es la forma en que escribes y modificas el código: los desarrolladores experimentados suelen generar menos código, con una intención más clara, que es más fácil de leer, probar y modificar cuando cambian los requisitos. Este artículo explica siete hábitos concretos, cada uno con un ejemplo de JavaScript o TypeScript antes y después, y detalla el beneficio específico que aporta cada uno.
Ninguno de estos hábitos requiere una herramienta nueva. Son decisiones que tomas al escribir una función, nombrar una variable, modelar datos o abrir una solicitud de integración.
1. Mantén visible el camino óptimo con cláusulas de protección
Supongamos que una función debe buscar a un usuario, confirmar que la cuenta está activa, verificar que el usuario tenga permisos y solo entonces realizar una acción. Un borrador inicial común es anidar una condición dentro de otra:
function performAction(userId) {
const user = findUser(userId);
if (user) {
if (user.isActive) {
if (user.hasPermission) {
return executeAction(user);
}
}
}
return null;
}
Funciona, pero la operación realmente importante se encuentra a tres niveles de profundidad, y la solución alternativa return null al final está visualmente desconectada de las condiciones que la generan.
La alternativa es rechazar inmediatamente los casos inválidos mediante cláusulas de protección, de modo que cada condición fallida aparezca en su propia línea. (El sangrado de este fragmento no es uniforme; las cuatro instrucciones siguientes a la búsqueda deben estar al mismo nivel.)
function performAction(userId) {
const user = findUser(userId);
if (!user) return null;
if (!user.isActive) return null;
if (!user.hasPermission) return null;
return executeAction(user);
}
Ahora el flujo principal sigue de forma directa: verificar, verificar, verificar, actuar. Esto no significa que la lógica anidada esté siempre equivocada. Cuando las ramas representan resultados realmente diferentes en lugar de condiciones previas, un if/else puede ser la forma más clara. El objetivo es evitar que la lógica empresarial esencial quede oculta bajo condiciones que en realidad solo sirven como filtros.
El hábito: tratar primero los casos excepcionales y dejar el flujo normal sin indentación. Quien ejecute la función debe poder ver lo que sucede habitualmente sin tener que seguir cinco niveles de llaves. Una ventaja adicional es que cada bloque de control puede devolver posteriormente un resultado o error más específico, lo cual es difícil de lograr de forma limpia en la versión anidada.
2. Den nombres a las cosas según su significado
Pocas modificaciones mejoran la legibilidad tanto como nombres más adecuados. Miren esta pareja de líneas:
const data = getOrder();
process(data);
¿Qué es data, y qué hace process con él? Hay que leer otro código para averiguarlo. Comparen:
const pendingOrder = getPendingOrder();
processOrder(pendingOrder);
Las mismas dos líneas ahora le indican qué tipo de orden está involucrado y qué le sucede. Los buenos nombres reducen el esfuerzo mental necesario para comprender un código, ya que el lector ya no tiene que reconstruir el significado a partir del contexto.
Los nombres de las funciones merecen la misma atención. Un verbo genérico como este oculta el comportamiento:
handle(order);
Mientras que esta versión lo expresa claramente:
calculateOrderTotal(order);
De igual manera, un simple send no deja claro qué se está enviando ni a quién:
send(order);
Y un nombre descriptivo responde a ambas preguntas:
sendOrderConfirmation(order);
Los nombres largos no son el objetivo; lo importante son los nombres que comunican la intención. Un efecto secundario útil es que los nombres vagos suelen corresponder a funciones igualmente vagas. Si handle es difícil de renombrar, es posible que haga varias cosas no relacionadas y merezca dividirse.
El hábito: antes de decidir un nombre, pregúntese si alguien que solo lea esa línea, sin mirar a ningún otro lugar, entendería qué representa el valor o la función. Si no, siga buscando un nombre mejor.
3. Mantener los sistemas externos detrás de un límite
Las aplicaciones se comunican con muchos sistemas externos: proveedores de pagos, servicios de correo electrónico, almacenamiento en la nube, proveedores de identidad, APIs de terceros y microservicios internos. Un error frecuente es permitir que los formatos de respuesta de estos sistemas se integren directamente en el código empresarial:
const customer = await stripeCustomer.get();
console.log(customer.email_address);
console.log(customer.customer_name);
Cada línea que lee email_address o customer_name depende ahora de los nombres exactos de los campos definidos por el proveedor. Si el proveedor renombra un campo o reestructura su respuesta, ese cambio afecta a todo el código.
La solución es utilizar una función de interfaz que llama al servicio externo y convierte su respuesta en un modelo propio de la aplicación:
async function getCustomer(customerId) {
const response = await externalService.getCustomer(customerId);
return {
email: response.email_address,
name: response.customer_name
};
}
El resto del código utiliza ese formato interno y nunca tiene conocimiento de los nombres utilizados por el proveedor:
const customer = await getCustomer(customerId);
console.log(customer.email);
console.log(customer.name);
Cuando la API externa cambia, solo es necesario actualizar la mapeo dentro de getCustomer. Ese mismo límite también es el lugar natural para validar la respuesta, normalizar los valores faltantes e insertar uno ficticio durante las pruebas. Esta es la idea detrás de los adaptadores y capas de protección en arquitecturas más complejas, aplicada a escala de una sola función.
El hábito: mantener la complejidad externa en los bordes. La lógica de negocio no debería necesitar conocer las particularidades de cada servicio de terceros, y esa separación se vuelve aún más valiosa a medida que aumenta el número de integraciones.
4. Hacer que los estados inválidos sean difíciles de representar
Una forma tentadora de definir un modelo es hacer que cada campo sea opcional, ya que es rápido y nunca falla al compilar:
type Order = {
id?: string;
paymentId?: string;
status?: string;
};
El costo se hace evidente en todos los lugares donde se utiliza el modelo. Cada consumidor debe realizar verificaciones preventivas, ya que nada garantiza la presencia de algún campo:
if (order.id) {
// ...
}
if (order.paymentId) {
// ...
}
if (order.status === "paid") {
// ...
}
El problema más profundo no es la verbosidad. El tipo no indica qué combinaciones de campos son válidas. Un pedido con un ID de pago pero sin identificador, o un estado de "paid" sin pago correspondiente, son todos válidos desde el punto de vista del compilador.
Un modelo más preciso describe un estado específico:
type PaidOrder = {
id: string;
paymentId: string;
status: "paid";
};
Un valor de tipo PaidOrder garantiza tres cosas:
- tiene un ID
- tiene un ID de pago
- su estado es exactamente
"paid"
El tipo ahora sirve tanto como documentación como como medida de seguridad en tiempo de compilación. En la práctica, se definiría un tipo por estado, como un pedido pendiente sin ID de pago y un pedido pagado con uno, y se combinarían en una unión discriminada basada en status. TypeScript puede entonces restringir automáticamente el tipo después de una comprobación como order.status === "paid", y advertirte cuando no se gestione un nuevo estado. Para ver un ejemplo práctico de este patrón, consulta sustituyendo los indicadores de carga con una unión discriminada.
El hábito: no opte por campos opcionales solo porque facilitan la redacción de la primera versión. Pregunte qué estados realmente deberían ser posibles y modele sus tipos en función de ellos. Esto no eliminará todos los errores, pero hará que sea mucho más difícil crear toda una categoría de datos incorrectos.
5. Separe las decisiones de las acciones
Las reglas empresariales se vuelven difíciles de probar cuando están entrelazadas con efectos secundarios. Tome como ejemplo una función que solo pueden usar usuarios adultos verificados:
async function processUser(userId) {
const user = await database.getUser(userId);
if (user.isVerified && user.age >= 18) {
await database.enableFeature(userId);
await emailService.send(user.email);
}
}
La regla en sí, que verifica que el usuario tenga al menos 18 años, se mezcla con una lectura de la base de datos, una escritura en la base de datos y un correo electrónico saliente. Para probar la regla es necesario simular los tres elementos.
Saque la decisión a una función pura que reciba datos y devuelva un valor booleano:
function canUseFeature(user) {
return user.isVerified && user.age >= 18;
}
Luego, la función de orquestación obtiene los datos del usuario, consulta la regla y ejecuta los efectos secundarios solo cuando esta es satisfactoria:
async function processUser(userId) {
const user = await database.getUser(userId);
if (!canUseFeature(user)) {
return;
}
await database.enableFeature(userId);
await emailService.send(user.email);
}
Ahora se puede probar la regla directamente, con un objeto sencillo:
expect(
canUseFeature({
isVerified: true,
age: 25
})
).toBe(true);
Ninguna base de datos, ningún servicio de correo electrónico, ninguna simulación: solo la decisión y sus entradas. Eso hace que sea muy sencillo cubrir casos límite como un usuario de 30 años no verificado o un usuario verificado que tiene exactamente 18 años.
El hábito: separar “¿deberíamos hacer esto?” de “¿cómo lo hacemos?”. Algunos ejemplos adecuados para esta separación incluyen:
- verificaciones de acceso y permisos
- cálculos de precios
- validación de entradas
- tomar decisiones sobre si notificar a alguien
- políticas de reintentos
- reglas de elegibilidad y activación de funciones
Cuanto más importante sea una regla para el negocio, mayor será su beneficio al estar contenida en una función pequeña, sencilla y bien probada.
6. Hacer que los errores sean útiles para las personas y el software
Un error como este casi no ofrece nada con lo que trabajar:
throw new Error("Something went wrong");
No indica qué falló, dónde, por qué, ni si el llamante puede hacer algo al respecto.
Un error mejor incluye un código estable junto con un mensaje legible para humanos. El ejemplo asume una clase AppError que extiende a Error y almacena ambos valores; no es una clase incorporada.
throw new AppError(
"PAYMENT_FAILED",
"The payment could not be completed."
);
Al ser serializado para una respuesta de API, brinda a los clientes algo predecible:
{
"code": "PAYMENT_FAILED",
"message": "The payment could not be completed."
}
Diferencia importante que hay que recordar: los mensajes están escritos para humanos, mientras que los códigos están diseñados para sistemas. Los mensajes pueden reformularse o traducirse, pero los valores del código deben mantenerse estables para que los clientes puedan actuar sobre ellos de forma segura. Un frontend, por ejemplo, puede reaccionar ante un fallo específico:
if (error.code === "PAYMENT_FAILED") {
showPaymentError();
}
Los registros también merecen el mismo trato. Es difícil tomar medidas basándose únicamente en un mensaje cuando son las 3 a.m.:
console.log("Payment failed");
Un contexto estructurado permite buscar y diagnosticar fácilmente la información:
logger.error("Payment failed", {
orderId,
provider,
attempt
});
Sé deliberado al decidir qué incluir en ese contexto. Nunca registres:
- contraseñas
- tokens de acceso
- claves API
- otros secretos
- datos personales sensibles
Si necesitas un formato estándar para los cuerpos de errores HTTP, vale la pena consultar los detalles del problema según RFC 9457.
El hábito: dar a cada fallo una forma que una persona pueda leer y un programa pueda utilizar para actuar. Lanzar el código es la parte fácil; el verdadero trabajo consiste en hacer que los fallos sean predecibles y fáciles de diagnosticar.
7. Mantén cada cambio enfocado
El último hábito se refiere a modificar código en lugar de escribirlo. Imagina una única solicitud de integración que contenga:
- una nueva función de pago
- una migración de la base de datos
- un refactoring del servicio de pagos
- actualizaciones en la interfaz frontal
- cambios en la lógica de intentos repetidos
- varias correcciones de errores no relacionados
Incluso si todo funciona, es muy difícil revisarlo adecuadamente. Los revisores lo examinan superficialmente, los riesgos se ocultan entre el ruido, y si algo falla en producción, revertir una parte implica revertir todo.
Al dividirlo en cambios enfocados, el mismo trabajo se ve así:
PR #1 → Add checkout validation
PR #2 → Refactor payment service
PR #3 → Update checkout UI
PR #4 → Improve payment retry logic
Cada uno es más fácil de:
- revisar
Los cambios enfocados también facilitan la colaboración: cuando aparece una regresión, el área a investigar es pequeña, y herramientas como git bisect apuntan a un cambio que tiene sentido por sí mismo.
El hábito: antes de integrar otro cambio en la tarea actual, pregúntate si pertenece al mismo problema. Si no es así, crea un commit o pull request separado para él. Un cambio enfocado es más fiable que uno enorme que, por casualidad, pasa las pruebas.
El hilo conductor: hacer que el próximo cambio sea más sencillo
A primera vista, estos hábitos parecen no estar relacionados, pero comparten un único objetivo. La buena ingeniería no se trata solo de hacer que el código de hoy funcione; se trata de que las modificaciones de mañana sean menos problemáticas. Cada hábito aporta su contribución:
- Un flujo visible y satisfactorio facilita seguir la lógica.
- Los nombres significativos hacen que la intención sea evidente.
- Los límites alrededor de los sistemas externos contienen el impacto de sus cambios.
- Los tipos precisos excluyen categorías completas de errores.
- Separar las decisiones de las acciones hace que las reglas de negocio sean verificables.
- Los errores estructurados permiten diagnosticar los fallos.
- Los cambios enfocados mantienen la base de código fácil de evolucionar.
Los frameworks y bibliotecas van y vienen, pero estos principios son igualmente aplicables en JavaScript, TypeScript, Python, Java, Go o cualquier otro lenguaje.
Puntos clave
- Cuando termines una función, un modelo, una integración o una solicitud de pull request, pregúntate no solo si funciona, sino también si el próximo desarrollador puede modificarla de forma segura.
- Las cláusulas de protección, los nombres precisos y los tipos exactos reducen la cantidad de información que un lector debe recordar.