Fecha de publicación:

El desarrollo de software enfrenta dos dificultades persistentes: complejidad y requisitos cambiantes. La descomposición ayuda con ambas, pero solo si las partes resultantes resisten el cambio. Este artículo repasa los elementos del código JavaScript, sus interfaces y las fronteras que impiden que un cambio se propague por toda la aplicación.

Elementos e interfaces

En distintos niveles de detalle, JavaScript contiene objetos, funciones, clases, módulos ES y paquetes npm. Objetos, funciones y clases son elementos básicos; módulos y paquetes los combinan.

El desarrollo bottom-up crea paquetes a partir de piezas pequeñas; el top-down recorre el camino inverso. En ambos casos, una buena descomposición busca alta cohesión dentro del componente y bajo acoplamiento entre componentes.

Direccionar código

En aplicaciones ES2015+, el módulo ES es el bloque principal. En navegador se carga mediante una jerarquía de URL; en Node.js es un archivo dentro de un paquete de node_modules.

La dirección a nivel de aplicación tiene dos partes: ruta del módulo y nombre de exportación. Estas son tres alternativas de importación:

import { exportName } from 'https://domain.example/path/to/mod.mjs';
import { exportName } from '../../path/to/mod.mjs';
import { exportName } from '@vendor/project/src/path/to/mod.mjs';

Las direcciones absolutas son más frágiles que las relativas o mapeadas. Los import maps desacoplan al consumidor de la ubicación física. export default también desacopla al consumidor del nombre interno de exportación, aunque limita el módulo a un único export por defecto.

Interfaces en cada nivel

Para una función, nombre, entradas y resultado constituyen la interfaz. Una firma posicional es concisa:

function producer(x1, x2) { return x1 * x2; }

Desestructurar un único objeto da una frontera pública más tolerante al cambio:

function producer({ x1, x2 }) {
  return { sum: x1 + x2, product: x1 * x2 };
}
const { sum, product } = producer({ x1: 1, x2: 2 });

Se pueden añadir campos sin desplazar posiciones. A cambio hay más código y menos legibilidad inmediata, así que el patrón es especialmente valioso en fronteras públicas o duraderas. Las clases exponen nombres, miembros públicos y contratos de método. Un módulo ES expone solo lo que exporta.

Un paquete npm es la unidad reutilizable mayor. Nombre, versión y punto de entrada main/exports forman parte del contrato. Sin restricciones de exportación, un consumidor puede importar archivos internos; por eso las convenciones público/privado deben estar claras.

Áreas públicas y privadas

Todo lo que queda fuera de la interfaz es implementación privada. Una función anidada, un campo privado de clase o una vinculación no exportada puede cambiar sin obligar a cambiar a consumidores:

class SomeClass {
  #privateValue;
  constructor() {
    const internal = {};
    this.useInternal = () => { internal.value = this.#privateValue; };
  }
  setValue(value) { this.#privateValue = value; }
}

Cuanto más comportamiento útil se mantenga privado, menor será la superficie pública y menos dependencias habrá que mantener. Un paquete puede declarar entrada principal y reexportar API prevista; también puede adoptar convenciones de carpetas, por ejemplo Api/ pública y demás módulos internos.

Reglas prácticas

Cuanto mayor es el componente, más importa la estabilidad de nombre e interfaz. Las buenas fronteras no evitan el cambio: mantienen local un cambio local.