Código JavaScript resistente al cambio
Fecha de publicación: 2021-09-22El 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.
- Un objeto expone propiedades.
- Una función expone argumentos de entrada y salida.
- Una clase expone propiedades y métodos públicos.
- Un módulo ES expone su objeto de exportaciones.
- Un paquete npm expone nombre, versión, punto de entrada y, en la práctica, módulos que el consumidor puede alcanzar.
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:
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
- Priorice cohesión alta dentro de un módulo y acoplamiento bajo entre módulos.
- Trate nombres y ubicación de paquetes, módulos, clases y exports como direcciones públicas estables.
- Añada propiedades, argumentos y métodos con más libertad que eliminarlos o renombrarlos; renombrar exige refactor amplio.
- Use entradas/salidas de objeto y desestructuración en fronteras públicas que evolucionan; use argumentos posicionales sin problema en helpers privados.
- Use alias locales para dependencias externas si reducen los lugares atados a un nombre externo.
- Declare reglas público/privado en exports del paquete y convenciones de estructura.
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.
Fragmentos adicionales de código fuente
import func from ‘./mod.mjs’;
import {fn} from ‘./mod.mjs’;
function outer() {
const obj = {};
function inner() {
const obj = {};
}
} > Глубоко вложенные области видимости ухудшают читаемость кода, а использование элементов кода с верхних уровней снижают устойчивость кода к изменениям (за исключением случая, когда элементы кода определяются на самом верхнем уровне — через импорты в es-модуле).
const OBJ = {prop: ‘value’}function consumer() {
console.log(OBJ.prop);
} ### Функции
function producer(x1, x2) {
return x2 * x1;
}const y = producer(1, 2); Я как-то уже размышлял на тему, что было бы, если бы у функции был только один входной параметр и один выходной:
const y = fn(x); JS с его деструктирующим присваиванием вплотную подошёл к этому варианту:
function producer({x1, x2}) {
return {y1: x1 + x2, y2: x2 * x1};
}const {y1, y2} = producer({x1: 1, x2: 2}); > Использование функций в таком виде добавляет коду устойчивости к изменениям относительно “классического” варианта. В этом случае мы можем смелее изменять входные и выходные аргументы (их количество и порядок следования), но платим за это читабельностью кода.
{
“another_dep”: “~2.2.0” } } Имяnpm-пакета участвует в адресацииes-модулей в nodejs-приложениях. В пакете может быть определён входной объект, в его./package.json: { “main”: “lib/entry.js” }
function producer(opts) {
function nested() {}
}
class SomeClass {
const nestedObj = {};
this.instMethod = function () {
nestedObj.prop = this.#privProp;
}
} setProp(data) {
this.#privProp = data;
}
} privProp и nestedObj являются внутренними элементами кода для класса SomeClass и недоступны извне напрямую.
{
“main”: “src/Shared/Container.mjs”
}
import Container from ‘@teqfw/di’;
import subModule from ‘./path/to/sub/modle.mjs’;export {
}
import * as module from ‘@scope/prj’; const sub = module.subModule; Но это именно “джентльменское соглашение” — ничто не мешает разработчику другогоnpm-пакета обратиться внутрь нашегоnpm-пакета к любомуes-модулю напрямую.
export default class TeqFw_Web_Back_Defaults {
constructor(spec) {
this.MOD_DI = spec[‘TeqFw_Di_Back_Defaults$’];
}
}