From 34b8f915381de02f9e13334634455074a5dc55ee Mon Sep 17 00:00:00 2001 From: David Date: Sat, 24 Oct 2026 14:11:48 +0200 Subject: [PATCH] Add testing strategy documentation for software verification practices --- .agents/skills/rdx-solid/SKILL.md | 61 + .../referencias/arquitectura-backend.md | 176 +++ .../referencias/arquitectura-decisiones.md | 191 +++ .../referencias/arquitectura-frontend.md | 144 ++ .../referencias/arquitectura-legacy.md | 143 ++ .../referencias/arquitectura-shared.md | 147 ++ .../rdx-solid/referencias/arquitectura.md | 441 ++++++ .../rdx-solid/referencias/clean-code.md | 1051 ++++++++++++++ .../rdx-solid/referencias/code-smells.md | 925 ++++++++++++ .../rdx-solid/referencias/complejidad.md | 842 +++++++++++ .../referencias/diseño-de-objetos.md | 930 ++++++++++++ .../referencias/patrones-de-diseño.md | 1284 +++++++++++++++++ .../rdx-solid/referencias/principios-solid.md | 804 +++++++++++ .agents/skills/rdx-solid/referencias/tdd.md | 567 ++++++++ .../skills/rdx-solid/referencias/testing.md | 1200 +++++++++++++++ 15 files changed, 8906 insertions(+) create mode 100644 .agents/skills/rdx-solid/SKILL.md create mode 100644 .agents/skills/rdx-solid/referencias/arquitectura-backend.md create mode 100644 .agents/skills/rdx-solid/referencias/arquitectura-decisiones.md create mode 100644 .agents/skills/rdx-solid/referencias/arquitectura-frontend.md create mode 100644 .agents/skills/rdx-solid/referencias/arquitectura-legacy.md create mode 100644 .agents/skills/rdx-solid/referencias/arquitectura-shared.md create mode 100644 .agents/skills/rdx-solid/referencias/arquitectura.md create mode 100644 .agents/skills/rdx-solid/referencias/clean-code.md create mode 100644 .agents/skills/rdx-solid/referencias/code-smells.md create mode 100644 .agents/skills/rdx-solid/referencias/complejidad.md create mode 100644 .agents/skills/rdx-solid/referencias/diseño-de-objetos.md create mode 100644 .agents/skills/rdx-solid/referencias/patrones-de-diseño.md create mode 100644 .agents/skills/rdx-solid/referencias/principios-solid.md create mode 100644 .agents/skills/rdx-solid/referencias/tdd.md create mode 100644 .agents/skills/rdx-solid/referencias/testing.md diff --git a/.agents/skills/rdx-solid/SKILL.md b/.agents/skills/rdx-solid/SKILL.md new file mode 100644 index 0000000..e7818db --- /dev/null +++ b/.agents/skills/rdx-solid/SKILL.md @@ -0,0 +1,61 @@ +--- +name: rdx-solid +description: Aplica principios SOLID, TDD, código limpio, diseño de software y buenas prácticas de ingeniería al escribir código, implementar funcionalidades, refactorizar, diseñar arquitecturas, revisar código, crear pruebas o depurar errores. Prioriza soluciones correctas, sencillas, mantenibles y coherentes con el proyecto, evitando la sobreingeniería y las abstracciones innecesarias. +--- + +# rdx-solid · Ingeniería de software + +Desarrolla y revisa software con una complejidad proporcional a los requisitos. Este archivo establece el flujo de trabajo y orienta la consulta de las referencias; los criterios técnicos y ejemplos se mantienen en ellas. + +## Prioridad y aplicación + +- Respeta las instrucciones del usuario, las restricciones del proyecto, su `AGENTS.md` y las herramientas configuradas. Las preferencias de este skill se aplican cuando no exista una convención más específica. +- Preservar la corrección, los contratos, la seguridad y el alcance de la tarea es obligatorio. Comunica cualquier limitación que impida comprobarlo. +- TDD y las preferencias de diseño son adaptables al contexto. Las métricas, la regla de tres y los patrones son heurísticas, no requisitos numéricos ni motivos para introducir abstracciones. +- Una excepción relevante debe tener una justificación técnica; no es necesario documentar excepciones triviales. + +## Flujo de trabajo + +1. **Comprender:** identifica requisitos, comportamiento existente, consumidores y archivos afectados. Contrasta los supuestos con el código y la documentación; aclara las ambigüedades que puedan cambiar el resultado. +2. **Consultar:** selecciona las referencias de la tabla según el problema. Lee las secciones pertinentes y combina ámbitos cuando el cambio lo requiera; no cargues todos los documentos por defecto. +3. **Implementar:** aplica la solución suficiente más sencilla y mantén los cambios dentro del alcance. Reutiliza las herramientas existentes; no aproveches la tarea para renombrar, reformatear o refactorizar áreas independientes. +4. **Verificar y entregar:** ejecuta las pruebas y comprobaciones de tipos, análisis estático y compilación pertinentes al impacto. Revisa contratos y documentación afectados. Resume cambios, decisiones relevantes y verificaciones; identifica lo pendiente y sus motivos sin atribuir resultados a comprobaciones no ejecutadas. + +## Referencias según la tarea + +Cada documento desarrolla sus criterios, ejemplos y listas de comprobación. Consulta la referencia específica en lugar de reproducir su contenido en este archivo. + +| Necesidad | Referencia | +|---|---| +| Ciclo TDD, triangulación, regresión y trabajo con código sin pruebas | [tdd.md](referencias/tdd.md) | +| Estrategia de pruebas, dobles, contratos, aislamiento y asincronía | [testing.md](referencias/testing.md) | +| Responsabilidades, extensión, sustitución, interfaces y dependencias | [principios-solid.md](referencias/principios-solid.md) | +| Nombres, funciones, control de flujo, errores, comentarios y formato | [clean-code.md](referencias/clean-code.md) | +| Encapsulación, contratos, composición, entidades, objetos de valor y agregados | [diseño-de-objetos.md](referencias/diseño-de-objetos.md) | +| KISS, YAGNI, DRY, diseño simple y deuda técnica | [complejidad.md](referencias/complejidad.md) | +| Detectar y priorizar problemas de diseño y refactorizaciones | [code-smells.md](referencias/code-smells.md) | +| Elegir y aplicar un patrón ante una necesidad real | [patrones-de-diseño.md](referencias/patrones-de-diseño.md) | +| Organización del sistema, límites y selección de estilos arquitectónicos | [arquitectura.md](referencias/arquitectura.md) | +| APIs, persistencia, transacciones, seguridad y operación de servicios | [arquitectura-backend.md](referencias/arquitectura-backend.md) | +| Componentes, estado, formularios, accesibilidad y consumo de APIs | [arquitectura-frontend.md](referencias/arquitectura-frontend.md) | +| Contratos, esquemas, paquetes y compatibilidad entre frontend y backend | [arquitectura-shared.md](referencias/arquitectura-shared.md) | +| Caracterización, modernización, migraciones y compatibilidad de sistemas heredados | [arquitectura-legacy.md](referencias/arquitectura-legacy.md) | +| Comparación de alternativas, ADR y decisiones estructurales | [arquitectura-decisiones.md](referencias/arquitectura-decisiones.md) | + +Para cambios arquitectónicos, parte de `arquitectura.md` y continúa con los ámbitos afectados. Para una corrección o refactorización, combina la guía del área con las de pruebas o diseño que resulten necesarias. + +## Convenciones adicionales + +Las convenciones generales de idioma, nomenclatura y formato se mantienen en `clean-code.md`, sección 9.4. Conserva las del módulo existente cuando difieran y evita normalizaciones ajenas a la tarea. + +Cuando el proyecto no establezca otra norma: + +- Escribe TSDoc en castellano. Usa prefijo `I` en interfaces únicamente si el área del proyecto ya sigue esa convención. +- Guarda la documentación general en `docs/`, con nombres en castellano y `kebab-case`. Actualízala cuando cambien contratos o comportamiento; evita documentación innecesaria para cambios triviales. +- En áreas con i18n, reutiliza sus claves y mecanismos; mantén coherentes `es` y `en` cuando ambos estén soportados. + +## Salvaguardas transversales + +- No modifiques código de terceros, dependencias o archivos generados salvo autorización expresa; respeta las exclusiones del proyecto y utiliza los generadores correspondientes. +- Al contrastar claves no confiables con propiedades propias, utiliza `Object.hasOwn(...)` o `Object.prototype.hasOwnProperty.call(...)`. No uses `in` si las propiedades heredadas no deben admitirse; para `Map`, utiliza `has(...)`. +- Cuando una tarea deba continuar con información incompleta, elige una alternativa conservadora que preserve el comportamiento existente y declara los supuestos materiales. No inventes requisitos, contratos ni resultados de verificación. diff --git a/.agents/skills/rdx-solid/referencias/arquitectura-backend.md b/.agents/skills/rdx-solid/referencias/arquitectura-backend.md new file mode 100644 index 0000000..9b92c24 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/arquitectura-backend.md @@ -0,0 +1,176 @@ +# Arquitectura backend + +> Referencia especializada del skill `rdx-solid`. Consulta primero [arquitectura.md](arquitectura.md) y aplica las convenciones de `SKILL.md` y `AGENTS.md` del proyecto. + +## 1. Propósito y alcance + +Este documento orienta el diseño y evolución de servicios backend, APIs, procesos asíncronos, persistencia e integraciones. Está pensado principalmente para TypeScript y Node.js, pero sus principios son transferibles a otros entornos. + +**Objetivo:** construir servicios correctos, seguros, verificables y mantenibles con una complejidad proporcional a sus requisitos. No impongas arquitecturas hexagonales, DDD ni una capa por concepto cuando no aporten valor. + +## 2. Reconocer la arquitectura existente + +Antes de modificar código: + +1. Identifica framework, estructura modular, puntos de entrada y convenciones. +2. Localiza los casos de uso, modelos, contratos y consumidores afectados. +3. Examina gestión de errores, autenticación, persistencia y pruebas existentes. +4. Comprueba si el cambio afecta a contratos públicos, datos persistidos o transacciones. +5. Limita las modificaciones al ámbito de la tarea. + +Para sistemas heredados, consulta además [arquitectura-legacy.md](arquitectura-legacy.md). Para decisiones estructurales importantes, consulta [arquitectura-decisiones.md](arquitectura-decisiones.md). + +## 3. Organización modular y responsabilidades + +Prefiere módulos funcionales cuando faciliten la cohesión, pero conserva la organización del proyecto si ya resulta adecuada. Un módulo puede combinar presentación, aplicación, dominio e infraestructura sin exigir directorios físicos para todas las capas. + +```text +src/ + modules/ + orders/ + order-routes.ts + create-order.ts + order.ts + order-repository.ts + postgres-order-repository.ts + order-contracts.ts + customers/ + ... + infrastructure/ + database/ + logging/ +``` + +Esta distribución es ilustrativa. Divide un módulo cuando su tamaño o responsabilidades reales lo justifiquen, no porque falten directorios de una plantilla. + +- **Transporte/presentación:** adapta HTTP, CLI, mensajería o RPC a entradas y respuestas de aplicación. +- **Aplicación:** coordina casos de uso, autorización específica y límites transaccionales. +- **Dominio:** aplica reglas, estados e invariantes. +- **Infraestructura:** implementa persistencia, proveedores e integraciones externas. + +Evita controladores que contengan toda la lógica de negocio y servicios que solo deleguen a otros servicios sin responsabilidad concreta. No coloques en `shared` reglas de negocio cuya propiedad corresponde a un módulo. + +## 4. APIs y contratos + +- Diseña endpoints y operaciones alrededor de recursos y casos de uso comprensibles. +- Define formatos de entrada, salida y error explícitos; no expongas entidades de base de datos por conveniencia. +- Valida datos no confiables en el límite de entrada. Los tipos TypeScript no validan JSON en ejecución. +- Utiliza códigos HTTP, semántica de errores y cabeceras apropiados; no devuelvas éxito para ocultar fallos. +- Evalúa paginación, límites, filtrado y orden estable en listados potencialmente grandes. +- Considera versionado y compatibilidad cuando haya consumidores independientes. +- No presupongas que frontend y backend se despliegan simultáneamente. + +```typescript +interface CreateOrderRequest { + customerId: string; + items: Array<{ productId: string; quantity: number }>; +} + +async function createOrderHandler(request: HttpRequest): Promise { + const input = createOrderSchema.parse(request.body); + const result = await createOrder.execute(input); + return { status: 201, body: { orderId: result.orderId } }; +} +``` + +El esquema, `HttpRequest`, `HttpResponse` y el caso de uso son colaboradores ilustrativos. Adapta los contratos a las herramientas del proyecto y no introduzcas validadores duplicados sin necesidad. + +Consulta [arquitectura-shared.md](arquitectura-shared.md) al publicar tipos o esquemas consumidos por frontend. + +## 5. Persistencia y transacciones + +- Modela el acceso a datos según las necesidades del dominio; no crees repositorios genéricos de forma automática. +- Mantén SQL y detalles de drivers fuera de las reglas de negocio cuando su acoplamiento dificulte evolucionar el sistema. +- Utiliza transacciones cuando varias escrituras deban confirmarse o revertirse como una unidad. +- Diseña restricciones de integridad también en la base de datos cuando corresponda. +- Define migraciones reproducibles, compatibles y con procedimientos de recuperación cuando el riesgo lo exija. +- Evita consultas N+1, cargas excesivas e índices especulativos: mide y evalúa el uso real. +- No prometas atomicidad entre base de datos y servicios externos sin un mecanismo que la garantice. + +```typescript +async function confirmOrder(orderId: OrderId): Promise { + await database.transaction(async (tx) => { + const order = await orderRepository.findForUpdate(tx, orderId); + order.confirm(); + await orderRepository.save(tx, order); + await outboxRepository.enqueue(tx, { + type: 'ORDER_CONFIRMED', + orderId: orderId.value, + }); + }); +} +``` + +El ejemplo requiere implementaciones transaccionales compatibles. Outbox es útil cuando hay que publicar eventos fiables tras una confirmación, pero no es obligatorio en sistemas que no lo necesitan. + +## 6. Seguridad y aislamiento + +- Distingue **autenticación** (identidad) de **autorización** (permiso para una operación o recurso). +- Autoriza en el servidor, incluso si la interfaz oculta acciones no permitidas. +- En sistemas multiempresa, comprueba el ámbito de datos y permisos en cada operación relevante; no confíes en un `tenantId` enviado por el cliente sin validación. +- Usa consultas parametrizadas y controles contra inyección. +- Protege secretos mediante mecanismos adecuados; nunca los incluyas en código ni registros. +- Valida límites de tamaño, cargas, archivos y tipos de contenido. +- Aplica privilegio mínimo a cuentas técnicas y conexiones. +- Gestiona información sensible y errores sin filtrar datos internos. + +No elimines controles existentes para simplificar los casos de uso o las pruebas. + +## 7. Asincronía, concurrencia y resiliencia + +- Usa correctamente `async/await`; evita promesas no esperadas salvo trabajo explícitamente desacoplado y supervisado. +- Establece plazos de espera, cancelación y tratamiento de fallos en llamadas remotas. +- Diseña reintentos solo para operaciones seguras o idempotentes, con límites y política de espera. +- Considera claves de idempotencia para operaciones que puedan repetirse por red o colas. +- Evita operaciones bloqueantes costosas en el hilo de eventos de Node.js. +- Define consistencia, orden, duplicados y errores para consumidores de mensajes. +- Protege cambios concurrentes cuando pueden vulnerar invariantes (bloqueos, versiones u otras técnicas adecuadas). +- Distingue fallos transitorios de fallos permanentes; no reintentes indiscriminadamente. + +## 8. Observabilidad y operación + +- Usa logging estructurado donde aporte utilidad diagnóstica. +- Conserva identificadores de correlación cuando existan flujos entre servicios. +- Expón métricas y trazas proporcionales a la criticidad y operación. +- Prepara comprobaciones de salud que reflejen correctamente el estado del servicio. +- Cierra conexiones y recursos de forma controlada. +- Evita registrar credenciales, tokens, datos personales o grandes cargas completas. +- Define procedimientos de despliegue, migración y reversión cuando correspondan. + +## 9. Pruebas y verificación + +Elige pruebas según el riesgo, no por cuotas fijas: + +- **Dominio:** reglas, invariantes y casos límite mediante pruebas rápidas. +- **Aplicación:** coordinación, permisos, tratamiento de errores y transacciones. +- **Infraestructura:** integración real con base de datos, consultas y adaptadores. +- **API:** validación, autenticación, autorización y contratos. +- **E2E:** flujos críticos y efectos entre componentes. + +Prueba especialmente los casos de concurrencia, idempotencia y fallo parcial cuando existan. Consulta [testing.md](testing.md) y [tdd.md](tdd.md). + +## 10. Señales de alerta + +- Controladores con reglas de negocio extensas. +- Entidades que importan directamente controladores o detalles de HTTP. +- Transacciones repartidas entre operaciones sin un responsable definido. +- Servicios que acceden a datos de otra empresa sin comprobar ámbito y permisos. +- Promesas sin supervisión y errores capturados y descartados. +- Operaciones de pago, envío o notificación repetibles sin garantías definidas. +- Repositorios, interfaces y capas creados únicamente por plantilla. +- Un cambio pequeño que obliga a modificar muchos módulos sin motivo real. + +Cada señal exige análisis, no refactorización automática. + +## 11. Lista de comprobación + +- [ ] ¿He identificado el contrato y los consumidores afectados? +- [ ] ¿Las reglas de negocio están en el componente apropiado? +- [ ] ¿Las entradas externas se validan en ejecución? +- [ ] ¿La autorización se comprueba en el servidor y en el ámbito correcto? +- [ ] ¿Las escrituras y llamadas externas tienen garantías claras ante errores? +- [ ] ¿He considerado concurrencia e idempotencia cuando corresponden? +- [ ] ¿Se respeta el alcance y la estructura del proyecto? +- [ ] ¿Se han ejecutado las verificaciones pertinentes y comunicado las pendientes? + +**Principio final:** el backend debe preservar invariantes, contratos y seguridad con el mínimo acoplamiento y complejidad justificados. diff --git a/.agents/skills/rdx-solid/referencias/arquitectura-decisiones.md b/.agents/skills/rdx-solid/referencias/arquitectura-decisiones.md new file mode 100644 index 0000000..81e3eba --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/arquitectura-decisiones.md @@ -0,0 +1,191 @@ +# Decisiones arquitectónicas y ADR + +> Referencia especializada del skill `rdx-solid`. Consulta [arquitectura.md](arquitectura.md) y las referencias del ámbito técnico afectado. + +## 1. Propósito + +Establecer un procedimiento ligero para elegir tecnologías, contratos y estructuras con criterios explícitos. No todas las decisiones requieren un ADR; las decisiones importantes deben poder explicarse y revisarse. + +**Objetivo:** seleccionar una solución suficiente y sostenible para los requisitos reales, documentando sus compromisos sin burocracia innecesaria. + +## 2. Cuándo realizar una evaluación formal + +Considérala cuando una decisión tenga impacto significativo en: + +- Compatibilidad de contratos o datos persistidos. +- Dependencias transversales y límites entre módulos. +- Seguridad, rendimiento, disponibilidad o resiliencia. +- Costes operativos o bloqueo con un proveedor. +- Despliegue y capacidad de reversión. +- Trabajo de varios equipos o evolución a medio plazo. + +Para ajustes internos locales, una breve justificación técnica puede ser suficiente. + +## 3. Proceso de decisión + +1. **Definir el problema:** qué requisito o problema observado exige decidir. +2. **Establecer restricciones:** compatibilidad, seguridad, equipo, presupuesto, plazo y entorno. +3. **Identificar alternativas viables:** incluye mantener la solución actual cuando tenga sentido. +4. **Comparar compromisos:** simplicidad, riesgo, operación, flexibilidad y migración. +5. **Seleccionar:** justificar la opción frente a las alternativas. +6. **Validar:** prototipo, medición o prueba dirigida si existe incertidumbre material. +7. **Documentar:** registrar alcance, consecuencias y condiciones de revisión. + +No simules un análisis exhaustivo si no hay datos: separa lo comprobado de lo supuesto. + +## 4. Criterios de comparación + +| Criterio | Preguntas | +|---|---| +| Adecuación funcional | ¿Resuelve los requisitos y restricciones reales? | +| Complejidad | ¿Cuánto código, infraestructura y conocimiento añade? | +| Compatibilidad | ¿Qué consumidores y formatos deben cambiar? | +| Seguridad | ¿Qué superficie de riesgo introduce? | +| Operación | ¿Cómo se despliega, observa y recupera? | +| Rendimiento | ¿Existen mediciones o límites cuantificados? | +| Equipo | ¿Se puede mantener con las capacidades disponibles? | +| Coste | ¿Licencias, infraestructura, soporte y migración? | +| Reversibilidad | ¿Puede sustituirse o revertirse razonablemente? | + +No asignes ponderaciones numéricas arbitrarias ni presentes estimaciones inciertas como medidas reales. + +## 5. Decisiones especialmente sensibles + +### Arquitectura y modularidad + +Antes de introducir microservicios, Clean Architecture o una nueva capa, comprueba que resuelve un problema real mejor que un monolito modular o una solución directa. + +### Persistencia + +Evalúa integridad, transacciones, consultas reales, capacidades del equipo, migraciones, backup y recuperación. No selecciones un motor solo por tendencias. + +### Dependencias externas + +Comprueba mantenimiento, compatibilidad, licencias, seguridad, tamaño, costes y alternativas nativas o ya instaladas. No agregues bibliotecas para operaciones triviales. + +### Contratos compartidos + +Evalúa versionado, consumidores desplegados, migración y fuente de verdad. Consulta [arquitectura-shared.md](arquitectura-shared.md). + +### Legacy + +Contrasta modernización gradual y sustitución; incluye paridad funcional, datos y reversión. Consulta [arquitectura-legacy.md](arquitectura-legacy.md). + +## 6. Architecture Decision Records (ADR) + +Un ADR registra una decisión arquitectónica significativa y los motivos por los que se tomó. No debe convertirse en una especificación técnica interminable. + +### Cuándo crear un ADR + +- Nuevo motor de base de datos o estrategia de persistencia. +- Cambio en límites modulares o dirección de dependencias. +- Selección de comunicación entre servicios. +- Cambio importante de autenticación o autorización. +- Decisión de migración con impacto sustancial. +- Dependencia estratégica que comprometa mantenimiento y operación. + +No crees ADR para cambios de nombres, pequeñas funciones o refactorizaciones locales sin impacto arquitectónico. + +### Plantilla sugerida + +```markdown +# ADR 001: Título descriptivo + +## Estado +Propuesta | Aceptada | Sustituida | Descartada + +## Contexto +Problema, requisitos y restricciones relevantes. + +## Alternativas consideradas +- Alternativa A: ventajas y costes. +- Alternativa B: ventajas y costes. +- Mantener situación actual, si es viable. + +## Decisión +Opción elegida y razones verificables. + +## Consecuencias +Beneficios, riesgos, migración, operación y costes. + +## Verificación y revisión +Cómo se comprobará y qué condiciones obligarían a revisarla. +``` + +### Ejemplo breve + +```markdown +# ADR 002: PostgreSQL para persistencia transaccional + +## Estado +Aceptada + +## Contexto +Se requieren transacciones, integridad relacional y consultas conocidas. + +## Alternativas consideradas +PostgreSQL, MySQL y mantener el motor actual. + +## Decisión +Adoptar PostgreSQL debido a requisitos concretos y experiencia del equipo. + +## Consecuencias +Migraciones, operación, copias de seguridad y adaptación de repositorios. + +## Verificación y revisión +Pruebas de integración, carga representativa y procedimiento de recuperación. +``` + +No afirmes que una tecnología es superior sin evidencia contextual suficiente. + +## 7. Cómo organizar ADR + +- Guarda las decisiones donde indique el proyecto; si no existe convención, considera `docs/adr/`. +- Utiliza nombres claros y estables, por ejemplo `001-persistencia-postgresql.md`. +- Conserva los ADR históricos y marca los sustituidos en vez de reescribir el historial sin explicarlo. +- Enlaza decisiones relacionadas y responsables cuando el proceso del proyecto lo requiera. +- No dupliques documentación técnica extensa que ya existe en otros documentos. + +## 8. Experimentos y mediciones + +Para decisiones con incertidumbre material: + +- Formula una hipótesis comprobable. +- Define criterios de aceptación y escenarios representativos. +- Mide antes de concluir sobre rendimiento o coste. +- Diferencia datos reales de estimaciones. +- Limita prototipos a aquello que deba validarse. +- No conviertas el prototipo en implementación de producción sin revisar seguridad, errores y operación. + +## 9. Evolución y reversión + +- Identifica consumidores, contratos y datos afectados. +- Planifica migración y coexistencia de versiones cuando corresponda. +- Establece condiciones de reversión si el riesgo lo exige. +- Prevé pruebas y observabilidad para comprobar el resultado. +- Revisa decisiones cuando cambien requisitos o aparezca evidencia relevante. + +No cambies una decisión vigente únicamente porque otra solución sea más moderna. + +## 10. Conducta del agente de programación + +- No inventes requisitos ni datos técnicos no comprobados. +- Expón supuestos materiales si la tarea debe continuar sin aclaración. +- Distingue propuesta, decisión aprobada e implementación realizada. +- No modifiques grandes áreas del sistema para aplicar unilateralmente una preferencia arquitectónica. +- Respeta `AGENTS.md`, contratos y restricciones específicas del proyecto. +- Comunica alternativas y riesgos de forma proporcional a la importancia de la decisión. +- Nunca declares verificados resultados de pruebas o benchmarks que no se hayan ejecutado. + +## 11. Lista de comprobación + +- [ ] ¿El problema y las restricciones están claramente definidos? +- [ ] ¿Se consideró mantener la solución actual? +- [ ] ¿Las alternativas son viables en este proyecto? +- [ ] ¿Se compararon costes, riesgos y mantenibilidad? +- [ ] ¿Se ha diferenciado evidencia de hipótesis? +- [ ] ¿La decisión preserva contratos y requisitos relevantes? +- [ ] ¿Necesita ADR o basta una justificación breve? +- [ ] ¿Se definieron verificación y revisión cuando correspondía? + +**Principio final:** una buena decisión arquitectónica es explícita, contextual, verificable y proporcional al problema que resuelve. diff --git a/.agents/skills/rdx-solid/referencias/arquitectura-frontend.md b/.agents/skills/rdx-solid/referencias/arquitectura-frontend.md new file mode 100644 index 0000000..2fc02c7 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/arquitectura-frontend.md @@ -0,0 +1,144 @@ +# Arquitectura frontend + +> Referencia especializada del skill `rdx-solid`. Consulta [arquitectura.md](arquitectura.md) y respeta las convenciones de `SKILL.md` y `AGENTS.md`. + +## 1. Propósito + +Organizar interfaces mantenibles, accesibles y verificables. Los ejemplos están orientados a React y TypeScript; adapta los criterios al framework utilizado. No reproduzcas automáticamente las capas del backend dentro del navegador. + +## 2. Partir de la estructura del proyecto + +Antes de modificar componentes, examina: enrutado, diseño visual, biblioteca de UI, convención de carpetas, gestión de estado, formularios, consumo de APIs, i18n y pruebas. Respeta los límites existentes y evita migraciones de arquitectura no solicitadas. + +## 3. Organización por funcionalidades + +Agrupa componentes, hooks, servicios y tipos de una funcionalidad cuando ello reduzca la dispersión. No crees todas las subcarpetas por defecto. + +```text +src/ + features/ + orders/ + components/ + order-list.tsx + order-form-dialog.tsx + hooks/ + use-orders.ts + api/ + orders-client.ts + index.ts + shared/ + ui/ + hooks/ +``` + +- `shared/` contiene elementos realmente reutilizables, no un cajón para código sin dueño. +- Mantén públicas solo las APIs que otros módulos deban consumir; evita importar detalles internos entre funcionalidades. +- No crees un componente por cada etiqueta HTML: la extracción debe representar una responsabilidad, reutilización o complejidad significativa. +- No concentres numerosos componentes independientes en un solo archivo si dificulta comprenderlos o mantenerlos. +- Modales y diálogos con lógica propia deben tender a componentes separados; un cuadro trivial puede permanecer local cuando resulte más claro. + +## 4. Componentes y responsabilidades + +- Prefiere composición sobre jerarquías de herencia de componentes. +- Mantén la lógica compleja de dominio fuera de la representación visual. +- Utiliza props explícitas y contratos de componentes comprensibles. +- Evita convertir cada componente en un conjunto de capas y controladores. +- Extrae hooks cuando encapsulen lógica reactiva reutilizable o significativa; no para ocultar una única llamada simple. +- No introduzcas renderizado derivado con estado duplicado cuando pueda calcularse a partir de props o estado existente. + +```tsx +interface OrderSummaryProps { + order: OrderSummary; + onSelect: (orderId: string) => void; +} + +export function OrderSummaryCard({ order, onSelect }: OrderSummaryProps) { + return ( +
+

{order.reference}

+ +
+ ); +} +``` + +El ejemplo presupone un tipo de presentación `OrderSummary` y una interfaz accesible que puede requerir más detalles según el diseño. + +## 5. Estado y datos remotos + +Distingue: + +- **Estado local:** interacción de un componente o pequeño subárbol. +- **Estado compartido de UI:** necesidades de varios componentes relacionados. +- **Estado remoto:** datos obtenidos del servidor, con carga, error y actualización. +- **Estado derivado:** valores calculables que normalmente no deben almacenarse por duplicado. + +No introduzcas un gestor global de estado para resolver un formulario aislado. Para datos remotos, reutiliza las bibliotecas de consulta, caché e invalidación que ya utiliza el proyecto. Evita duplicar en estado global la caché del servidor sin motivo. + +Controla carreras entre solicitudes, cancelación, refresco y estados vacíos cuando corresponda. + +## 6. Formularios y cuadros de diálogo + +- Conserva validaciones de experiencia de usuario en frontend y la validación autoritativa en backend. +- Muestra errores comprensibles y asociados al campo o acción pertinente. +- Considera guardado pendiente, reintentos, cambios sin guardar y envío duplicado. +- Mantén los formularios complejos y modales con responsabilidad propia en componentes independientes. +- Respeta foco, navegación por teclado, cierre, etiquetas y mensajes accesibles. +- Reutiliza los componentes del sistema de diseño y no edites paquetes de terceros para implementar personalizaciones locales. + +## 7. Acceso a APIs y contratos + +- Centraliza donde corresponda la adaptación de respuestas, autenticación y errores de transporte. +- No expongas directamente modelos internos de la base de datos como modelos de UI. +- Maneja explícitamente carga, vacío, éxito y error. +- No confíes en tipos estáticos para garantizar la forma de datos remotos en ejecución. +- Evita mezclar llamadas HTTP y transformación compleja dentro de componentes de presentación. +- No asumas que frontend y backend se actualizan al mismo tiempo; mantén compatibilidad según sus contratos. + +Consulta [arquitectura-shared.md](arquitectura-shared.md) para DTO, tipos, esquemas y generación de clientes. + +## 8. Accesibilidad, i18n y seguridad + +- Utiliza HTML semántico, etiquetas, navegación por teclado y gestión de foco. +- Prueba estados accesibles de carga, error y modales. +- No insertes textos literales en áreas gobernadas por i18n; conserva `es` y `en` cuando sean idiomas soportados. +- Trata contenido no confiable de forma segura; evita inyección de HTML. +- No almacenes secretos de servidor en código distribuido al navegador. +- La ocultación de botones no sustituye la autorización del backend. + +## 9. Rendimiento + +Optimiza basándote en evidencia: identifica re-renderizados costosos, bundles grandes, rutas pesadas y solicitudes redundantes. No añadas memoización a todos los componentes por rutina. Utiliza división de código, virtualización o carga diferida cuando el volumen real lo requiera. Mantén claves estables en listas. + +## 10. Pruebas + +- Prueba comportamientos percibidos por el usuario en lugar de detalles de implementación. +- Comprueba formularios, validaciones, errores, roles y accesibilidad relevante. +- Utiliza pruebas de componentes e integración para flujos locales; E2E para recorridos críticos. +- No hagas que pruebas de UI dependan innecesariamente de detalles internos de hooks. +- Verifica los contratos de API cuando cambien. + +## 11. Señales de alerta + +- Componentes enormes con estado, transporte y reglas mezclados. +- Estado remoto duplicado en varios almacenes. +- Hooks extraídos sin responsabilidad concreta. +- `shared/` que contiene reglas exclusivas de una funcionalidad. +- Múltiples diálogos complejos incrustados en un componente contenedor. +- Uso de `useEffect` para derivar valores calculables directamente. +- Lógica de autorización exclusivamente en frontend. +- Optimizaciones preventivas que dificultan la lectura. + +## 12. Lista de comprobación + +- [ ] ¿La modificación respeta las convenciones de React y del repositorio? +- [ ] ¿Cada componente y hook mantiene una responsabilidad clara? +- [ ] ¿El estado pertenece al ámbito apropiado? +- [ ] ¿Los formularios y diálogos son accesibles y verificables? +- [ ] ¿Se respetan i18n, seguridad y contratos de API? +- [ ] ¿He evitado abstracciones o librerías nuevas sin necesidad? +- [ ] ¿Las pruebas cubren el comportamiento modificado? + +**Principio final:** el frontend debe expresar flujos de usuario claros sin convertir la interfaz en un conjunto de abstracciones innecesarias. diff --git a/.agents/skills/rdx-solid/referencias/arquitectura-legacy.md b/.agents/skills/rdx-solid/referencias/arquitectura-legacy.md new file mode 100644 index 0000000..d4ad521 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/arquitectura-legacy.md @@ -0,0 +1,143 @@ +# Arquitectura y evolución de sistemas heredados (Legacy) + +> Referencia especializada del skill `rdx-solid`. Consulta [arquitectura.md](arquitectura.md) y el documento del ámbito afectado: [arquitectura-backend.md](arquitectura-backend.md), [arquitectura-frontend.md](arquitectura-frontend.md) o [arquitectura-shared.md](arquitectura-shared.md). + +## 1. Propósito + +Modernizar y mantener sistemas existentes con seguridad, preservando comportamientos y contratos requeridos. «Legacy» no significa necesariamente código de baja calidad: puede ser software estable, valioso y escasamente documentado. + +**Prioridad:** comprender antes de cambiar; verificar antes de declarar el cambio correcto. No imponer patrones o nomenclaturas nuevas de forma masiva. + +## 2. Reconocimiento inicial + +1. Identifica límites del sistema, ejecutables, servicios y consumidores. +2. Revisa dependencias, versiones, compilación y despliegue. +3. Localiza contratos públicos, formatos de intercambio y persistencia. +4. Identifica comportamientos implícitos, trabajos programados y efectos secundarios. +5. Comprueba las pruebas existentes y los mecanismos de recuperación. +6. Delimita la tarea, sus riesgos y los componentes afectados. + +No presupongas que un comportamiento aparentemente extraño es un error: puede ser un requisito histórico o un contrato con consumidores externos. + +## 3. Caracterización del comportamiento + +Cuando falten especificaciones o pruebas, utiliza ejemplos de entrada y salida, datos anonimizados, consultas, trazas o pruebas de caracterización para describir el comportamiento actual. + +Distingue: + +- **Comportamiento a conservar:** contratos y resultados vigentes necesarios. +- **Defecto a corregir:** fallo confirmado respecto al comportamiento esperado. +- **Cambio funcional:** nuevo requisito que modifica la semántica. + +No conviertas automáticamente un fallo conocido en una obligación permanente mediante una prueba de caracterización. + +## 4. Refactorización incremental + +- Realiza cambios pequeños y reversibles cuando sea posible. +- Separa las correcciones funcionales de las refactorizaciones generales. +- Introduce pruebas de regresión en torno a comportamientos críticos cuando sea viable. +- No renombres, reformatees o muevas grandes áreas ajenas a la tarea. +- Conserva convenciones y límites de módulos existentes salvo necesidad justificada. +- Utiliza adaptadores temporales si permiten migrar consumidores gradualmente. +- Evita reescrituras completas sin evaluación de costes, riesgos y continuidad del servicio. + +La ausencia de pruebas no prohíbe todo cambio: exige mecanismos de verificación proporcionales, mayor cautela y documentación de incertidumbres. + +## 5. Estrategias de modernización + +| Estrategia | Cuándo considerarla | Riesgo a controlar | +|---|---|---| +| Refactorización localizada | Corregir estructura sin cambiar contratos | Regresiones involuntarias | +| Adaptador / fachada | Aislar un contrato antiguo | Traducción semántica incorrecta | +| Capa anticorrupción | Separar modelos de dominio antiguo y nuevo | Complejidad de mapeo | +| Strangler Fig | Sustituir capacidades progresivamente | Enrutamiento y coexistencia | +| Migración por etapas | Cambiar persistencia o infraestructura | Consistencia y reversión | +| Sustitución completa | Sistema realmente inviable de mantener | Alcance, datos y paridad funcional | + +Escoge la opción con menor riesgo y coste total razonable; no asumas que reescribir equivale a mejorar. + +## 6. Capas de adaptación y anticorrupción + +Una capa anticorrupción evita que el nuevo dominio adopte conceptos o errores del modelo antiguo. + +```typescript +interface CustomerDirectory { + findCustomer(id: CustomerId): Promise; +} + +class LegacyCustomerAdapter implements CustomerDirectory { + constructor(private readonly legacyApi: LegacyCustomerApi) {} + + async findCustomer(id: CustomerId): Promise { + const legacyRecord = await this.legacyApi.readCustomer(id.value); + return legacyRecord ? mapLegacyCustomer(legacyRecord) : null; + } +} +``` + +Los tipos y colaboradores son ilustrativos. Verifica diferencias de identificación, nulos, fechas, moneda, errores y codificación: un mapeo estructural no garantiza equivalencia semántica. + +No crees una capa anticorrupción cuando un adaptador sencillo sea suficiente. + +## 7. Migración de datos y persistencia + +- Determina esquemas, claves, restricciones, volumen y dependencias. +- Diseña migraciones reproducibles con validaciones de integridad. +- Evalúa ventanas de mantenimiento, backups y recuperación. +- Considera expansión-contracción para cambios de esquema compatibles. +- Si existe doble escritura, define fallos, reconciliación y fuente de verdad; no la adoptes sin necesidad. +- Verifica volumen, rendimiento y compatibilidad con versiones desplegadas. +- Evita transformaciones destructivas sin un procedimiento probado o adecuado de recuperación. + +## 8. Contratos, compatibilidad y despliegue + +Identifica qué consumidores pueden quedar afectados: clientes externos, informes, procesos por lotes, integraciones, archivos de intercambio, aplicaciones móviles o servicios anteriores. + +- Mantén formatos y significado de campos cuando sea requerido. +- Utiliza versionado o adaptadores si los consumidores no pueden migrar a la vez. +- Controla coexistencia de versiones y despliegues escalonados. +- Define condiciones de reversión cuando el riesgo lo exija. +- No elimines endpoints o campos aparentemente no usados sin verificar consumidores externos. + +## 9. Dependencias y tecnologías obsoletas + +- Determina compatibilidad y riesgos de seguridad de runtimes, librerías y drivers. +- No actualices todas las dependencias durante una corrección puntual sin necesidad. +- Prioriza vulnerabilidades o incompatibilidades relevantes según su impacto. +- Realiza actualizaciones por etapas y verifica sus contratos. +- Documenta dependencias no sustituibles y mecanismos de mitigación. + +## 10. Pruebas y seguimiento + +- Pruebas de caracterización para comportamiento existente. +- Regresión para defectos corregidos. +- Integración para adaptadores y persistencia. +- Contrato para consumidores externos y coexistencia. +- E2E para flujos de negocio críticos. +- Comparación controlada de resultados antiguo/nuevo cuando sea viable. + +Evita utilizar exclusivamente métricas de cobertura como garantía de migración. Observa errores, tiempos de respuesta, integridad de datos y resultados funcionales tras un cambio cuando el entorno lo permita. + +## 11. Señales de alerta + +- Reescribir todo antes de conocer el comportamiento existente. +- Confundir limpieza estética con mejora arquitectónica necesaria. +- Modificar contratos sin inventariar consumidores. +- Migrar datos sin validar integridad ni recuperación. +- Repartir reglas inconsistentes entre implementación antigua y nueva. +- Eliminar lógica aparentemente redundante con efectos históricos desconocidos. +- Introducir frameworks nuevos para una corrección localizada. +- Declarar paridad funcional sin pruebas o evidencias suficientes. + +## 12. Lista de comprobación + +- [ ] ¿Conozco comportamiento, contratos y consumidores afectados? +- [ ] ¿He distinguido defectos de comportamientos históricos a conservar? +- [ ] ¿La estrategia de migración es proporcional al riesgo? +- [ ] ¿Existe verificación antes y después del cambio? +- [ ] ¿La integridad de los datos y la compatibilidad están protegidas? +- [ ] ¿Se han previsto reversión o recuperación cuando corresponda? +- [ ] ¿He evitado reestructuraciones ajenas a la tarea? +- [ ] ¿He comunicado incertidumbres y verificaciones pendientes? + +**Principio final:** modernizar significa reducir el riesgo y mejorar la capacidad de evolución, no reescribir por preferencia tecnológica. diff --git a/.agents/skills/rdx-solid/referencias/arquitectura-shared.md b/.agents/skills/rdx-solid/referencias/arquitectura-shared.md new file mode 100644 index 0000000..e7dcd14 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/arquitectura-shared.md @@ -0,0 +1,147 @@ +# Arquitectura del código compartido entre frontend y backend + +> Referencia especializada del skill `rdx-solid`. Consulta [arquitectura.md](arquitectura.md) y, según la tarea, [arquitectura-backend.md](arquitectura-backend.md) y [arquitectura-frontend.md](arquitectura-frontend.md). + +## 1. Propósito + +Compartir **contratos y lógica realmente reutilizable** sin acoplar el navegador a implementaciones internas del servidor. Compartir código no equivale a compartir todas las entidades, dependencias o políticas del sistema. + +## 2. Qué puede compartirse + +| Candidato | Condiciones | +|---|---| +| DTO y contratos públicos | Deben describir el intercambio entre aplicaciones, no tablas internas. | +| Tipos y enumeraciones contractuales | Semántica común y política de compatibilidad explícita. | +| Esquemas de transporte | Independientes de secretos y recursos de servidor. | +| Funciones puras | Sin dependencias del entorno ni efectos secundarios. | +| Formatos e identificadores interoperables | Responsabilidad compartida, validación adecuada. | +| Clientes generados de API | Fuente de verdad definida y versión compatible. | + +Evita compartir directamente repositorios, servicios de dominio internos, modelos ORM, credenciales, acceso a archivos de servidor, lógica de autorización exclusivamente backend o tipos que dependan de Node.js cuando se ejecutarán en el navegador. + +## 3. Diseñar contratos explícitos + +Un contrato de API es una representación de intercambio. No tiene por qué coincidir con la entidad de dominio, el modelo de persistencia ni el modelo de presentación. + +```typescript +export interface OrderResponse { + id: string; + status: 'pending' | 'confirmed'; + totalMinorUnits: number; + currency: string; +} +``` + +Un `OrderResponse` puede ser compartido entre aplicaciones; la entidad `Order` del backend puede mantener invariantes y objetos de valor internos diferentes. Evita filtrar propiedades privadas o campos de base de datos al contrato público. + +## 4. Validación y límites de confianza + +- TypeScript comprueba tipos durante el desarrollo, pero no valida datos externos en ejecución. +- Valida datos que cruzan límites de confianza, especialmente en el servidor. +- Puedes compartir esquemas de intercambio cuando ahorre duplicación y sean compatibles con ambos entornos. +- No consideres que la validación de formulario sustituye la validación del servidor. +- Diferencia validación de transporte de reglas de negocio y autorización. + +```typescript +// Ejemplo conceptual con una biblioteca de esquemas ya adoptada. +export const createOrderSchema = schema.object({ + customerId: schema.string(), + items: schema.array( + schema.object({ + productId: schema.string(), + quantity: schema.number().int().positive(), + }), + ), +}); + +export type CreateOrderInput = Infer; +``` + +`schema` e `Infer` representan APIs ilustrativas; utiliza las de la biblioteca real del proyecto. No añadas una dependencia nueva exclusivamente por este ejemplo. + +## 5. Estructura de paquetes + +```text +packages/ + contracts/ + src/ + orders/ + order-response.ts + create-order-request.ts + index.ts + shared-utils/ + src/ + format-identifier.ts +apps/ + api/ + web/ +``` + +Este esquema es opcional. Un proyecto pequeño puede compartir tipos sin múltiples paquetes; un monorepo grande puede requerir separación adicional. No crees paquetes vacíos ni abstraigas antes de conocer los consumidores. + +## 6. Dirección de dependencias + +- El paquete compartido no debe importar código de `apps/api` ni de `apps/web`. +- Frontend y backend pueden depender de contratos compartidos, sin dependencia recíproca entre sus implementaciones. +- Evita ciclos y dependencias transitivas que incluyan recursos exclusivos de Node.js en bundles de navegador. +- Usa puntos de entrada públicos y exportaciones explícitas cuando aporten límites estables. +- Separa código específico de entorno cuando sea necesario; no confíes en condiciones de ejecución que permitan empaquetar secretos. + +```text +apps/web ────┐ + ├──► packages/contracts +apps/api ────┘ +``` + +## 7. Versionado y compatibilidad + +- Trata los contratos compartidos como contratos públicos cuando los consumidores se despliegan por separado. +- Considera cambios aditivos, campos opcionales y estrategias de transición compatibles. +- Evita cambiar el significado de un campo sin una migración explícita. +- Define versiones y política de publicación cuando exista distribución independiente. +- No asumas que actualizar una dependencia compartida actualiza automáticamente a todos sus consumidores. +- Considera pruebas de contrato o compatibilidad entre versiones cuando el riesgo lo justifique. + +## 8. Generación frente a mantenimiento manual + +Puedes generar tipos y clientes desde OpenAPI, JSON Schema u otra fuente contractual si reduce divergencias. Debe existir **una fuente de verdad** identificable. + +- No edites manualmente archivos generados. +- Evita mantener simultáneamente tipos manuales y generados con significado idéntico sin necesidad. +- Comprueba la estabilidad del generador, su coste y la calidad del resultado. +- No introduzcas generación de código para dos tipos sencillos que evolucionan juntos sin problemas. + +## 9. Reutilización y propiedad + +Antes de mover código a un paquete compartido: + +1. Identifica consumidores reales. +2. Comprueba que representa el mismo conocimiento o contrato. +3. Verifica compatibilidad de entorno y dependencias. +4. Define quién mantiene el contrato. +5. Comprueba que el movimiento no hace más difíciles cambios locales. + +No confundas duplicación accidental con dos reglas similares pertenecientes a ámbitos de negocio diferentes. + +## 10. Riesgos habituales + +- Exponer entidades ORM al frontend. +- Empaquetar módulos de Node.js en el bundle del navegador. +- Compartir lógica de autorización que depende de contexto de servidor y utilizarla como control de seguridad en cliente. +- Cambiar contratos sin verificar consumidores desplegados. +- Crear `common` o `shared` como contenedor de responsabilidades inconexas. +- Generar importaciones circulares mediante archivos barril. +- Acoplar pruebas de frontend a implementaciones internas de backend. + +## 11. Verificación y lista de comprobación + +- [ ] ¿El código compartido expresa un contrato o comportamiento realmente común? +- [ ] ¿No importa implementaciones específicas de frontend o backend? +- [ ] ¿Es compatible con los entornos donde se ejecutará? +- [ ] ¿La validación de datos externos está situada en límites correctos? +- [ ] ¿Se han preservado contratos públicos y compatibilidad de versiones? +- [ ] ¿La fuente de verdad del esquema está definida? +- [ ] ¿No se han exportado secretos ni detalles internos? +- [ ] ¿Se han comprobado builds, tipos y pruebas de ambos consumidores afectados? + +**Principio final:** comparte contratos y conocimiento estable, no el acoplamiento entre implementaciones. diff --git a/.agents/skills/rdx-solid/referencias/arquitectura.md b/.agents/skills/rdx-solid/referencias/arquitectura.md new file mode 100644 index 0000000..48a29e9 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/arquitectura.md @@ -0,0 +1,441 @@ +# Arquitectura de software (Software Architecture) + +La arquitectura de software define cómo se organizan los componentes de un sistema, qué responsabilidades asumen, cómo colaboran y qué dependencias existen entre ellos. + +Su objetivo es facilitar el desarrollo, mantenimiento y evolución del software, respetando sus requisitos funcionales y no funcionales. + +Este documento desarrolla los principios establecidos en el `SKILL.md` principal y constituye el punto de entrada a las recomendaciones arquitectónicas especializadas. + +**La arquitectura debe adaptarse a las necesidades reales del proyecto. No existe un estilo arquitectónico universalmente adecuado para todos los sistemas.** + +## 1. Objetivos de la arquitectura + +Una arquitectura adecuada debe permitir: + +1. **Añadir** funcionalidades con un esfuerzo razonable. +2. **Modificar** comportamientos existentes de forma segura. +3. **Eliminar** funcionalidades sin provocar dependencias innecesarias. +4. **Probar** los comportamientos mediante mecanismos de verificación apropiados. +5. **Desplegar** el sistema de forma fiable, incluyendo despliegues independientes cuando sean necesarios. +6. **Comprender** las responsabilidades y relaciones entre componentes. +7. **Evolucionar** el sistema sin introducir complejidad accidental desproporcionada. + +También debe responder a los requisitos no funcionales relevantes: + +- Seguridad. +- Rendimiento. +- Escalabilidad. +- Disponibilidad. +- Integridad y consistencia de datos. +- Observabilidad. +- Compatibilidad. +- Recuperación ante fallos. +- Coste de operación y mantenimiento. + +**La calidad arquitectónica debe evaluarse respecto a los requisitos del sistema, no por la cantidad de capas, módulos, interfaces o patrones utilizados.** + +--- + +## 2. Principios arquitectónicos generales + +### 2.1. Responsabilidades claras + +Organiza el sistema mediante componentes con responsabilidades coherentes. + +- Agrupa los elementos que evolucionen conjuntamente. +- Separa responsabilidades que cambien por motivos independientes. +- Evita componentes que acumulen funcionalidades sin relación clara. +- No fragmentes el código en clases o módulos que no aporten una responsabilidad real. + +Aplica el principio de responsabilidad única (SRP) con criterio profesional. + +### 2.2. Cohesión y acoplamiento + +Favorece componentes con alta cohesión y dependencias controladas. + +- Mantén próximos los elementos relacionados. +- Evita dependencias innecesarias entre funcionalidades. +- Reduce el conocimiento de detalles internos ajenos. +- Define contratos cuando aporten estabilidad o desacoplamiento. +- Evita interfaces y capas que únicamente deleguen operaciones sin añadir valor. + +**El objetivo no es eliminar todas las dependencias, sino mantener únicamente las necesarias y apropiadas.** + +### 2.3. Organización modular + +Utiliza límites modulares cuando faciliten la comprensión y evolución del sistema. + +Una organización puede basarse en: + +- Funcionalidades. +- Dominios de negocio. +- Responsabilidades técnicas. +- Capas. +- Subsistemas. +- Una combinación de estos criterios. + +Prefiere la organización por funcionalidades cuando permita mantener cohesionados los componentes relacionados. + +No obstante: + +- No reorganices automáticamente un proyecto existente. +- No impongas módulos independientes para operaciones triviales. +- No introduzcas estructuras de directorios que no aporten claridad. +- Respeta los límites y convenciones existentes. + +### 2.4. Separación de responsabilidades técnicas + +Distingue las responsabilidades de presentación, aplicación, dominio e infraestructura cuando el sistema lo justifique. + +- **Presentación:** interacción con usuarios o consumidores. +- **Aplicación:** coordinación de casos de uso. +- **Dominio:** reglas, conceptos e invariantes de negocio. +- **Infraestructura:** persistencia, comunicaciones e integraciones externas. + +Estas responsabilidades no obligan a crear cuatro capas físicas en todas las aplicaciones. + +**La separación conceptual es más importante que la cantidad de directorios o clases utilizados.** + +### 2.5. Dirección de las dependencias + +Evita que las reglas de negocio dependan innecesariamente de detalles técnicos. + +En arquitecturas limpias, hexagonales o equivalentes: + +- Las dependencias del código deben orientarse hacia las políticas de mayor nivel. +- La infraestructura puede implementar contratos definidos en la aplicación o el dominio. +- Los componentes internos deben permanecer desacoplados de tecnologías externas cuando corresponda. + +Distingue entre: + +- **Dependencias del código:** qué módulos conocen y utilizan otros módulos. +- **Flujo de ejecución:** qué componentes intervienen al ejecutar una operación. + +Ambos pueden seguir direcciones diferentes. + +No introduzcas interfaces únicamente para cumplir formalmente una regla de dependencias. + +### 2.6. Contratos y compatibilidad + +Los contratos delimitan cómo colaboran los componentes. + +Pueden expresarse mediante: + +- Interfaces y tipos. +- Funciones. +- APIs. +- Eventos. +- Esquemas de datos. +- Protocolos. +- Pruebas de contrato. + +Antes de modificar un contrato público: + +1. Identifica sus consumidores. +2. Comprueba su comportamiento actual. +3. Evalúa el impacto del cambio. +4. Mantén la compatibilidad requerida. +5. Define una estrategia de migración cuando sea necesaria. +6. Verifica las implementaciones afectadas. + +No presupongas que compartir una interfaz garantiza que dos implementaciones sean funcionalmente sustituibles. + +### 2.7. Arquitectura proporcional + +Selecciona la estructura arquitectónica según los requisitos reales. + +No impongas automáticamente: + +- Clean Architecture. +- Arquitectura hexagonal. +- Microservicios. +- Arquitectura orientada a eventos. +- Múltiples capas de servicios. +- Contenedores de inyección de dependencias. +- Repositorios para cualquier acceso a datos. + +Estos mecanismos pueden aportar ventajas cuando existe una necesidad identificada. + +**Una arquitectura sencilla y modular puede ser preferible a una estructura más sofisticada que incremente innecesariamente el coste de mantenimiento.** + +--- + +## 3. Estilos arquitectónicos + +Conoce los estilos arquitectónicos habituales y selecciona el más adecuado según el contexto. + +| Estilo | Características | Cuándo considerarlo | +|---|---|---| +| **Arquitectura por capas** | Organiza responsabilidades técnicas en capas. | Aplicaciones con flujos y responsabilidades relativamente sencillos. | +| **Organización por funcionalidades** | Agrupa los componentes relacionados con una capacidad. | Sistemas con funcionalidades que evolucionan independientemente. | +| **Monolito modular** | Módulos cohesionados dentro de una unidad de despliegue. | Aplicaciones empresariales que necesitan modularidad sin distribución operativa. | +| **Arquitectura hexagonal** | Utiliza puertos y adaptadores para desacoplar mecanismos externos. | Sistemas con integraciones o dependencias de infraestructura relevantes. | +| **Clean Architecture** | Separa políticas de negocio y detalles técnicos mediante límites explícitos. | Sistemas cuyo dominio y necesidades de evolución justifican esa separación. | +| **Microservicios** | Distribuye capacidades en servicios desplegables independientemente. | Sistemas con necesidades reales de autonomía, escalabilidad u operación distribuida. | +| **Arquitectura orientada a eventos** | Comunica componentes mediante eventos. | Procesos asíncronos o desacoplamiento temporal justificado. | + +Los estilos no son necesariamente excluyentes. + +Por ejemplo, un monolito modular puede organizarse por funcionalidades y aplicar principios hexagonales en determinados módulos. + +### Criterios de selección + +Antes de adoptar una arquitectura: + +- Identifica los requisitos funcionales. +- Evalúa los requisitos no funcionales. +- Considera el tamaño y la evolución prevista del sistema. +- Analiza las capacidades del equipo. +- Evalúa el coste de desarrollo y operación. +- Considera los riesgos de migración. +- Comprueba si la arquitectura existente ya satisface las necesidades. + +No cambies de estilo arquitectónico únicamente por tendencias tecnológicas. + +--- + +## 4. Arquitectura en proyectos existentes + +Al trabajar sobre un proyecto existente: + +1. Identifica su estructura y estilo arquitectónico actual. +2. Examina sus módulos, contratos y convenciones. +3. Comprende el comportamiento que debe preservarse. +4. Localiza los componentes directamente afectados. +5. Evalúa el impacto de las modificaciones. +6. Introduce cambios incrementales. +7. Verifica los resultados. + +### Reglas de evolución + +- Respeta los límites existentes salvo que exista una necesidad justificada de modificarlos. +- No introduzcas nuevas capas para una modificación puntual si no son necesarias. +- No combines cambios funcionales con reestructuraciones arquitectónicas extensas. +- Evita modificar componentes ajenos al alcance. +- Mantén compatibilidad con consumidores existentes cuando corresponda. +- Documenta las decisiones arquitectónicas significativas. + +**Una arquitectura existente debe mejorarse para resolver problemas reales, no sustituirse automáticamente por otra considerada más moderna.** + +--- + +## 5. Documentación arquitectónica especializada + +Las recomendaciones específicas se encuentran distribuidas en documentos independientes. + +**Consulta únicamente los documentos relevantes para la tarea, combinándolos cuando sea necesario.** + +### 5.1. Arquitectura backend + +**[arquitectura-backend.md](arquitectura-backend.md)** + +Consulta este documento cuando trabajes con: + +- Servicios backend. +- Casos de uso. +- Lógica de dominio. +- APIs. +- Persistencia. +- Transacciones. +- Autenticación y autorización del servidor. +- Procesamiento asíncrono. +- Eventos y mensajería. +- Integraciones externas. +- Observabilidad y operaciones. + +Incluye recomendaciones para organizar módulos y servicios backend sin introducir capas o abstracciones innecesarias. + +### 5.2. Arquitectura frontend + +**[arquitectura-frontend.md](arquitectura-frontend.md)** + +Consulta este documento cuando trabajes con: + +- Componentes de interfaz. +- Aplicaciones React u otros frameworks frontend. +- Organización por funcionalidades. +- Hooks y lógica de presentación. +- Gestión del estado. +- Formularios y cuadros de diálogo. +- Consumo de APIs. +- Internacionalización. +- Accesibilidad. +- Rendimiento y pruebas de interfaz. + +Incluye criterios para evitar componentes monolíticos y separar responsabilidades sin trasladar innecesariamente patrones propios del backend. + +### 5.3. Código compartido entre backend y frontend + +**[arquitectura-shared.md](arquitectura-shared.md)** + +Consulta este documento cuando trabajes con: + +- Paquetes compartidos. +- Monorepos. +- Tipos y contratos utilizados por frontend y backend. +- Esquemas de validación. +- DTO. +- Enumeraciones contractuales. +- Utilidades comunes. +- Generación de tipos. +- Compatibilidad entre aplicaciones. +- Dependencias entre paquetes. + +Su objetivo es permitir una reutilización segura sin acoplar el frontend a implementaciones internas del backend. + +### 5.4. Arquitectura de sistemas heredados + +**[arquitectura-legacy.md](arquitectura-legacy.md)** + +Consulta este documento cuando trabajes con: + +- Código legado. +- Refactorización de sistemas existentes. +- Modernización de aplicaciones. +- Migraciones tecnológicas. +- Sustitución progresiva de componentes. +- Compatibilidad con contratos antiguos. +- Migraciones de persistencia. +- Pruebas de caracterización. +- Adaptadores y capas anticorrupción. +- Convivencia entre sistemas antiguos y nuevos. + +Su prioridad es preservar el comportamiento y controlar el riesgo durante la evolución del software. + +### 5.5. Decisiones arquitectónicas + +**[arquitectura-decisiones.md](arquitectura-decisiones.md)** + +Consulta este documento cuando necesites: + +- Seleccionar tecnologías. +- Comparar alternativas arquitectónicas. +- Introducir dependencias importantes. +- Diseñar nuevos módulos. +- Modificar límites existentes. +- Evaluar requisitos no funcionales. +- Analizar costes y compromisos. +- Documentar decisiones mediante ADR. +- Planificar cambios arquitectónicos significativos. + +No es necesario consultarlo para modificaciones rutinarias que no impliquen decisiones arquitectónicas relevantes. + +--- + +## 6. Selección de referencias según la tarea + +Antes de aplicar recomendaciones especializadas: + +1. Identifica el ámbito del cambio. +2. Determina qué componentes y contratos están afectados. +3. Consulta los documentos correspondientes. +4. Combina las recomendaciones cuando la tarea abarque varios ámbitos. +5. Aplica las convenciones específicas del proyecto. +6. Evita trasladar reglas de un ámbito a otro sin analizar su adecuación. + +### Ejemplos + +| Tarea | Referencias recomendadas | +|---|---| +| Crear un endpoint backend | `arquitectura-backend.md` | +| Refactorizar un componente React | `arquitectura-frontend.md` | +| Compartir un esquema de validación | `arquitectura-shared.md`, `arquitectura-backend.md` y `arquitectura-frontend.md` | +| Migrar un servicio heredado | `arquitectura-legacy.md` y `arquitectura-backend.md` | +| Modificar un contrato consumido por frontend y backend | `arquitectura-shared.md`, `arquitectura-backend.md` y `arquitectura-frontend.md` | +| Elegir una nueva base de datos | `arquitectura-backend.md` y `arquitectura-decisiones.md` | +| Rediseñar límites entre módulos | `arquitectura-decisiones.md` y los documentos correspondientes a los módulos afectados | +| Modernizar una aplicación frontend heredada | `arquitectura-legacy.md` y `arquitectura-frontend.md` | + +Los documentos especializados son complementarios, no categorías excluyentes. + +--- + +## 7. Relación con otros documentos del skill + +La documentación de arquitectura debe centrarse en: + +- Organización del sistema. +- Límites entre componentes. +- Dependencias. +- Contratos. +- Selección de estilos. +- Evolución arquitectónica. + +Para profundizar en otros aspectos, utiliza: + +- **[principios-solid.md](principios-solid.md):** principios SOLID y diseño de dependencias. +- **[diseño-de-objetos.md](diseño-de-objetos.md):** responsabilidades, entidades, objetos de valor y agregados. +- **[patrones-de-diseño.md](patrones-de-diseño.md):** patrones de diseño y criterios para aplicarlos. +- **[complejidad.md](complejidad.md):** control de la complejidad y prevención de sobreingeniería. +- **[code-smells.md](code-smells.md):** detección de problemas estructurales. +- **[testing.md](testing.md):** estrategias y niveles de pruebas. +- **[tdd.md](tdd.md):** desarrollo guiado por pruebas. + +Evita duplicar explicaciones completas de estos conceptos dentro de los documentos arquitectónicos. + +--- + +## 8. Señales de alerta arquitectónicas + +Revisa la arquitectura cuando detectes: + +- Dependencias circulares problemáticas. +- Reglas de negocio acopladas innecesariamente a infraestructura. +- Módulos con responsabilidades poco claras. +- Cambios pequeños que requieren modificar numerosos componentes. +- Dependencias sobre detalles internos de otros módulos. +- Estado mutable compartido difícil de controlar. +- Contratos públicos inestables. +- Paquetes compartidos que acumulan responsabilidades inconexas. +- Capas que únicamente delegan sin aportar valor. +- Abstracciones destinadas a requisitos hipotéticos. +- Duplicación de conocimiento entre módulos. +- Dificultad para verificar comportamientos relevantes. + +Estas señales justifican un análisis, pero no obligan automáticamente a realizar una refactorización. + +**La refactorización debe estar motivada por un problema real y mantenerse dentro del alcance autorizado.** + +--- + +## 9. Lista de comprobación arquitectónica + +### Antes de implementar + +- [ ] ¿Comprendo los requisitos y restricciones del sistema? +- [ ] ¿He identificado la arquitectura existente? +- [ ] ¿Conozco los módulos y contratos afectados? +- [ ] ¿He consultado las referencias especializadas necesarias? +- [ ] ¿La solución propuesta es proporcional al problema? +- [ ] ¿Estoy evitando abstracciones innecesarias? + +### Durante la implementación + +- [ ] ¿Las responsabilidades permanecen cohesionadas? +- [ ] ¿Las dependencias están justificadas? +- [ ] ¿Se respetan los contratos existentes? +- [ ] ¿Estoy evitando cambios arquitectónicos ajenos a la tarea? +- [ ] ¿Las modificaciones mantienen la compatibilidad requerida? +- [ ] ¿La implementación sigue las convenciones del proyecto? + +### Al finalizar + +- [ ] ¿Se satisfacen los requisitos? +- [ ] ¿Se ha preservado el comportamiento que debía mantenerse? +- [ ] ¿Se han ejecutado las verificaciones pertinentes? +- [ ] ¿Los límites entre módulos siguen siendo coherentes? +- [ ] ¿He evitado introducir complejidad accidental? +- [ ] ¿Se han documentado las decisiones arquitectónicas significativas? +- [ ] ¿He comunicado las limitaciones y riesgos pendientes? + +--- + +## Principio final + +> "La arquitectura debe facilitar la evolución del software, no imponer una complejidad superior a la que requieren sus necesidades." + +Selecciona estructuras, módulos, capas y contratos según los requisitos y el contexto del proyecto. + +Respeta la arquitectura existente y modifica sus límites únicamente cuando exista una razón técnica o funcional justificada. + +**La mejor arquitectura es aquella que satisface los requisitos, mantiene responsabilidades claras, controla las dependencias y permite evolucionar el sistema de forma segura con la menor complejidad accidental razonable.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/clean-code.md b/.agents/skills/rdx-solid/referencias/clean-code.md new file mode 100644 index 0000000..d08fc8a --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/clean-code.md @@ -0,0 +1,1051 @@ +# Prácticas de código limpio (Clean Code) + +> Parte de esta guía se basa en conceptos del resumen del curso **"Clean Code"** de [Academind GmbH / Maximilian Schwarzmüller](https://academind.com), © 2020. + +Este documento desarrolla las prácticas de código limpio definidas en el `SKILL.md` principal. Sus recomendaciones deben aplicarse con criterio profesional, priorizando la corrección, claridad, simplicidad y mantenibilidad del software. + +Las métricas y reglas estructurales descritas a continuación son orientaciones de diseño, salvo que las convenciones específicas del proyecto establezcan requisitos más estrictos. + +## 1. ¿Qué es el código limpio? + +Es código que resulta: + +- **Fácil de comprender:** expresa claramente su intención. +- **Fácil de modificar:** permite realizar cambios localizados y controlados. +- **Fácil de probar:** facilita verificar su comportamiento y controlar sus dependencias. +- **Sencillo:** evita complejidad, abstracciones y mecanismos innecesarios. +- **Mantenible:** facilita corregir errores y evolucionar funcionalidades sin introducir riesgos desproporcionados. + +El código limpio no es necesariamente el más corto ni el que utiliza más patrones de diseño. + +**El objetivo es minimizar la complejidad global del sistema, no optimizar métricas aisladas.** + +## 2. Enfoque centrado en las personas + +El software tiene tres grupos principales de destinatarios: + +1. **Usuarios:** necesitan que el producto satisfaga sus necesidades. +2. **Clientes:** esperan obtener valor económico, operativo o estratégico. +3. **Desarrolladores:** deben comprender, mantener y evolucionar el software. + +El código suele leerse muchas más veces de las que se escribe. + +Por tanto, debe favorecer la comprensión y el mantenimiento sin perjudicar las necesidades funcionales del producto. + +La calidad debe evaluarse considerando tanto el comportamiento observable como el coste de evolución del sistema. + +--- + +## 3. Principios de nomenclatura + +### 3.1. Consistencia y unicidad + +**Prioridad máxima: un mismo concepto debe tener un nombre coherente dentro de su contexto.** + +Evita utilizar términos diferentes para representar exactamente la misma operación o responsabilidad. + +```typescript +// MAL: Nombres inconsistentes para una misma operación. +getUserById(id); +fetchCustomerById(id); +retrieveClientById(id); + +// BIEN: Convención coherente para operaciones equivalentes. +getUser(id); +getOrder(id); +getProduct(id); +``` + +No obstante, distingue correctamente los conceptos del dominio. Por ejemplo, `User`, `Customer` y `Client` no deben tratarse como sinónimos si representan entidades diferentes. + +Mantén las convenciones existentes de un módulo cuando sean coherentes y no contradigan requisitos superiores. + +### 3.2. Comprensibilidad + +Utiliza nombres que expresen el significado del dominio. + +```typescript +// MAL: Nombre genérico. +const arr = users.filter((user) => user.isActive); + +// BIEN: Expresa claramente el resultado. +const activeUsers = users.filter((user) => user.isActive); +``` + +No utilices terminología de negocio incorrecta únicamente para evitar términos técnicos. + +### 3.3. Especificidad + +Evita nombres ambiguos como `data`, `info`, `manager`, `handler`, `processor` o `utils` cuando no permitan identificar la responsabilidad real. + +```typescript +// MAL: Responsabilidades imprecisas. +class DataManager {} + +function processInfo(data: unknown): void {} + +// BIEN: Responsabilidades identificables. +class OrderRepository {} + +function validatePayment(payment: Payment): void {} +``` + +Estos términos no están prohibidos. Pueden resultar adecuados cuando describan correctamente el concepto, como `EventHandler` o `ImageProcessor`. + +### 3.4. Brevedad sin perder claridad + +Los nombres deben ser concisos, pero suficientemente descriptivos. + +```typescript +// MAL: Abreviaturas crípticas. +const usrLst = getUsrs(); + +// MAL: Nombre innecesariamente largo. +const listOfAllActiveUsersInTheSystem = getActiveUsers(); + +// BIEN: Breve y claro. +const activeUsers = getActiveUsers(); +``` + +### 3.5. Facilidad de búsqueda + +Utiliza nombres suficientemente específicos para facilitar su localización. + +```typescript +// MAL: Nombre demasiado genérico. +const data = fetch(); + +// BIEN: Identifica el contenido. +const orderSummary = fetchOrderSummary(); +``` + +### 3.6. Facilidad de pronunciación + +Los identificadores deben poder comunicarse con claridad entre desarrolladores. + +```typescript +// MAL: Abreviatura difícil de interpretar. +const genymdhms = generateTimestamp(); + +// BIEN: Concepto comprensible. +const timestamp = generateTimestamp(); +``` + +### 3.7. Evitar términos redundantes + +No añadas palabras que repitan información evidente. + +```typescript +// MAL: Redundancia innecesaria. +const userData = user; +class UserClass {} + +// BIEN: Nombres directos. +const currentUser = user; +class User {} +``` + +--- + +## 4. Funciones y métodos + +Las funciones deben ser comprensibles, cohesionadas y expresar una responsabilidad concreta. + +### 4.1. Minimizar los parámetros + +Un número reducido de parámetros suele facilitar la lectura y el uso de una función. + +Sin embargo, **no existe un número máximo universalmente correcto**. + +```typescript +// Operación sin parámetros. +createSession(); + +// Operación con un parámetro. +isValidEmail(email); + +// Operación con dos parámetros. +calculateTotal(price, quantity); + +// Operación con varios parámetros relacionados. +createUser({ + firstName: 'Max', + lastName: 'Smith', + email: 'test@example.com', + role: 'editor', + age: 31, +}); +``` + +Agrupa parámetros cuando formen una unidad conceptual o mejoren la legibilidad. + +```typescript +interface Rectangle { + x: number; + y: number; + width: number; + height: number; +} + +function createRectangle(rectangle: Rectangle): void { + // Crear el rectángulo. +} + +createRectangle({ + x: 10, + y: 9, + width: 30, + height: 12, +}); +``` + +No crees objetos de parámetros innecesarios para funciones sencillas con dos o tres argumentos claros. + +### 4.2. Mantener las funciones pequeñas y cohesionadas + +Una función debe tener una responsabilidad identificable. + +Evita mezclar validaciones, acceso a datos, decisiones de negocio y efectos externos cuando exista una separación clara y útil. + +```typescript +// MAL: Mezcla responsabilidades y compara contraseñas directamente. +async function login(email: string, password: string): Promise { + if (!email.includes('@')) { + throw new Error('Correo electrónico no válido'); + } + + const user = await database.findUserByEmail(email); + + if (!user || user.password === password) { + throw new Error('Credenciales no válidas'); + } + + await sessionService.create(user.id); +} +``` + +Además de mezclar responsabilidades, el ejemplo anterior contiene un error lógico en la comprobación de las credenciales y presupone una comparación directa de contraseñas. + +Una implementación más clara delega las operaciones específicas: + +```typescript +async function login( + email: string, + password: string, +): Promise { + validateLoginInput(email, password); + + const user = await findUserByEmail(email); + + if (!user) { + throw new Error('Credenciales no válidas'); + } + + await verifyCredentials(user, password); + await createUserSession(user.id); +} +``` + +Este ejemplo presupone que `findUserByEmail` devuelve `Promise` y que `verifyCredentials` rechaza la promesa cuando la contraseña no es válida. Solo se crea la sesión después de verificar las credenciales. Adapta el error genérico a la política del proyecto sin filtrar si una cuenta existe. + +La verificación de credenciales debe utilizar el mecanismo seguro establecido por el proyecto. + +**No extraigas funciones únicamente para reducir líneas. Cada extracción debe aportar significado, reutilización o una separación útil de responsabilidades.** + +### 4.3. Un nivel de abstracción coherente + +Las operaciones dentro de una función deben mantener un nivel de abstracción razonablemente uniforme. + +```typescript +// MAL: Mezcla configuración técnica y operación de negocio. +async function printDocument(documentPath: string): Promise { + const options = { encoding: 'utf8' as const }; + const content = await fileSystem.readFile(documentPath, options); + + const printer = new Printer('pdf'); + + await printer.print(content); +} +``` + +```typescript +// BIEN: Las operaciones expresan la intención. +async function printDocument(documentPath: string): Promise { + const document = await readDocument(documentPath); + + await printPdfDocument(document); +} +``` + +No es necesario crear una función auxiliar para cada instrucción. El objetivo es evitar que los detalles técnicos oculten el flujo principal. + +### 4.4. Dividir funciones con criterio + +Evita las extracciones artificiales. + +**Señales de una división innecesaria:** + +1. La función extraída únicamente cambia el nombre de una operación trivial. +2. Comprender el flujo requiere navegar entre demasiadas funciones. +3. No existe un nombre significativo para la nueva función. + +```typescript +// EXCESIVAMENTE FRAGMENTADO. +async function createUser(email: string): Promise { + await saveUser(email); +} + +async function saveUser(email: string): Promise { + const user = buildUser(email); + await userRepository.save(user); +} + +function buildUser(email: string): User { + return new User(email); +} +``` + +```typescript +// MÁS DIRECTO. +async function createUser(email: string): Promise { + const user = new User(email); + + await userRepository.save(user); +} +``` + +Extrae código cuando: + +- Represente una responsabilidad diferenciada. +- Oculte complejidad técnica significativa. +- Exprese una operación relevante del dominio. +- Facilite las pruebas o el mantenimiento. +- Permita reutilizar comportamiento sin crear acoplamientos artificiales. + +### 4.5. Evitar efectos secundarios inesperados + +Los efectos secundarios deben ser coherentes con el nombre y el contrato de la operación. + +```typescript +// MAL: Una validación crea una sesión. +async function validateCredentials( + email: string, + password: string, +): Promise { + await verifyCredentials(email, password); + await createSession(); +} +``` + +```typescript +// BIEN: La operación expresa que inicia una sesión. +async function authenticateAndStartSession( + email: string, + password: string, +): Promise { + await verifyCredentials(email, password); + await createSession(); +} +``` + +Cuando sea posible, separa consultas, validaciones y modificaciones de estado. + +No obstante, una operación puede combinar responsabilidades técnicas necesarias para satisfacer un caso de uso, siempre que su contrato sea claro. + +--- + +## 5. Estructuras de control + +### 5.1. Priorizar condiciones comprensibles + +Las condiciones positivas suelen resultar más directas, pero no deben imponerse universalmente. + +```typescript +// Claro: expresa directamente la condición. +if (isEmpty(content)) { + throw new Error('El contenido está vacío'); +} +``` + +```typescript +// También correcto: negación natural del estado esperado. +if (!isOpen(transaction)) { + throw new Error('La transacción no está abierta'); +} +``` + +Selecciona la expresión que mejor comunique la intención. + +### 5.2. Evitar el anidamiento excesivo + +El anidamiento profundo dificulta seguir el flujo de ejecución. + +Utiliza cláusulas de guarda y retornos anticipados cuando mejoren la claridad. + +```typescript +// MAL: Anidamiento innecesario. +async function messageUser( + user: User | null, + message: string, +): Promise { + if (user) { + if (message) { + if (user.acceptsMessages) { + const success = await user.sendMessage(message); + + if (success) { + console.log('Mensaje enviado'); + } + } + } + } +} +``` + +```typescript +// BIEN: Cláusula de guarda. +async function messageUser( + user: User | null, + message: string, +): Promise { + if (!user || !message || !user.acceptsMessages) { + return; + } + + const success = await user.sendMessage(message); + + if (success) { + console.log('Mensaje enviado'); + } +} +``` + +El objetivo es mejorar la lectura sin alterar el comportamiento. + +### 5.3. Extraer estructuras de control complejas + +Extrae una estructura de control cuando represente un comportamiento independiente. + +```typescript +async function connectDatabase(uri: string): Promise { + validateDatabaseUri(uri); + + try { + return await connectPrimaryDatabase(uri); + } catch (error) { + if (!isRecoverableConnectionError(error)) { + throw error; + } + + return connectFallbackDatabase(uri); + } +} +``` + +Este ejemplo presupone que el sistema admite una conexión alternativa. + +La recuperación ante errores debe responder a una necesidad real y no ocultar fallos de configuración o seguridad. + +No introduzcas funciones auxiliares cuando el condicional sea más claro directamente. + +### 5.4. Utilizar polimorfismo cuando aporte valor + +Los condicionales repetidos pueden indicar que existe una variación de comportamiento. + +Sin embargo, no todo `if` o `switch` necesita sustituirse por polimorfismo. + +```typescript +interface PaymentProcessor { + processPayment(transaction: Transaction): Promise; + processRefund(transaction: Transaction): Promise; +} +``` + +```typescript +class CreditCardProcessor implements PaymentProcessor { + async processPayment(transaction: Transaction): Promise { + await processCreditCardPayment(transaction); + } + + async processRefund(transaction: Transaction): Promise { + await processCreditCardRefund(transaction); + } +} +``` + +```typescript +class PayPalProcessor implements PaymentProcessor { + async processPayment(transaction: Transaction): Promise { + await processPayPalPayment(transaction); + } + + async processRefund(transaction: Transaction): Promise { + await processPayPalRefund(transaction); + } +} +``` + +Una factoría o registro puede seleccionar el procesador adecuado según el método de pago. + +Este enfoque resulta útil cuando hay varias implementaciones, comportamiento significativo o posibilidades reales de extensión. + +Para dos alternativas sencillas y estables, un condicional puede ser más claro. + +### 5.5. Gestión de errores + +Utiliza excepciones para fallos excepcionales y resultados tipados cuando representen mejor errores de negocio esperados. + +**Ejemplo con excepciones:** + +```typescript +async function createUser(email: string): Promise { + validateEmail(email); + + const existingUser = await userRepository.findByEmail(email); + + if (existingUser) { + throw new EmailAlreadyExistsError(email); + } + + return userRepository.save(new User(email)); +} +``` + +**Ejemplo con resultados tipados:** + +```typescript +type CreateUserResult = + | { success: true; user: User } + | { success: false; reason: 'EMAIL_ALREADY_EXISTS' }; +``` + +Ambos enfoques pueden ser correctos según la arquitectura. + +Evita: + +- Capturar excepciones y descartarlas silenciosamente. +- Usar excepciones indiscriminadamente como flujo normal. +- Devolver códigos ambiguos sin significado. +- Registrar información sensible. +- Ocultar el motivo real de un fallo. + +--- + +## 6. Clases y objetos + +### 6.1. Objetos frente a contenedores de datos + +Distingue entre: + +**Objetos:** encapsulan estado y exponen comportamiento. + +**Contenedores de datos:** representan y transportan información. + +```typescript +// Contenedor de datos. +interface UserData { + name: string; + age: number; +} +``` + +```typescript +// Objeto que encapsula comportamiento. +class User { + constructor( + private readonly name: string, + private readonly age: number, + ) {} + + greet(): string { + return `Hola, soy ${this.name} y tengo ${this.age} años.`; + } +} +``` + +Ambos diseños son válidos. + +Utiliza estructuras de datos para DTO, respuestas de API, modelos de presentación y otros escenarios donde exponer información sea su finalidad. + +Utiliza objetos con comportamiento cuando sea necesario proteger invariantes o encapsular reglas del dominio. + +Evita mezclar responsabilidades sin una justificación clara, pero no prohíbas que una entidad exponga datos necesarios mediante métodos o propiedades controladas. + +### 6.2. Cohesión de clases + +La cohesión describe hasta qué punto los elementos de una clase colaboran para cumplir una responsabilidad común. + +Una clase cohesionada: + +- Tiene una finalidad reconocible. +- Agrupa operaciones relacionadas. +- Mantiene un estado coherente. +- Presenta motivos de cambio relacionados. + +Una cohesión aparentemente baja puede indicar responsabilidades mezcladas, pero no debe evaluarse únicamente contando cuántos atributos utiliza cada método. + +Las clases sin estado, como determinados servicios, también pueden tener una cohesión elevada. + +**Revisa las clases cuyo comportamiento no pueda describirse mediante una responsabilidad clara.** + +--- + +## 7. Object Calisthenics: nueve heurísticas de diseño + +*Object Calisthenics* es un conjunto de ejercicios orientados a mejorar el diseño orientado a objetos. + +Sus reglas pueden aplicarse estrictamente durante ejercicios de aprendizaje, pero **no deben convertirse automáticamente en restricciones obligatorias de producción**. + +### 7.1. Reducir los niveles de indentación + +**Regla original:** un nivel de indentación por método. + +**Aplicación recomendada:** mantener los flujos comprensibles y reducir el anidamiento cuando resulte excesivo. + +```typescript +function processOrders(orders: Order[]): void { + for (const order of orders) { + if (!order.isValid()) { + continue; + } + + processOrder(order); + } +} + +function processOrder(order: Order): void { + for (const item of order.items) { + if (!item.inStock) { + continue; + } + + processItem(item); + } +} +``` + +No es necesario eliminar todos los niveles de anidamiento si la alternativa perjudica la claridad. + +### 7.2. Evitar `else` cuando sea innecesario + +**Regla original:** no utilizar `else`. + +**Aplicación recomendada:** preferir retornos anticipados cuando simplifiquen el flujo. + +```typescript +function getDiscount(user: User): number { + if (user.isPremium) { + return 20; + } + + return 0; +} +``` + +Utilizar `else` sigue siendo válido cuando representa claramente dos ramas complementarias. + +No introduzcas funciones auxiliares o lógica artificial únicamente para eliminar esta palabra clave. + +### 7.3. Encapsular primitivas con significado de dominio + +**Regla original:** encapsular todos los valores primitivos. + +**Aplicación recomendada:** utilizar objetos de valor cuando aporten validación, invariantes, operaciones o semántica. + +```typescript +class Email { + constructor(readonly value: string) { + if (!isValidEmail(value)) { + throw new InvalidEmailError(); + } + } +} +``` + +```typescript +class Age { + constructor(readonly value: number) { + if (!Number.isInteger(value) || value < 0 || value > 150) { + throw new InvalidAgeError(); + } + } +} +``` + +```typescript +function createUser(email: Email, age: Age): User { + return new User(email, age); +} +``` + +No encapsules todos los números, cadenas o booleanos de forma indiscriminada. + +En TypeScript, considera *branded types* cuando únicamente necesites diferenciar identificadores. + +### 7.4. Colecciones de primera clase + +**Regla original:** una clase que contenga una colección no debe tener otros atributos. + +**Aplicación recomendada:** encapsular colecciones cuando tengan comportamiento o invariantes propios. + +```typescript +class OrderItems { + private readonly items: OrderItem[]; + + constructor(items: readonly OrderItem[] = []) { + this.items = [...items]; + } + + add(item: OrderItem): void { + this.items.push(item); + } + + total(): Money { + return calculateOrderTotal(this.items); + } + + isEmpty(): boolean { + return this.items.length === 0; + } +} +``` + +```typescript +class Order { + constructor( + readonly id: OrderId, + private readonly items: OrderItems, + ) {} +} +``` + +La copia del constructor protege la estructura del array frente a cambios a través de la referencia original. Es superficial: si los elementos contienen estado mutable que pueda comprometer invariantes, define también su propiedad, inmutabilidad o copia adecuada. `readonly` no convierte automáticamente los objetos anidados en inmutables. + +Utiliza arrays y colecciones nativas cuando sean suficientes. + +### 7.5. Ley de Deméter + +**Regla original:** un único punto por línea. + +**Aplicación recomendada:** evitar que un componente dependa de los detalles internos de estructuras ajenas. + +```typescript +// Acoplamiento a la estructura interna. +const city = order.customer.address.city; +``` + +```typescript +// El dominio proporciona la operación necesaria. +const city = order.getShippingCity(); +``` + +El segundo enfoque resulta adecuado cuando obtener la ciudad de envío constituye una responsabilidad del pedido. + +No obstante, acceder a datos públicos de DTO o utilizar APIs fluidas puede ser perfectamente legítimo. + +### 7.6. Evitar abreviaturas innecesarias + +**Regla original:** no utilizar abreviaturas. + +**Aplicación recomendada:** evitar abreviaturas ambiguas y mantener las ampliamente reconocidas. + +```typescript +// MAL. +const custRepo = new CustRepo(); +const ord = new Ord(); +``` + +```typescript +// BIEN. +const customerRepository = new CustomerRepository(); +const order = new Order(); +``` + +Abreviaturas habituales como `id`, `url`, `api` o `http` pueden utilizarse cuando sean coherentes con las convenciones del proyecto. + +### 7.7. Mantener entidades pequeñas + +**Regla original:** + +- Clases de menos de 50 líneas. +- Métodos de menos de 10 líneas. +- Archivos de menos de 100 líneas. + +**Aplicación recomendada:** + +- Revisa los métodos extensos cuando acumulen complejidad o responsabilidades. +- Como orientación, presta atención a los métodos que superen aproximadamente 10-20 líneas. +- Revisa las clases grandes cuando mezclen responsabilidades independientes. +- Organiza los archivos para facilitar su navegación. +- No establezcas límites absolutos de líneas como criterio de calidad. + +Una implementación clara de 25 líneas puede ser preferible a cinco funciones artificiales de cinco líneas. + +### 7.8. Limitar el estado de las clases + +**Regla original:** no utilizar más de dos variables de instancia. + +**Aplicación recomendada:** mantener un estado coherente con la responsabilidad de la clase. + +```typescript +class Order { + constructor( + readonly id: OrderId, + readonly customerId: CustomerId, + readonly items: OrderItems, + readonly status: OrderStatus, + ) {} +} +``` + +Esta clase puede ser correcta aunque tenga cuatro atributos. + +Evalúa si el estado pertenece al mismo concepto, no si supera un límite numérico. + +Extrae objetos cuando representen subconceptos reales y no únicamente para reducir el número de propiedades. + +### 7.9. Favorecer comportamiento frente a acceso indiscriminado al estado + +**Regla original:** no utilizar getters, setters ni propiedades públicas. + +**Aplicación recomendada:** encapsular las operaciones que deban preservar invariantes. + +```typescript +// Riesgo: La lógica de retirada queda fuera del objeto. +if (account.balance >= amount) { + account.balance -= amount; +} +``` + +```typescript +// Mejor: La cuenta controla sus reglas. +class Account { + constructor(private balance: Money) { + if (balance.isNegative()) { + throw new InvalidInitialBalanceError(); + } + } + + withdraw(amount: Money): WithdrawResult { + if (!amount.isPositive()) { + return WithdrawResult.invalidAmount(); + } + + if (!this.balance.isGreaterThanOrEqualTo(amount)) { + return WithdrawResult.insufficientFunds(); + } + + this.balance = this.balance.subtract(amount); + + return WithdrawResult.success(); + } +} +``` + +El ejemplo presupone un `Money` inmutable con comparación y resta entre monedas compatibles, y los resultados `success`, `invalidAmount` e `insufficientFunds`. Mantiene las mismas comprobaciones de importe y saldo inicial que `BankAccount` en [diseño-de-objetos.md](diseño-de-objetos.md). Si falla una validación o una operación monetaria, no debe modificarse el saldo. + +Los getters y propiedades de solo lectura siguen siendo adecuados cuando exponen información sin comprometer invariantes. + +Evita setters que permitan introducir estados inválidos. + +--- + +## 8. Comentarios + +### 8.1. Explicar decisiones y motivos + +Los comentarios deben aportar información que no resulte evidente al leer el código. + +Prioriza explicaciones sobre: + +- Decisiones de diseño. +- Restricciones de negocio. +- Compatibilidad. +- Riesgos. +- Contratos. +- Comportamientos inesperados. +- Soluciones temporales justificadas. + +```typescript +// MAL: Repite la operación. +counter++; +``` + +```typescript +// BIEN: Explica una particularidad relevante. +// La API heredada utiliza índices basados en cero. +const displayIndex = apiIndex + 1; +``` + +### 8.2. Preferir código expresivo + +Cuando sea posible, utiliza nombres y abstracciones claras en lugar de comentarios que expliquen condiciones complejas. + +```typescript +// MENOS CLARO. +if (user.subscriptionLevel >= 2 && !user.isBanned) { + // Permitir acceso a funciones premium. +} +``` + +```typescript +// MÁS CLARO. +if (user.canAccessPremiumFeatures()) { + enablePremiumFeatures(); +} +``` + +No obstante, la documentación pública, los comentarios sobre decisiones técnicas y las explicaciones de contratos pueden ser necesarios incluso con código bien nombrado. + +### 8.3. Comentarios que deben evitarse + +- Comentarios redundantes. +- Explicaciones que repiten literalmente el código. +- Comentarios obsoletos. +- Información que debería expresarse mediante tipos o nombres. +- Código comentado que ya no se utiliza. + +Los comentarios `TODO` deben identificar tareas concretas y, cuando el proyecto lo requiera, enlazar con su seguimiento correspondiente. + +--- + +## 9. Formato y organización + +### 9.1. Organización vertical + +- Mantén juntos los elementos relacionados. +- Separa conceptos mediante líneas vacías cuando mejore la legibilidad. +- Agrupa propiedades, métodos y firmas de sobrecarga relacionadas. +- Mantén una organización coherente entre archivos. +- Sitúa las operaciones públicas y los detalles internos según las convenciones del proyecto. + +### 9.2. Organización horizontal + +- Utiliza una indentación consistente. +- Mantén espacios coherentes alrededor de operadores. +- Evita líneas innecesariamente extensas. +- Como orientación, considera longitudes aproximadas de 80-120 caracteres. +- Respeta la configuración del formateador oficial del proyecto. + +No introduzcas modificaciones de formato ajenas a la tarea. + +### 9.3. Organización narrativa + +Cuando resulte apropiado, estructura el código desde operaciones de alto nivel hacia detalles de implementación. + +```typescript +class OrderProcessor { + async process(order: Order): Promise { + this.validate(order); + + const total = this.calculateTotal(order); + + return this.save(order, total); + } + + private validate(order: Order): void { + // Validar invariantes y requisitos. + } + + private calculateTotal(order: Order): Money { + return order.calculateTotal(); + } + + private async save( + order: Order, + total: Money, + ): Promise { + return this.repository.save(order, total); + } +} +``` + +El ejemplo ilustra la organización narrativa y presupone las dependencias y contratos correspondientes. + +No es obligatorio colocar todos los métodos privados detrás de los públicos si las convenciones del proyecto establecen otra organización. + +### 9.4. Convenciones del proyecto + +Cuando el proyecto no establezca otra norma, aplica estas convenciones generales. Las preferencias adicionales y la prioridad de instrucciones se mantienen en el `SKILL.md` principal: + +- Identificadores en inglés. +- Comentarios y documentación en castellano. +- Archivos y directorios en `kebab-case`. +- Clases, tipos e interfaces en `PascalCase`. +- Variables, funciones y métodos en `camelCase`. +- Constantes globales en `UPPER_SNAKE_CASE`. +- Llaves obligatorias en estructuras de control. +- Respeto de las convenciones existentes y de las herramientas de formato configuradas. + +--- + +## 10. Lista de comprobación de código limpio + +### Nomenclatura + +- [ ] ¿Los identificadores expresan claramente su significado? +- [ ] ¿Utilizo términos coherentes con el dominio? +- [ ] ¿Los nombres son suficientemente específicos? +- [ ] ¿He evitado abreviaturas ambiguas? +- [ ] ¿Las variables booleanas expresan condiciones claras? +- [ ] ¿Se mantienen las convenciones del proyecto? + +### Funciones y métodos + +- [ ] ¿Cada función tiene una responsabilidad coherente? +- [ ] ¿Los parámetros son necesarios y comprensibles? +- [ ] ¿He agrupado parámetros cuando representan un concepto común? +- [ ] ¿Las operaciones mantienen un nivel de abstracción razonable? +- [ ] ¿He evitado dividir funciones artificialmente? +- [ ] ¿Los efectos secundarios son previsibles? +- [ ] ¿El código expresa claramente su intención? + +### Estructuras de control + +- [ ] ¿Las condiciones son comprensibles? +- [ ] ¿Puedo reducir el anidamiento sin complicar el código? +- [ ] ¿Las cláusulas de guarda aportan claridad? +- [ ] ¿Los condicionales complejos justifican una extracción? +- [ ] ¿He evitado introducir polimorfismo innecesario? +- [ ] ¿El tratamiento de errores es adecuado al contexto? + +### Objetos y clases + +- [ ] ¿La clase tiene una responsabilidad identificable? +- [ ] ¿Su estado es coherente con esa responsabilidad? +- [ ] ¿Las dependencias están justificadas? +- [ ] ¿Los objetos protegen correctamente sus invariantes? +- [ ] ¿Los contenedores de datos se utilizan cuando corresponde? +- [ ] ¿Los objetos de valor aportan semántica o seguridad real? +- [ ] ¿He evitado clases y abstracciones artificiales? +- [ ] ¿Se respetan los principios SOLID cuando son aplicables? + +### Complejidad y mantenimiento + +- [ ] ¿La solución es suficientemente sencilla? +- [ ] ¿Existe duplicación de reglas de negocio o conocimiento? +- [ ] ¿He evitado abstracciones prematuras? +- [ ] ¿Los métodos extensos siguen siendo comprensibles? +- [ ] ¿La organización facilita localizar y modificar funcionalidades? +- [ ] ¿He preservado el comportamiento y los contratos existentes? +- [ ] ¿Los cambios están limitados al alcance de la tarea? + +### Comentarios y formato + +- [ ] ¿Los comentarios aportan información relevante? +- [ ] ¿He eliminado comentarios redundantes u obsoletos? +- [ ] ¿El formato es consistente con el proyecto? +- [ ] ¿Las operaciones relacionadas están agrupadas? +- [ ] ¿He evitado reformatear código ajeno a la modificación? + +--- + +## Principio final + +> "El código limpio no es el que cumple más reglas, sino el que permite comprender, modificar y verificar el comportamiento con el menor coste razonable." + +Utiliza las reglas de esta guía como herramientas para mejorar decisiones de ingeniería. + +No sacrifiques la claridad, la simplicidad o la mantenibilidad para cumplir una métrica aislada. + +**Una solución sencilla, explícita y correctamente verificada es preferible a una arquitectura artificialmente compleja que únicamente aparenta seguir buenas prácticas.** diff --git a/.agents/skills/rdx-solid/referencias/code-smells.md b/.agents/skills/rdx-solid/referencias/code-smells.md new file mode 100644 index 0000000..69eb7c6 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/code-smells.md @@ -0,0 +1,925 @@ +# Problemas de diseño y antipatrones (Code Smells & Anti-Patterns) + +## 1. ¿Qué son los Code Smells? + +Los *Code Smells* son indicios de posibles problemas en el diseño o la estructura del software. + +No representan necesariamente errores funcionales. Señalan características que pueden dificultar: + +- Comprender el código. +- Modificar funcionalidades. +- Realizar pruebas. +- Mantener contratos. +- Reutilizar comportamientos. +- Evolucionar la arquitectura. + +**Un Code Smell es una señal para investigar, no una obligación automática de refactorizar.** + +Su importancia depende del contexto, las responsabilidades, el impacto y el coste de mantenimiento. + +Este documento desarrolla los criterios establecidos en el `SKILL.md` principal. + +### Criterios generales + +- No evalúes la calidad exclusivamente mediante métricas numéricas. +- Identifica el problema concreto que produce la estructura observada. +- Diferencia entre código mejorable y código que compromete la corrección. +- Prioriza los problemas que afectan al mantenimiento, seguridad o evolución. +- No modifiques código estable únicamente para satisfacer una regla estética. +- Respeta el alcance de la tarea. +- Conserva el comportamiento observable durante las refactorizaciones. + +--- + +## 2. Las cinco categorías principales + +### 2.1. Bloaters: estructuras excesivamente grandes + +Son componentes que han acumulado tamaño o complejidad hasta dificultar su comprensión. + +| Problema | Señal de revisión | Posible refactorización | +|---|---|---| +| **Long Method (Método largo)** | Método extenso con varios niveles de responsabilidad. | Extract Method, Compose Method. | +| **Large Class (Clase grande)** | Clase con responsabilidades independientes o demasiadas dependencias. | Extract Class, SRP. | +| **Long Parameter List (Lista larga de parámetros)** | Parámetros numerosos o difíciles de relacionar. | Introduce Parameter Object. | +| **Data Clumps (Agrupaciones de datos)** | Los mismos datos aparecen juntos repetidamente. | Introducir un tipo u objeto que represente el concepto. | +| **Primitive Obsession (Obsesión por los primitivos)** | Conceptos de dominio representados mediante primitivas ambiguas. | Value Objects, tipos nominales. | + +Los límites de líneas y parámetros son orientativos, no criterios absolutos. + +### 2.2. Object-Orientation Abusers: uso inadecuado de la orientación a objetos + +| Problema | Señal de revisión | Posible refactorización | +|---|---|---| +| **Switch Statements (Condicionales por tipo)** | Comprobaciones complejas o repetidas de variantes de comportamiento. | Polimorfismo, Strategy o simplificación. | +| **Parallel Inheritance (Herencia paralela)** | Ampliar una jerarquía obliga a ampliar otra. | Revisar responsabilidades, composición o jerarquías. | +| **Refused Bequest (Herencia rechazada)** | Una subclase no puede cumplir correctamente el contrato heredado. | Replace Inheritance with Delegation. | +| **Alternative Classes (Clases alternativas)** | Componentes equivalentes presentan contratos innecesariamente distintos. | Normalizar contratos o introducir una abstracción compartida cuando corresponda. | + +No toda estructura `switch` ni toda jerarquía de herencia constituye un problema. + +### 2.3. Change Preventers: estructuras que dificultan los cambios + +| Problema | Señal de revisión | Posible refactorización | +|---|---|---| +| **Divergent Change (Cambios divergentes)** | Una clase cambia frecuentemente por motivos independientes. | Extract Class, SRP. | +| **Shotgun Surgery (Cambios dispersos)** | Una modificación pequeña afecta a muchos componentes. | Reorganizar responsabilidades y localizar conocimiento compartido. | +| **Parallel Inheritance (Herencia paralela)** | Varias jerarquías deben evolucionar coordinadamente. | Composición o revisión de jerarquías. | + +El objetivo es reducir la amplificación de cambios sin introducir nuevas dependencias innecesarias. + +### 2.4. Dispensables: elementos potencialmente prescindibles + +| Problema | Señal de revisión | Posible refactorización | +|---|---|---| +| **Comments (Comentarios redundantes)** | Explican operaciones evidentes o compensan nombres poco claros. | Renombrar, simplificar o eliminar comentarios redundantes. | +| **Duplicate Code (Código duplicado)** | Se repiten reglas o conocimiento que debe mantenerse sincronizado. | Extraer comportamiento común cuando esté justificado. | +| **Dead Code (Código muerto)** | Código sin consumidores ni comportamiento alcanzable. | Eliminar tras verificar su falta de uso. | +| **Speculative Generality (Generalización especulativa)** | Abstracciones creadas para necesidades hipotéticas. | Aplicar YAGNI y simplificar. | +| **Lazy Class (Clase innecesariamente trivial)** | Clase sin responsabilidad o valor propio. | Inline Class o eliminar la abstracción. | + +No elimines código público o aparentemente no utilizado sin comprobar consumidores externos, carga dinámica y mecanismos de extensión. + +### 2.5. Couplers: acoplamiento excesivo + +| Problema | Señal de revisión | Posible refactorización | +|---|---|---| +| **Feature Envy (Envidia de funcionalidades)** | Un método depende principalmente de datos y operaciones de otro componente. | Reubicar comportamiento o introducir un servicio de dominio. | +| **Inappropriate Intimacy (Intimidad inapropiada)** | Los componentes conocen o modifican detalles internos ajenos. | Encapsulación, contratos, extracción de responsabilidades. | +| **Message Chains (Cadenas de mensajes)** | Navegación profunda por estructuras internas ajenas. | Hide Delegate cuando reduzca un acoplamiento real. | +| **Middle Man (Intermediario innecesario)** | Componente que solo delega sin aportar una responsabilidad. | Inline Class o simplificación. | + +Las capas de adaptación, seguridad, transacciones o integración pueden parecer intermediarios, pero cumplir una responsabilidad arquitectónica legítima. + +--- + +## 3. Los siete Code Smells más habituales + +### 3.1. Long Method (Método largo) + +**Señal:** una función acumula responsabilidades o detalles que dificultan comprender su intención. + +El problema no es superar una cantidad concreta de líneas. + +#### Ejemplo problemático + +```typescript +async function processOrder(order: Order): Promise { + if (order.items.length === 0) { + throw new EmptyOrderError(); + } + + if (!order.customer) { + throw new MissingCustomerError(); + } + + let subtotal = 0; + + for (const item of order.items) { + subtotal += item.price * item.quantity; + subtotal -= item.discount ?? 0; + } + + const taxRate = getTaxRate(order.customer.state); + const total = subtotal * (1 + taxRate); + + await orderRepository.save(order, total); + await emailService.sendConfirmation(order.customer.email); +} +``` + +La operación mezcla validación, cálculo, persistencia y notificación. + +#### Alternativa con responsabilidades explícitas + +```typescript +async function processOrder(order: Order): Promise { + validateOrder(order); + + const total = calculateOrderTotal(order); + + await orderRepository.save(order, total); + await emailService.sendConfirmation(order.customer.email); +} +``` + +Esta separación mejora la lectura, pero no resuelve por sí sola todos los aspectos de consistencia del proceso. + +En una aplicación real deben definirse las transacciones y el tratamiento de errores, especialmente cuando la persistencia se completa y posteriormente falla la notificación. + +#### Cuándo refactorizar + +- La función realiza operaciones conceptualmente independientes. +- Su flujo resulta difícil de seguir. +- Los cambios afectan frecuentemente a partes no relacionadas. +- Las pruebas requieren configuraciones complejas. +- Existen fragmentos con significado propio que pueden extraerse. + +#### Cuándo no refactorizar + +- La función es extensa, pero lineal y comprensible. +- La extracción multiplica funciones triviales. +- El código representa un algoritmo cohesionado. +- La refactorización no aporta una mejora clara. + +**No fragmentes artificialmente un método únicamente para reducir su número de líneas.** + +--- + +### 3.2. Large Class (Clase demasiado grande) + +**Señal:** una clase concentra responsabilidades independientes y presenta múltiples motivos de cambio. + +#### Ejemplo problemático + +```typescript +class User { + login(): Promise { + // Autenticación. + } + + resetPassword(): Promise { + // Recuperación de credenciales. + } + + setTheme(theme: Theme): void { + // Preferencias. + } + + sendEmail(message: string): Promise { + // Notificaciones. + } + + charge(amount: Money): Promise { + // Facturación. + } +} +``` + +La clase agrupa responsabilidades de autenticación, preferencias, comunicaciones y facturación. + +#### Alternativa + +```typescript +class User { + constructor( + readonly id: UserId, + readonly email: Email, + ) {} +} + +class AuthenticationService { + // Gestiona los casos de uso de autenticación. +} + +class UserPreferences { + // Gestiona las preferencias del usuario. +} + +class NotificationService { + // Gestiona las comunicaciones. +} + +class BillingService { + // Gestiona las operaciones de facturación. +} +``` + +Esta distribución es orientativa. Las responsabilidades concretas dependerán del dominio y de la arquitectura existente. + +No es obligatorio crear todas estas clases si el proyecto no necesita dichas capacidades. + +#### Indicadores relevantes + +- Responsabilidades independientes. +- Dependencias numerosas y poco relacionadas. +- Modificaciones frecuentes por requisitos diferentes. +- Pruebas difíciles de aislar. +- Dificultad para describir el propósito de la clase. + +**El tamaño de una clase es una señal secundaria. La cohesión y los motivos de cambio son criterios más importantes.** + +--- + +### 3.3. Feature Envy (Envidia de funcionalidades) + +**Señal:** un método utiliza ampliamente la información o el comportamiento de otro objeto. + +#### Ejemplo + +```typescript +class Order { + calculateShipping(customer: Customer): number { + if (customer.country === 'US') { + return customer.state === 'CA' ? 10 : 15; + } + + return 25; + } +} +``` + +La operación depende principalmente de la localización del cliente. + +Sin embargo, trasladar directamente `calculateShipping()` a `Customer` no es necesariamente correcto. + +La política de envío puede depender también de: + +- Peso del pedido. +- Dirección de entrega. +- Transportista. +- Modalidad de envío. +- Restricciones comerciales. +- Tarifas vigentes. + +#### Alternativa mediante una responsabilidad específica + +```typescript +interface ShippingPolicy { + calculate(order: Order, destination: Address): Money; +} +``` + +```typescript +class OrderService { + constructor( + private readonly shippingPolicy: ShippingPolicy, + ) {} + + calculateShipping(order: Order, destination: Address): Money { + return this.shippingPolicy.calculate(order, destination); + } +} +``` + +Esta alternativa resulta apropiada si el cálculo representa una política de negocio independiente. + +Para un caso sencillo, una función de dominio puede ser suficiente. + +#### Criterios de revisión + +- Identifica quién es responsable de la regla. +- No traslades métodos únicamente porque consulten muchos atributos de otra clase. +- Considera servicios de dominio cuando una operación involucre varios conceptos. +- Evita introducir interfaces y servicios innecesarios. + +--- + +### 3.4. Primitive Obsession (Obsesión por los primitivos) + +**Señal:** conceptos del dominio se representan mediante tipos primitivos que permiten confusiones o estados inválidos. + +#### Ejemplo + +```typescript +function createUser( + email: string, + age: number, + postalCode: string, +): User { + // Crear el usuario. +} +``` + +La firma no expresa determinadas restricciones ni diferencia conceptos semánticamente relacionados. + +#### Alternativa mediante objetos de valor + +```typescript +class Email { + constructor(readonly value: string) { + if (!isValidEmail(value)) { + throw new InvalidEmailError(); + } + } +} + +class Age { + constructor(readonly value: number) { + if (!Number.isInteger(value) || value < 0 || value > 150) { + throw new InvalidAgeError(); + } + } +} +``` + +```typescript +function createUser( + email: Email, + age: Age, + address: Address, +): User { + return new User(email, age, address); +} +``` + +Los objetos de valor pueden proteger invariantes cuando su construcción y estado están correctamente controlados. + +No sustituyen automáticamente la validación de datos externos. + +#### Alternativas en TypeScript + +Los *branded types* pueden ser suficientes cuando únicamente se necesita distinguir identificadores. + +```typescript +type UserId = string & { readonly __brand: 'UserId' }; +type OrderId = string & { readonly __brand: 'OrderId' }; +``` + +Estas distinciones existen durante la comprobación estática, no durante la ejecución. + +#### Criterios de aplicación + +Utiliza objetos de valor cuando aporten: + +- Validación. +- Invariantes. +- Semántica. +- Comportamiento propio. +- Seguridad de tipos. + +No encapsules todos los valores primitivos indiscriminadamente. + +--- + +### 3.5. Switch Statements (Condicionales por tipo) + +**Señal:** comprobaciones de tipo repetidas que distribuyen una misma variación de comportamiento por numerosos componentes. + +#### Ejemplo + +```typescript +type Shape = + | { type: 'circle'; radius: number } + | { type: 'rectangle'; width: number; height: number } + | { type: 'triangle'; base: number; height: number }; + +function getArea(shape: Shape): number { + switch (shape.type) { + case 'circle': + return Math.PI * shape.radius ** 2; + + case 'rectangle': + return shape.width * shape.height; + + case 'triangle': + return shape.base * shape.height * 0.5; + } +} +``` + +Este código no constituye necesariamente un problema. + +En TypeScript, las uniones discriminadas permiten representar variantes de forma clara y segura. + +#### Cuándo considerar polimorfismo + +Si los mismos condicionales aparecen repetidamente para calcular áreas, perímetros, dibujar figuras y aplicar transformaciones, puede resultar útil encapsular comportamientos. + +```typescript +interface Shape { + getArea(): number; + getPerimeter(): number; +} +``` + +```typescript +class Circle implements Shape { + constructor(private readonly radius: number) {} + + getArea(): number { + return Math.PI * this.radius ** 2; + } + + getPerimeter(): number { + return 2 * Math.PI * this.radius; + } +} +``` + +Este enfoque puede facilitar la incorporación de nuevas figuras con comportamientos propios. + +#### Criterios de decisión + +- Conserva `switch` cuando resulte sencillo y expresivo. +- Aprovecha las uniones discriminadas y comprobaciones exhaustivas de TypeScript. +- Utiliza polimorfismo cuando las variantes tengan comportamientos significativos. +- No crees jerarquías artificiales para eliminar condicionales triviales. +- Evalúa si resulta más frecuente añadir operaciones nuevas o variantes nuevas. + +**La presencia de un `switch` no demuestra por sí sola un problema de diseño.** + +--- + +### 3.6. Inappropriate Intimacy (Intimidad inapropiada) + +**Señal:** varios componentes conocen o modifican directamente detalles internos ajenos. + +#### Ejemplo problemático + +```typescript +class Order { + process(inventory: Inventory): void { + for (const item of this.items) { + const stock = inventory.stockLevels[item.sku]; + + if (stock.quantity < item.quantity) { + throw new OutOfStockError(item.sku); + } + + stock.quantity -= item.quantity; + } + } +} +``` + +El pedido modifica directamente la estructura interna del inventario. + +Además, podría dejar una reserva parcial si se agotan las existencias de un artículo después de haber descontado otros. + +#### Alternativa con comportamiento encapsulado + +```typescript +class Inventory { + reserve(items: readonly OrderItem[]): ReserveResult { + for (const item of items) { + if (!this.canReserve(item)) { + return ReserveResult.outOfStock(item.sku); + } + } + + this.deductStock(items); + + return ReserveResult.success(); + } +} +``` + +```typescript +class Order { + process(inventory: Inventory): void { + const result = inventory.reserve(this.items); + + if (!result.isSuccess()) { + throw new OutOfStockError(result.failedSku); + } + } +} +``` + +Esta implementación ilustra la separación de responsabilidades. + +Para garantizar una reserva atómica en producción, también deben analizarse la persistencia, las transacciones y la concurrencia. + +#### Criterios de revisión + +- Evita modificar directamente estados internos ajenos. +- Ubica las invariantes en el componente responsable. +- Utiliza contratos explícitos. +- No introduzcas getters que permitan eludir el encapsulamiento. +- Evalúa si las operaciones necesitan garantías transaccionales. + +--- + +### 3.7. Speculative Generality (Generalización especulativa) + +**Señal:** el sistema incorpora abstracciones y funcionalidades destinadas a necesidades hipotéticas. + +#### Ejemplo problemático + +```typescript +interface PaymentProcessor { + process(): Promise; + rollback(): Promise; + audit(): Promise; + generateReport(): Promise; + scheduleRecurring(): Promise; +} +``` + +Una implementación puede verse obligada a declarar capacidades que no necesita o no admite. + +#### Alternativa + +```typescript +interface PaymentProcessor { + process(): Promise; +} +``` + +```typescript +class StripePaymentProcessor implements PaymentProcessor { + async process(): Promise { + // Procesar el pago. + } +} +``` + +Incorpora nuevos contratos cuando exista una necesidad real. + +#### Criterios + +- Aplica YAGNI. +- Evita capacidades hipotéticas. +- No introduzcas interfaces genéricas para problemas todavía desconocidos. +- Respeta ISP al diseñar contratos. +- Prefiere extender una solución sencilla cuando aparezca una necesidad real. + +--- + +## 4. Otros Code Smells importantes + +### 4.1. Divergent Change (Cambios divergentes) + +Se produce cuando un componente debe modificarse por motivos independientes. + +Por ejemplo, un servicio que cambia por: + +- Nuevas reglas de facturación. +- Modificaciones de autenticación. +- Cambios en plantillas de correo. +- Evolución del almacenamiento. + +**Solución:** identificar responsabilidades y separarlas cuando exista un beneficio real. + +No confundas varios métodos de una misma responsabilidad con motivos de cambio independientes. + +### 4.2. Shotgun Surgery (Cambios dispersos) + +Se produce cuando una modificación pequeña obliga a alterar numerosos componentes. + +Puede indicar: + +- Conocimiento duplicado. +- Reglas distribuidas. +- Contratos inestables. +- Acoplamiento excesivo. + +**Solución:** localizar el conocimiento compartido y reorganizar las responsabilidades cuando corresponda. + +No introduzcas un componente central que termine convirtiéndose en una clase todopoderosa. + +### 4.3. Parallel Inheritance (Herencia paralela) + +Ocurre cuando ampliar una jerarquía obliga sistemáticamente a ampliar otra. + +Puede indicar acoplamiento estructural. + +**Posibles soluciones:** + +- Composición. +- Interfaces compartidas. +- Revisión de las relaciones de herencia. +- Eliminación de jerarquías artificiales. + +No unifiques automáticamente jerarquías que representen conceptos distintos. + +### 4.4. Refused Bequest (Herencia rechazada) + +Una clase derivada no puede utilizar o respetar correctamente parte del contrato heredado. + +Puede ser una señal de incumplimiento de LSP. + +**Posibles soluciones:** + +- Sustituir herencia por composición. +- Revisar el contrato base. +- Separar interfaces. +- Rediseñar la especialización. + +La existencia de métodos heredados no utilizados no demuestra por sí sola un problema. + +### 4.5. Data Clumps (Agrupaciones de datos) + +Los mismos parámetros o propiedades aparecen juntos repetidamente. + +```typescript +function calculateDistance( + startLatitude: number, + startLongitude: number, + endLatitude: number, + endLongitude: number, +): number { + // Calcular distancia. +} +``` + +Puede resultar apropiado introducir un concepto `Coordinates`. + +```typescript +interface Coordinates { + latitude: number; + longitude: number; +} +``` + +```typescript +function calculateDistance( + start: Coordinates, + end: Coordinates, +): number { + // Calcular distancia. +} +``` + +No extraigas automáticamente todas las agrupaciones de parámetros. + +### 4.6. Dead Code (Código muerto) + +Incluye código sin comportamiento alcanzable o sin consumidores reales. + +Antes de eliminarlo: + +- Comprueba referencias. +- Revisa exports públicos. +- Considera carga dinámica. +- Comprueba registros de componentes. +- Revisa mecanismos de reflexión o configuración. +- Verifica que no existan consumidores externos. + +**No asumas que un símbolo está sin utilizar únicamente porque una búsqueda textual no encuentre referencias directas.** + +### 4.7. Lazy Class (Clase innecesariamente trivial) + +Una clase puede parecer pequeña, pero representar legítimamente: + +- Un objeto de valor. +- Una entidad sencilla. +- Un adaptador. +- Un contrato de integración. +- Un componente de infraestructura. + +No elimines clases únicamente por tener pocas líneas. + +Evalúa si aportan semántica, encapsulamiento o una responsabilidad real. + +### 4.8. Message Chains (Cadenas de mensajes) + +Las cadenas profundas pueden indicar conocimiento excesivo sobre estructuras internas ajenas. + +```typescript +const city = order.getCustomer().getAddress().getCity(); +``` + +Puede ser preferible una operación de dominio: + +```typescript +const city = order.getShippingCity(); +``` + +Sin embargo, no toda cadena de llamadas constituye un problema. + +Son legítimas las APIs fluidas, transformaciones de colecciones y navegación por DTO cuando no introducen acoplamiento problemático. + +### 4.9. Middle Man (Intermediario innecesario) + +Un componente delega todas sus operaciones sin aportar comportamiento relevante. + +Antes de eliminarlo, comprueba si representa: + +- Un límite arquitectónico. +- Un contrato estable. +- Una separación entre módulos. +- Un adaptador. +- Un mecanismo de autorización. +- Un límite transaccional. + +Una delegación aparentemente trivial puede tener una finalidad legítima. + +--- + +## 5. Estrategias de prevención + +### 5.1. Aplicar SOLID con criterio + +Utiliza SOLID para organizar responsabilidades y controlar dependencias. + +No introduzcas interfaces o capas innecesarias únicamente para cumplir formalmente sus principios. + +### 5.2. Utilizar TDD cuando resulte apropiado + +Las pruebas pueden revelar: + +- Responsabilidades difíciles de aislar. +- Dependencias excesivas. +- Contratos ambiguos. +- Efectos secundarios inesperados. + +TDD debe ser una práctica preferente, no una obligación universal. + +### 5.3. Utilizar Object Calisthenics como herramienta de análisis + +Sus reglas ayudan a detectar posibles problemas de: + +- Anidamiento. +- Tamaño. +- Encapsulamiento. +- Responsabilidad. +- Cohesión. + +No conviertas los límites numéricos en prohibiciones absolutas. + +### 5.4. Realizar revisiones de código + +Las revisiones permiten detectar problemas que no siempre identifican las herramientas automáticas. + +Prioriza: + +- Corrección funcional. +- Claridad. +- Seguridad. +- Contratos. +- Responsabilidades. +- Dependencias. +- Complejidad accidental. + +### 5.5. Refactorizar de forma incremental + +No permitas que los problemas relevantes se acumulen indefinidamente. + +Sin embargo, evita refactorizaciones generales no solicitadas o desconectadas de la tarea. + +### 5.6. Utilizar análisis estático + +Aprovecha las herramientas establecidas por el proyecto: + +- TypeScript. +- ESLint. +- Formateadores. +- Análisis de dependencias. +- Herramientas de detección de código duplicado. +- Comprobaciones de complejidad. + +Las métricas automáticas son indicadores, no sustitutos del criterio de ingeniería. + +--- + +## 6. Qué hacer cuando detectes un Code Smell + +Sigue este procedimiento: + +### Paso 1. Confirmar el problema + +Determina si realmente perjudica la comprensión, el mantenimiento, las pruebas o la evolución. + +### Paso 2. Evaluar el alcance + +Comprueba si el problema está relacionado con la tarea actual. + +No amplíes innecesariamente el alcance. + +### Paso 3. Evaluar el riesgo + +Considera: + +- Consumidores afectados. +- Contratos públicos. +- Persistencia. +- Compatibilidad. +- Dependencias. +- Pruebas disponibles. + +### Paso 4. Proteger el comportamiento + +Utiliza pruebas existentes, pruebas de caracterización u otros mecanismos de verificación. + +No exijas escribir pruebas nuevas cuando no sean viables, pero mantén un procedimiento de comprobación adecuado. + +### Paso 5. Refactorizar incrementalmente + +Realiza cambios pequeños y cohesionados. + +Evita combinar refactorizaciones extensas con cambios funcionales no relacionados. + +### Paso 6. Verificar + +Ejecuta: + +- Pruebas relacionadas. +- Comprobaciones de tipos. +- Análisis estático. +- Compilación cuando corresponda. +- Verificaciones adicionales según el impacto. + +### Paso 7. Comunicar el resultado + +Identifica: + +- Problema detectado. +- Cambio realizado. +- Beneficio esperado. +- Verificaciones ejecutadas. +- Limitaciones pendientes. + +No declares una refactorización como verificada cuando no hayas ejecutado las comprobaciones pertinentes. + +--- + +## 7. Prioridad de los problemas detectados + +No todos los Code Smells tienen la misma importancia. + +| Prioridad | Situación | Actuación recomendada | +|---|---|---| +| **Crítica** | Riesgo de corrupción de datos, vulnerabilidad o incumplimiento grave de invariantes. | Corregir o comunicar inmediatamente según el alcance y las autorizaciones. | +| **Alta** | Responsabilidades mezcladas que provocan errores o cambios difíciles. | Priorizar si afecta a la tarea. | +| **Media** | Duplicación de reglas, acoplamiento o complejidad mantenible pero relevante. | Planificar su corrección cuando exista oportunidad justificada. | +| **Baja** | Problemas estéticos, nombres mejorables o métricas aisladas. | Corregir únicamente si aporta valor y está dentro del alcance. | + +Esta clasificación es orientativa. + +La prioridad real depende del impacto, la probabilidad de fallo, la criticidad del sistema y el coste de corrección. + +--- + +## 8. Lista de comprobación de Code Smells + +### Responsabilidades + +- [ ] ¿Los componentes tienen responsabilidades coherentes? +- [ ] ¿Existen clases con motivos de cambio independientes? +- [ ] ¿Se están concentrando operaciones sin relación clara? +- [ ] ¿La organización facilita localizar el comportamiento? + +### Complejidad + +- [ ] ¿Los métodos son comprensibles? +- [ ] ¿El anidamiento dificulta seguir el flujo? +- [ ] ¿Existen condiciones complejas que puedan simplificarse? +- [ ] ¿He evitado fragmentaciones artificiales? +- [ ] ¿Las abstracciones responden a necesidades reales? + +### Acoplamiento + +- [ ] ¿Los componentes conocen detalles internos ajenos? +- [ ] ¿Existen dependencias innecesarias? +- [ ] ¿Las reglas de negocio están ubicadas correctamente? +- [ ] ¿Los contratos públicos son estables? +- [ ] ¿Los cambios requieren modificar demasiados componentes? + +### Duplicación + +- [ ] ¿Existe conocimiento de negocio duplicado? +- [ ] ¿Las duplicaciones representan realmente el mismo concepto? +- [ ] ¿Una abstracción compartida simplificaría el mantenimiento? +- [ ] ¿He evitado aplicar mecánicamente la regla de tres? + +### Seguridad de la refactorización + +- [ ] ¿He comprobado el comportamiento existente? +- [ ] ¿Los cambios están relacionados con la tarea? +- [ ] ¿Se conservan los contratos y la compatibilidad? +- [ ] ¿He ejecutado las verificaciones pertinentes? +- [ ] ¿He evitado cambios adicionales innecesarios? + +--- + +## Principio final + +> "Un Code Smell indica dónde puede existir un problema de diseño, pero no determina automáticamente cómo debe resolverse." + +Utiliza los indicadores de esta guía para orientar el análisis, no para imponer refactorizaciones mecánicas. + +La calidad del software depende de su corrección, claridad, cohesión, capacidad de evolución y coste de mantenimiento. + +**No refactorices para satisfacer métricas. Refactoriza para resolver problemas reales de diseño y mantenimiento, preservando el comportamiento y evitando complejidad innecesaria.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/complejidad.md b/.agents/skills/rdx-solid/referencias/complejidad.md new file mode 100644 index 0000000..440f487 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/complejidad.md @@ -0,0 +1,842 @@ +# Gestión de la complejidad (Managing Complexity) + +La complejidad es uno de los principales factores que afectan a la comprensión, evolución y mantenimiento del software. + +El objetivo de una buena ingeniería no es eliminar toda complejidad, sino distinguir aquella que pertenece al problema de la que introducimos mediante nuestras decisiones técnicas. + +Este documento desarrolla los principios establecidos en el `SKILL.md` principal. + +**El objetivo es minimizar la complejidad accidental y expresar claramente la complejidad esencial, sin sacrificar la corrección, la seguridad ni los requisitos del sistema.** + +## 1. Los dos tipos de complejidad + +### 1.1. Complejidad esencial (Essential Complexity) + +Es la complejidad inherente al problema que debe resolver el software. + +Puede incluir: + +- Reglas de negocio. +- Lógica del dominio. +- Requisitos funcionales. +- Restricciones legales o normativas. +- Estados y transiciones de procesos. +- Relaciones entre entidades. +- Requisitos de consistencia y seguridad. + +No puede eliminarse simplemente mediante una refactorización si forma parte real de los requisitos. + +Sin embargo, sí puede organizarse, encapsularse y expresarse de una manera más comprensible. + +**Ejemplo:** un sistema de facturación debe contemplar impuestos, descuentos, monedas y reglas de redondeo. + +Estas reglas pueden ser complejas por naturaleza. El objetivo es modelarlas correctamente, no ocultarlas mediante simplificaciones que alteren su comportamiento. + +### 1.2. Complejidad accidental (Accidental Complexity) + +Es la complejidad introducida por nuestras propias decisiones de implementación, organización o arquitectura. + +Puede incluir: + +- Abstracciones incorrectas. +- Capas intermedias innecesarias. +- Dependencias excesivas. +- Configuraciones complejas sin justificación. +- Patrones de diseño aplicados artificialmente. +- Duplicación de conocimiento. +- Acoplamiento entre componentes. +- Estado global difícil de controlar. +- Deuda técnica. +- Mecanismos proporcionados por frameworks que podrían resolverse de manera más directa. + +La complejidad accidental debe reducirse cuando hacerlo aporte una mejora real y el coste de la modificación esté justificado. + +### Principio fundamental + +> "La mejor solución expresa correctamente la complejidad del dominio introduciendo la menor complejidad técnica razonable." + +No confundas simplicidad con ausencia de estructura. + +En ocasiones, introducir una abstracción, un módulo o un contrato adicional reduce significativamente la complejidad global. + +--- + +## 2. Detección de la complejidad + +Existen tres indicadores especialmente útiles para identificar problemas estructurales. + +### 2.1. Amplificación de cambios (Change Amplification) + +Se produce cuando una modificación relativamente pequeña obliga a modificar numerosas partes del sistema. + +**Síntoma habitual:** + +"Para añadir este campo tengo que modificar quince archivos." + +**Posibles causas:** + +- Responsabilidades dispersas. +- Conocimiento duplicado. +- Contratos mal definidos. +- Acoplamiento entre módulos. +- Transformaciones redundantes de datos. +- Límites arquitectónicos inadecuados. + +### Cómo actuar + +1. Identifica por qué deben modificarse tantos componentes. +2. Distingue los cambios necesarios de los redundantes. +3. Comprueba si existe conocimiento duplicado. +4. Revisa los contratos y límites entre módulos. +5. Valora una reorganización que reduzca futuros cambios. + +No asumas que modificar muchos archivos constituye automáticamente un problema. + +En arquitecturas modulares, una modificación legítima de un contrato puede requerir cambios explícitos en varios consumidores. + +**Lo importante es evitar cambios innecesarios, no minimizar artificialmente el número de archivos modificados.** + +### 2.2. Carga cognitiva (Cognitive Load) + +Representa el esfuerzo mental necesario para comprender un comportamiento. + +**Síntoma habitual:** + +"Para entender esta función necesito comprender otras diez clases." + +**Posibles causas:** + +- Dependencias ocultas. +- Exceso de abstracciones. +- Nomenclatura poco clara. +- Acoplamiento excesivo. +- Flujo de ejecución fragmentado. +- Responsabilidades mal distribuidas. +- Estado compartido difícil de rastrear. + +### Cómo actuar + +- Utiliza nombres expresivos. +- Mantén las responsabilidades cohesionadas. +- Haz explícitas las dependencias relevantes. +- Evita capas que únicamente deleguen operaciones. +- Reduce los saltos innecesarios entre archivos. +- Organiza el código según las necesidades de comprensión y mantenimiento. + +No fragmentes funciones o clases únicamente para cumplir límites de líneas. + +Una implementación lineal y comprensible puede ser preferible a varias funciones auxiliares sin significado propio. + +### 2.3. Incógnitas desconocidas (Unknown Unknowns) + +Son situaciones en las que el comportamiento del sistema resulta inesperado porque existen relaciones, efectos o dependencias que no se habían identificado. + +**Síntoma habitual:** + +"He modificado esta función y se ha roto una funcionalidad aparentemente independiente." + +**Posibles causas:** + +- Estado global. +- Efectos secundarios ocultos. +- Dependencias implícitas. +- Contratos no documentados. +- Comportamientos compartidos. +- Acoplamiento entre módulos. +- Orden de ejecución no evidente. + +### Cómo actuar + +- Identifica dependencias y efectos secundarios. +- Define contratos claros. +- Mejora la cobertura de los comportamientos relevantes. +- Reduce el estado global mutable. +- Utiliza pruebas de integración cuando correspondan. +- Evita operaciones que modifiquen estados ajenos de forma inesperada. +- Mantén trazabilidad suficiente de las operaciones relevantes. + +**La previsibilidad del sistema es una característica fundamental de su mantenibilidad.** + +--- + +## 3. Valores de Extreme Programming (XP) + +Extreme Programming propone cinco valores que ayudan a controlar la complejidad. + +### 3.1. Comunicación (Communication) + +El código debe comunicar claramente su propósito. + +Contribuyen a ello: + +- Nombres expresivos. +- Estructuras comprensibles. +- Contratos claros. +- Pruebas significativas. +- Documentación relevante. +- Revisiones de código. + +La comunicación no consiste en añadir comentarios indiscriminadamente, sino en reducir el esfuerzo necesario para comprender las decisiones importantes. + +### 3.2. Simplicidad (Simplicity) + +Implementa la solución más sencilla que satisfaga correctamente los requisitos. + +No confundas simplicidad con: + +- Menor número de líneas. +- Menor número de clases. +- Ausencia de abstracciones. +- Eliminación de validaciones. +- Reducción de controles de seguridad. + +Una solución es sencilla cuando permite comprender su funcionamiento y modificarla con un esfuerzo razonable. + +### 3.3. Retroalimentación (Feedback) + +Los ciclos de retroalimentación permiten detectar errores y problemas de diseño antes de que se acumulen. + +Herramientas habituales: + +- TDD cuando resulte apropiado. +- Pruebas automatizadas. +- Integración continua. +- Análisis estático. +- Revisiones de código. +- Monitorización. +- Validación de requisitos. + +Prioriza mecanismos de verificación que proporcionen información útil de manera rápida y reproducible. + +### 3.4. Valentía (Courage) + +La valentía técnica consiste en corregir problemas relevantes cuando existe una justificación y es posible controlar el riesgo. + +No significa refactorizar agresivamente todo el código que se encuentre. + +Actúa cuando: + +- La complejidad bloquea funcionalidades. +- El diseño provoca errores. +- Las dependencias impiden avanzar. +- La deuda técnica genera un coste significativo. +- Existe una oportunidad de mejora relacionada con la tarea. + +### 3.5. Respeto (Respect) + +Escribe código pensando en quienes tendrán que comprenderlo y mantenerlo. + +Esto implica: + +- Respetar las convenciones del proyecto. +- Evitar cambios innecesarios. +- Mantener contratos. +- Comunicar decisiones relevantes. +- No introducir complejidad injustificada. +- Preservar el trabajo existente que funciona correctamente. + +**La mantenibilidad también depende de respetar la continuidad y coherencia del proyecto.** + +--- + +## 4. KISS — Keep It Simple + +> "La solución más sencilla que satisface correctamente los requisitos suele ser la más adecuada." + +KISS propone evitar complejidad que no aporta valor. + +### Cómo aplicarlo + +1. Comienza por comprender el problema. +2. Identifica una solución directa. +3. Añade complejidad únicamente cuando exista una necesidad. +4. Prefiere mecanismos conocidos y suficientemente probados. +5. Cuestiona las abstracciones nuevas. +6. Compara el coste de mantenimiento de las alternativas. + +### Ejemplo de sobreingeniería + +```typescript +class UserServiceFactoryProvider { + private static instance: UserServiceFactoryProvider; + + static getInstance(): UserServiceFactoryProvider { + // Gestionar la instancia global. + } + + createFactory(): UserServiceFactory { + // Construir una factoría adicional. + } +} +``` + +Esta estructura introduce varios niveles de creación sin demostrar una necesidad concreta. + +### Alternativa sencilla + +```typescript +class UserService { + constructor( + private readonly repository: UserRepository, + ) {} + + async getUser(id: UserId): Promise { + return this.repository.findById(id); + } +} +``` + +El servicio tiene una responsabilidad clara y recibe explícitamente su dependencia. + +### Cuándo añadir complejidad + +Puede estar justificado introducir más estructura cuando: + +- Existen variantes reales de comportamiento. +- Deben protegerse contratos importantes. +- Se necesita desacoplar infraestructura. +- Existen invariantes de dominio complejas. +- La concurrencia exige mecanismos específicos. +- El mantenimiento de la solución directa se vuelve costoso. + +**KISS no significa evitar el diseño, sino utilizar el diseño estrictamente necesario.** + +--- + +## 5. YAGNI — You Aren't Gonna Need It + +> "No implementes funcionalidades hasta que exista una necesidad real." + +YAGNI ayuda a evitar desarrollar capacidades destinadas únicamente a requisitos hipotéticos. + +### Señales de alerta + +- "Puede que lo necesitemos algún día". +- "Sería interesante añadirlo". +- "Por si acaso". +- "Así estará preparado para cualquier cambio". +- "Vamos a hacerlo completamente configurable aunque solo exista un caso". + +Estas expresiones no demuestran necesariamente un problema, pero justifican revisar la necesidad. + +### Costes de incumplir YAGNI + +1. **Tiempo de desarrollo:** implementación de funcionalidades no necesarias. +2. **Mantenimiento:** código adicional que debe evolucionar. +3. **Carga cognitiva:** más elementos que comprender. +4. **Riesgo de abstracción incorrecta:** decisiones basadas en necesidades futuras mal estimadas. +5. **Superficie de fallos:** más estados, configuraciones y rutas de ejecución. + +### Ejemplo + +```typescript +// Modelo diseñado para posibilidades todavía no requeridas. +interface User { + name: string; + email: string; + middleName?: string; + secondaryEmail?: string; + faxNumber?: string; + linkedinProfile?: string; + twitterHandle?: string; +} +``` + +Si esas propiedades no corresponden a requisitos actuales, pueden introducir complejidad innecesaria. + +### Alternativa + +```typescript +interface User { + name: string; + email: Email; +} +``` + +### Aplicación pragmática + +YAGNI no impide: + +- Implementar requisitos futuros ya aprobados. +- Cumplir necesidades de escalabilidad definidas. +- Mantener compatibilidad prevista contractualmente. +- Diseñar puntos de extensión necesarios. +- Preparar migraciones conocidas. +- Incorporar controles de seguridad o integridad indispensables. + +Distingue entre: + +**Necesidades futuras definidas:** forman parte de los requisitos y pueden justificar decisiones actuales. + +**Necesidades futuras hipotéticas:** no deben condicionar innecesariamente el diseño. + +**No utilices YAGNI como justificación para ignorar requisitos no funcionales conocidos.** + +--- + +## 6. DRY — Don't Repeat Yourself + +> "Cada conocimiento relevante del sistema debería tener una representación única y no ambigua." + +DRY busca evitar la duplicación de conocimiento, especialmente cuando varias implementaciones deben mantenerse sincronizadas. + +### 6.1. Duplicación de código frente a duplicación de conocimiento + +Dos fragmentos pueden parecer iguales sin representar el mismo concepto. + +Por ejemplo, dos procesos pueden compartir inicialmente una secuencia de validación, cálculo y persistencia, pero responder a reglas de negocio independientes. + +Unificarlos prematuramente puede generar acoplamiento. + +Por el contrario, dos implementaciones diferentes de una misma regla fiscal pueden representar una duplicación de conocimiento peligrosa. + +**DRY no significa eliminar toda repetición sintáctica.** + +### 6.2. La regla de tres + +La regla de tres es una heurística que recomienda observar varias repeticiones antes de extraer una abstracción. + +```text id="90ck2r" +Primera aparición → Implementar directamente. +Segunda aparición → Evaluar la similitud. +Tercera aparición → Revisar si existe una responsabilidad compartida. +``` + +No constituye una obligación. + +### Criterios de aplicación + +- No extraigas automáticamente código después de tres repeticiones. +- No esperes obligatoriamente a tres apariciones cuando ya exista conocimiento compartido. +- Comprueba que los fragmentos representan la misma regla o responsabilidad. +- Evita introducir abstracciones para similitudes superficiales. +- Prefiere pequeñas duplicaciones cuando permitan mantener responsabilidades independientes. + +### 6.3. Ejemplo de duplicación + +```typescript +async function processUserOrder(order: Order): Promise { + validateOrder(order); + calculateTaxes(order); + await saveOrder(order); +} + +async function processGuestOrder(order: Order): Promise { + validateOrder(order); + calculateTaxes(order); + await saveOrder(order); + await sendGuestConfirmation(order); +} +``` + +Existe una secuencia compartida, pero debe comprobarse si representa exactamente el mismo proceso de negocio. + +### 6.4. Extracción justificada + +Si ambos procesos comparten realmente validación, cálculo y persistencia, puede utilizarse una operación común. + +```typescript id="fcu9zd" +async function persistValidatedOrder(order: Order): Promise { + validateOrder(order); + calculateTaxes(order); + await saveOrder(order); +} +``` + +```typescript id="jds64w" +async function processUserOrder(order: Order): Promise { + await persistValidatedOrder(order); +} + +async function processGuestOrder(order: Order): Promise { + await persistValidatedOrder(order); + await sendGuestConfirmation(order); +} +``` + +Este ejemplo presupone que `calculateTaxes()` actualiza correctamente el estado del pedido antes de persistirlo y que ambas operaciones comparten las mismas reglas. + +En una implementación real deben definirse las garantías transaccionales y el tratamiento de errores. + +### 6.5. Evitar abstracciones que alteren el comportamiento + +No introduzcas callbacks genéricos o mecanismos de extensión si su única finalidad es eliminar unas pocas líneas repetidas. + +Las abstracciones deben representar una responsabilidad coherente. + +Cuando existan descuentos, impuestos, pagos y persistencia, el orden de las operaciones puede ser una regla de negocio. + +**Nunca reorganices operaciones únicamente para reducir duplicación si ello altera sus resultados o garantías.** + +--- + +## 7. Separación de responsabilidades (Separation of Concerns) + +> "Cada componente debe ocuparse de un conjunto coherente de responsabilidades." + +La separación de responsabilidades permite organizar el software en unidades comprensibles y con dependencias controladas. + +### Responsabilidades habituales + +- Reglas de negocio. +- Casos de uso. +- Persistencia. +- Presentación. +- Validación. +- Integración con servicios externos. +- Comunicación y notificaciones. + +No es necesario crear una clase independiente para cada una de estas operaciones en todos los proyectos. + +### 7.1. Ejemplo de responsabilidades mezcladas + +```typescript id="8pohc9" +class OrderProcessor { + async process(order: Order): Promise { + if (order.items.length === 0) { + throw new EmptyOrderError(); + } + + const total = order.items.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + + const database = new Database(); + await database.saveOrder(order, total); + + const emailClient = new EmailClient(); + await emailClient.send(order.customer.email, 'Pedido confirmado'); + } +} +``` + +El procesador conoce directamente las reglas del pedido, la infraestructura de persistencia y el proveedor de correo. + +### 7.2. Alternativa con responsabilidades explícitas + +```typescript id="x9g6u2" +class OrderProcessor { + constructor( + private readonly repository: OrderRepository, + private readonly notifier: OrderNotifier, + ) {} + + async process(order: Order): Promise { + order.validate(); + + const total = order.calculateTotal(); + + await this.repository.save(order, total); + await this.notifier.notifyConfirmation(order); + } +} +``` + +En esta alternativa: + +- `Order` protege sus reglas de dominio. +- `OrderRepository` abstrae la persistencia. +- `OrderNotifier` representa las notificaciones. +- `OrderProcessor` coordina el caso de uso. + +No se han introducido validadores ni calculadoras independientes porque el ejemplo no demuestra que sean necesarios. + +### 7.3. Separación no significa fragmentación + +Evita crear estructuras como: + +```text id="jvkg1z" +OrderValidator +OrderCalculationService +OrderTotalFactory +OrderPersistenceCoordinator +OrderNotificationDispatcher +OrderProcessingFacade +``` + +si únicamente delegan llamadas triviales y no representan responsabilidades reales. + +### 7.4. Consistencia entre operaciones + +La separación de responsabilidades no garantiza por sí sola la consistencia del proceso. + +Por ejemplo, después de guardar un pedido puede fallar el envío de correo. + +Debe definirse si: + +- El pedido permanece confirmado. +- La notificación se reintenta. +- Se registra un evento pendiente. +- Se utiliza una cola. +- La operación necesita compensación. + +Estas decisiones deben responder a los requisitos de negocio y arquitectura. + +**Una arquitectura con responsabilidades separadas también necesita contratos y garantías de ejecución bien definidos.** + +--- + +## 8. Gestión de la deuda técnica (Technical Debt) + +La deuda técnica representa decisiones o deficiencias que incrementan el coste futuro de mantenimiento y evolución del software. + +No toda deuda técnica es producto de una mala decisión. Puede responder a compromisos deliberados. + +### 8.1. Tipos de deuda técnica + +**Deuda deliberada** + +Se acepta conscientemente una solución limitada para satisfacer una necesidad concreta. + +Debe conocerse su impacto y establecerse cuándo conviene revisarla. + +**Deuda accidental** + +Surge por errores de diseño, desconocimiento, decisiones inadecuadas o falta de comprensión del sistema. + +**Degradación progresiva** + +Se produce cuando el software pierde adecuación con el tiempo debido a cambios en requisitos, dependencias, plataformas o prácticas técnicas. + +Puede aparecer incluso aunque la implementación inicial fuese correcta. + +### 8.2. Regla del Boy Scout + +> "Deja el código en mejores condiciones de las que lo encontraste." + +Este principio debe aplicarse sin contradecir el control del alcance. + +No significa que cualquier tarea autorice refactorizaciones no solicitadas. + +### Aplicación recomendada + +- Mejora los elementos directamente relacionados con la tarea cuando sea necesario. +- Corrige problemas pequeños que impidan realizar correctamente el cambio. +- Evita modificar código ajeno a la tarea. +- No reformatees archivos completos sin necesidad. +- No introduzcas refactorizaciones extensas de manera oportunista. +- Comunica los problemas relevantes que detectes fuera del alcance. + +### 8.3. Cuándo reducir deuda técnica + +Prioriza la reducción de deuda cuando: + +- Bloquea una funcionalidad. +- Provoca errores. +- Dificulta significativamente las pruebas. +- Incrementa el riesgo de cambios. +- Afecta a componentes que evolucionan frecuentemente. +- Existe una tarea específica de mantenimiento o refactorización. +- Su coste futuro supera razonablemente el coste de corregirla. + +### 8.4. Cuándo posponer una refactorización + +Puede ser apropiado posponerla cuando: + +- El código es estable y no se prevén modificaciones. +- Va a sustituirse próximamente. +- El beneficio esperado es reducido. +- El cambio presenta un riesgo desproporcionado. +- No está relacionada con la tarea actual. +- No existen mecanismos adecuados para verificar una modificación amplia. + +La ausencia de pruebas no debe interpretarse como una prohibición absoluta de refactorizar. + +Cuando sea necesario modificar código legado sin pruebas: + +1. Analiza su comportamiento. +2. Identifica los contratos afectados. +3. Incorpora pruebas de caracterización cuando sea viable. +4. Realiza cambios incrementales. +5. Utiliza los mecanismos de verificación disponibles. + +### 8.5. Registrar deuda técnica relevante + +Cuando corresponda, documenta: + +- Problema identificado. +- Consecuencias. +- Riesgo. +- Motivo por el que se mantiene. +- Posibles actuaciones futuras. + +No generes documentación extensa para problemas triviales. + +--- + +## 9. Los cuatro elementos del diseño simple (XP) + +Los cuatro elementos del diseño simple establecen prioridades para evaluar la calidad estructural del software. + +### 9.1. Supera las pruebas + +El software debe funcionar correctamente y satisfacer los requisitos. + +Las pruebas deben proporcionar confianza suficiente en los comportamientos relevantes. + +No es necesario ejecutar indiscriminadamente toda la batería de pruebas para cada modificación si el alcance permite una verificación más proporcionada. + +### 9.2. Expresa claramente la intención + +El código debe comunicar su propósito. + +Esto incluye: + +- Nombres comprensibles. +- Responsabilidades coherentes. +- Contratos claros. +- Estructuras previsibles. +- Dependencias explícitas cuando corresponda. + +### 9.3. Evita la duplicación de conocimiento + +Aplica DRY con criterio. + +No elimines duplicaciones sintácticas que representen conceptos independientes. + +No introduzcas abstracciones prematuras para satisfacer una regla numérica. + +### 9.4. Mantiene una estructura mínima suficiente + +Evita elementos innecesarios: + +- Clases. +- Interfaces. +- Métodos. +- Capas. +- Dependencias. +- Configuraciones. +- Mecanismos de extensión. + +Sin embargo, no reduzcas artificialmente el número de componentes cuando ello perjudique la cohesión, claridad o mantenibilidad. + +**El objetivo es minimizar la complejidad estructural, no utilizar siempre la menor cantidad posible de código.** + +--- + +## 10. Estrategias para controlar la complejidad + +### 10.1. Favorecer cambios incrementales + +- Divide las modificaciones extensas en pasos comprensibles. +- Conserva el comportamiento existente cuando corresponda. +- Verifica los resultados de cada etapa. +- Evita introducir numerosos cambios independientes simultáneamente. + +### 10.2. Mantener contratos estables + +- Define interfaces comprensibles. +- Evita exponer detalles internos. +- Identifica consumidores antes de modificar contratos públicos. +- Respeta la compatibilidad requerida. + +### 10.3. Controlar dependencias + +- Reutiliza las bibliotecas existentes cuando sean suficientes. +- Prefiere APIs nativas para necesidades sencillas. +- Introduce dependencias nuevas únicamente cuando aporten ventajas claras. +- Evita acoplar reglas de negocio a detalles técnicos innecesarios. + +### 10.4. Aplicar abstracciones justificadas + +Antes de introducir una abstracción, comprueba: + +1. ¿Qué problema resuelve? +2. ¿Qué conocimiento o responsabilidad encapsula? +3. ¿Reduce la complejidad global? +4. ¿Facilita las pruebas o el mantenimiento? +5. ¿Existe una alternativa más sencilla? + +### 10.5. Mantener la arquitectura proporcional + +No todas las aplicaciones necesitan: + +- Arquitectura hexagonal. +- Clean Architecture. +- Microservicios. +- Sistemas de eventos. +- Contenedores de dependencias. +- Múltiples capas de servicios. + +Utiliza estas estructuras cuando los requisitos y la complejidad del sistema lo justifiquen. + +### 10.6. Respetar el código existente + +- No refactorices fuera del alcance. +- Mantén las convenciones del proyecto. +- Evita cambios cosméticos innecesarios. +- Conserva los contratos. +- Comunica los riesgos relevantes. +- No introduzcas nuevas abstracciones únicamente por preferencia personal. + +--- + +## 11. Errores habituales al gestionar la complejidad + +| Error | Consecuencia | Enfoque recomendado | +|---|---|---| +| Confundir simplicidad con pocas líneas | Código compacto pero difícil de comprender. | Evaluar claridad y complejidad global. | +| Aplicar YAGNI de forma absoluta | Ignorar requisitos futuros ya definidos. | Diferenciar necesidades reales de hipótesis. | +| Eliminar toda duplicación | Abstracciones incorrectas y acoplamiento. | Aplicar DRY al conocimiento compartido. | +| Aplicar literalmente la regla de tres | Retrasar abstracciones necesarias o forzar extracciones. | Utilizarla como heurística. | +| Crear una clase por responsabilidad mínima | Fragmentación excesiva. | Agrupar responsabilidades cohesionadas. | +| Refactorizar indiscriminadamente | Cambios fuera del alcance y regresiones. | Controlar el impacto y justificar cambios. | +| Evitar refactorizar por falta de pruebas | Acumulación de problemas estructurales. | Utilizar caracterización y verificación incremental. | +| Introducir patrones prematuramente | Complejidad accidental. | Diseñar para problemas reales. | +| Optimizar sin mediciones | Esfuerzo y complejidad sin beneficio demostrado. | Medir antes de optimizar cuando sea posible. | +| Ocultar dependencias | Comportamientos inesperados. | Mantener contratos y colaboraciones claras. | + +--- + +## 12. Lista de comprobación de complejidad + +### Antes de implementar + +- [ ] ¿Comprendo los requisitos reales? +- [ ] ¿He distinguido la complejidad esencial de la accidental? +- [ ] ¿Existe una solución directa y comprensible? +- [ ] ¿Estoy introduciendo capacidades para necesidades hipotéticas? +- [ ] ¿Puedo reutilizar mecanismos existentes? +- [ ] ¿He evaluado el impacto sobre los contratos actuales? + +### Durante la implementación + +- [ ] ¿Cada abstracción tiene una responsabilidad clara? +- [ ] ¿Las dependencias están justificadas? +- [ ] ¿El código mantiene un flujo comprensible? +- [ ] ¿Estoy evitando duplicar reglas de negocio? +- [ ] ¿He evitado fragmentaciones artificiales? +- [ ] ¿Estoy respetando el alcance de la tarea? +- [ ] ¿La solución preserva las invariantes y garantías necesarias? + +### Durante la refactorización + +- [ ] ¿Existe un problema real que justifique el cambio? +- [ ] ¿La modificación reduce la complejidad global? +- [ ] ¿Se conserva el comportamiento observable? +- [ ] ¿Dispongo de mecanismos de verificación adecuados? +- [ ] ¿Estoy evitando cambios ajenos al objetivo? +- [ ] ¿La nueva estructura será más sencilla de mantener? + +### Al finalizar + +- [ ] ¿La solución satisface los requisitos? +- [ ] ¿Se han ejecutado las verificaciones pertinentes? +- [ ] ¿El código expresa claramente su intención? +- [ ] ¿Se han evitado dependencias innecesarias? +- [ ] ¿Los contratos afectados mantienen la compatibilidad requerida? +- [ ] ¿He identificado limitaciones o deuda técnica relevante? +- [ ] ¿Otro desarrollador podría comprender y modificar la solución razonablemente? + +--- + +## Principio final + +> "La complejidad que pertenece al problema debe modelarse correctamente. La complejidad que introducimos nosotros debe justificarse." + +Aplica KISS, YAGNI, DRY y los valores de Extreme Programming como herramientas para tomar mejores decisiones, no como restricciones mecánicas. + +Evita tanto la sobreingeniería como las simplificaciones que comprometan requisitos, contratos o seguridad. + +**La solución preferible es aquella que satisface correctamente las necesidades actuales y los requisitos definidos, introduce la menor complejidad accidental razonable y permite evolucionar el sistema de forma segura y mantenible.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/diseño-de-objetos.md b/.agents/skills/rdx-solid/referencias/diseño-de-objetos.md new file mode 100644 index 0000000..28cc91e --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/diseño-de-objetos.md @@ -0,0 +1,930 @@ +# Diseño orientado a objetos (Object-Oriented Design) + +El diseño orientado a objetos permite organizar el software mediante componentes que encapsulan estado, comportamiento y responsabilidades. + +Su objetivo es facilitar la comprensión, evolución y mantenimiento del sistema mediante objetos cohesionados, contratos claros y dependencias controladas. + +Este documento desarrolla los principios establecidos en el `SKILL.md` principal y debe interpretarse siguiendo sus criterios de aplicación. + +**El objetivo no es maximizar el número de clases, interfaces o patrones, sino encontrar una organización sencilla y coherente con el dominio y los requisitos reales.** + +## 1. Diseño guiado por responsabilidades (Responsibility-Driven Design, RDD) + +La idea fundamental del diseño guiado por responsabilidades es: + +> "Los objetos se definen principalmente por sus responsabilidades, no por los datos que almacenan." + +Una responsabilidad describe aquello que un objeto conoce, hace o decide dentro del sistema. + +### 1.1. Identificación de objetos + +Al analizar requisitos, utiliza las siguientes pistas: + +1. **Sustantivos:** pueden sugerir entidades, objetos de valor o conceptos del dominio. +2. **Verbos:** pueden sugerir operaciones, comportamientos o casos de uso. +3. **Conceptos del dominio:** pueden justificar objetos con identidad, objetos de valor o servicios especializados. + +Por ejemplo, ante el requisito: + +"Un cliente puede realizar un pedido que contiene productos y al que se pueden aplicar descuentos." + +Podrían identificarse: + +- `Customer`: cliente. +- `Order`: pedido. +- `OrderItem`: línea de pedido. +- `Product`: producto. +- `DiscountPolicy`: política de descuentos. + +Sin embargo, **no conviertas automáticamente cada sustantivo en una clase ni cada verbo en un método**. + +Comprueba que los elementos identificados representan conceptos reales y responsabilidades necesarias. + +### 1.2. Identificación de responsabilidades + +Para cada objeto, considera: + +- **¿Qué conoce?** Información o estado del que es responsable. +- **¿Qué hace?** Operaciones o comportamientos que proporciona. +- **¿Qué decide?** Reglas o decisiones que le corresponden. +- **¿Qué invariantes protege?** Condiciones que deben mantenerse válidas. +- **¿De quién depende?** Colaboradores necesarios para cumplir su responsabilidad. + +El objetivo es asignar cada comportamiento al componente que tenga la información y responsabilidad adecuadas. + +### 1.3. Estereotipos de objetos + +Los estereotipos permiten identificar responsabilidades habituales. + +| Estereotipo | Propósito | Ejemplo | +|---|---|---| +| **Information Holder (Contenedor de información)** | Mantiene información o estado del dominio. | `User`, `Product`, `Address` | +| **Structurer (Estructurador)** | Gestiona relaciones entre objetos. | `OrderItems`, `UserGroup` | +| **Service Provider (Proveedor de servicios)** | Ejecuta operaciones o proporciona capacidades específicas. | `PaymentProcessor`, `EmailSender` | +| **Coordinator (Coordinador)** | Orquesta procesos y la colaboración entre componentes. | `OrderFulfillmentService` | +| **Controller (Controlador)** | Recibe solicitudes, gestiona decisiones de control y delega operaciones. | `CheckoutController` | +| **Interfacer (Adaptador de interfaces)** | Adapta información o contratos entre sistemas. | `UserApiAdapter`, `DatabaseMapper` | + +Estos estereotipos son orientativos. + +Una clase puede combinar responsabilidades compatibles sin que ello implique necesariamente un problema de diseño. + +Tampoco es obligatorio que toda clase encaje perfectamente en una de estas categorías. + +### 1.4. Preguntas fundamentales + +Para cada clase, módulo o componente, pregúntate: + +1. ¿Cuál es su responsabilidad principal? +2. ¿Qué motivos justificarían modificarlo? +3. ¿Está asumiendo responsabilidades independientes? +4. ¿Sus dependencias son necesarias? +5. ¿Existe una organización más sencilla? +6. ¿Representa correctamente un concepto o comportamiento del dominio? + +Si las respuestas no resultan claras, revisa el diseño. + +**No refactorices automáticamente una clase porque no corresponda a un patrón conocido o porque supere una métrica numérica.** + +--- + +## 2. Tell, Don't Ask (Ordena, no preguntes) + +Este principio propone delegar el comportamiento en los objetos responsables, en lugar de consultar su estado interno y tomar decisiones desde el exterior. + +### Ejemplo: retirada de dinero + +**Menos adecuado: lógica de negocio fuera del objeto** + +```typescript +// MAL: El consumidor modifica directamente el saldo. +if (account.getBalance() >= amount) { + account.setBalance(account.getBalance() - amount); +} +``` + +Este enfoque permite que distintos consumidores implementen reglas diferentes y puede comprometer las invariantes de la cuenta. + +**Más adecuado: comportamiento encapsulado** + +```typescript +const result = account.withdraw(amount); + +if (result.isSuccess()) { + // Continuar con la operación. +} +``` + +La cuenta es responsable de comprobar si la retirada está permitida y actualizar su estado correctamente. + +### Criterios de aplicación + +- Encapsula las reglas de negocio junto al estado que deben proteger. +- Evita que los consumidores reproduzcan decisiones propias de un objeto. +- Expón operaciones que expresen intenciones del dominio. +- Reduce la necesidad de consultar y modificar repetidamente el estado interno. + +### Excepciones legítimas + +No apliques *Tell, Don't Ask* indiscriminadamente. + +Consultar datos es adecuado cuando se trabaja con: + +- DTO. +- Modelos de presentación. +- Respuestas de API. +- Objetos de configuración. +- Estructuras destinadas al transporte de datos. +- Consultas o proyecciones de solo lectura. + +**El principio debe proteger el encapsulamiento y las invariantes, no impedir el acceso legítimo a la información.** + +--- + +## 3. Diseño por contrato (Design by Contract, DbC) + +El diseño por contrato establece las condiciones que deben cumplirse antes, durante y después de ejecutar una operación. + +### Elementos de un contrato + +**Precondiciones:** condiciones que deben cumplirse antes de ejecutar una operación. + +**Postcondiciones:** garantías que proporciona una operación al completarse correctamente. + +**Invariantes:** condiciones que deben mantenerse válidas durante la vida observable del objeto. + +### Ejemplo + +```typescript +class BankAccount { + constructor(private balance: Money) { + if (balance.isNegative()) { + throw new InvalidInitialBalanceError(); + } + } + + withdraw(amount: Money): WithdrawResult { + if (!amount.isPositive()) { + return WithdrawResult.invalidAmount(); + } + + if (this.balance.isLessThan(amount)) { + return WithdrawResult.insufficientFunds(); + } + + this.balance = this.balance.subtract(amount); + + return WithdrawResult.success(this.balance); + } +} +``` + +En este ejemplo: + +- **Invariante:** el saldo nunca puede ser negativo. +- **Precondición de éxito:** el importe debe ser positivo y no superar el saldo disponible. +- **Postcondición de éxito:** el saldo disminuye exactamente en el importe retirado. +- **Resultado de rechazo:** se devuelve un resultado que identifica el motivo sin modificar el saldo. + +El ejemplo presupone que `Money` impide operaciones entre monedas incompatibles y utiliza una representación monetaria adecuada. + +### Criterios de diseño por contrato + +- Define las invariantes relevantes del dominio. +- Valida las precondiciones en los límites apropiados. +- Evita que las operaciones dejen objetos en estados inválidos. +- Expresa claramente los resultados esperados. +- Utiliza tipos para representar restricciones cuando sea apropiado. +- No dupliques validaciones innecesariamente en todas las capas. +- Distingue entre errores excepcionales y rechazos de negocio esperados. + +No es obligatorio documentar expresamente cada precondición y postcondición cuando el contrato resulte evidente mediante los tipos, nombres y pruebas. + +### Contratos y compatibilidad + +Al modificar una operación existente: + +- Identifica sus consumidores. +- Mantén las garantías previamente establecidas. +- No debilites invariantes sin una justificación funcional. +- Evita cambios silenciosos en el tratamiento de errores. +- Comprueba el principio de sustitución de Liskov cuando exista herencia o implementación de interfaces. + +--- + +## 4. Composición frente a herencia (Composition Over Inheritance) + +La composición permite construir comportamientos mediante la colaboración entre objetos. + +La herencia permite especializar tipos y reutilizar contratos o comportamientos comunes. + +**Prefiere composición cuando aporte mayor flexibilidad, menor acoplamiento o una representación más adecuada del dominio.** + +### 4.1. Riesgos de la herencia + +- Acoplamiento entre clases base y derivadas. +- Dependencia de detalles de implementación heredados. +- Dificultad para modificar clases base sin afectar a sus derivados. +- Jerarquías difíciles de comprender. +- Relaciones artificiales de tipo "es un". +- Problemas de sustitución cuando las clases derivadas alteran contratos. + +Estos riesgos no implican que la herencia sea incorrecta por definición. + +### 4.2. Cuándo utilizar herencia + +Puede ser apropiada cuando: + +- Existe una relación conceptual real de especialización. +- Las clases derivadas cumplen el contrato del tipo base. +- El framework requiere una jerarquía concreta. +- Se utiliza deliberadamente un patrón como *Template Method*. +- La reutilización y extensión del comportamiento justifican la estructura. + +No utilices herencia únicamente para compartir unas pocas líneas de implementación. + +### 4.3. Composición mediante políticas + +**Ejemplo basado en herencia:** + +```typescript +class PremiumUser extends User { + getDiscount(): number { + return 20; + } +} +``` + +Este diseño puede resultar válido, pero vincula el comportamiento del descuento con la jerarquía de usuarios. + +**Alternativa mediante composición:** + +```typescript +interface DiscountPolicy { + calculate(): number; +} +``` + +```typescript +class User { + constructor( + private readonly discountPolicy: DiscountPolicy, + ) {} + + getDiscount(): number { + return this.discountPolicy.calculate(); + } +} +``` + +```typescript +class PremiumDiscount implements DiscountPolicy { + calculate(): number { + return 20; + } +} + +class StandardDiscount implements DiscountPolicy { + calculate(): number { + return 5; + } +} + +class NoDiscount implements DiscountPolicy { + calculate(): number { + return 0; + } +} +``` + +```typescript +const premiumUser = new User(new PremiumDiscount()); +const standardUser = new User(new StandardDiscount()); +const regularUser = new User(new NoDiscount()); +``` + +La composición permite variar la política sin crear nuevas subclases de usuario. + +### Criterios para elegir + +- Utiliza composición cuando necesites combinar o sustituir comportamientos independientes. +- Utiliza herencia cuando exista una especialización legítima con sustitución segura. +- No introduzcas una interfaz y varias clases si un condicional sencillo resuelve adecuadamente un comportamiento estable. +- Evita crear estrategias intercambiables para variaciones hipotéticas. +- Considera funciones y objetos sencillos como alternativas a jerarquías complejas. + +**La composición es una preferencia de diseño, no una prohibición de utilizar herencia.** + +--- + +## 5. Ley de Deméter (Law of Demeter) + +La Ley de Deméter, también conocida como principio de mínimo conocimiento, propone limitar las dependencias de un componente respecto a las estructuras internas de otros. + +De manera orientativa, un método debería colaborar principalmente con: + +1. El propio objeto (`this`). +2. Los parámetros recibidos. +3. Los objetos que crea. +4. Sus colaboradores directos. + +### Ejemplo + +**Acoplamiento a estructuras internas:** + +```typescript +const city = order.getCustomer().getAddress().getCity(); +``` + +Este código conoce la organización interna del pedido, el cliente y su dirección. + +**Alternativa con una operación de dominio:** + +```typescript +const city = order.getShippingCity(); +``` + +La segunda alternativa puede reducir el acoplamiento cuando la ciudad de envío sea una responsabilidad del pedido. + +### Interpretación correcta + +La Ley de Deméter no significa que deba existir un único punto por línea. + +No prohíbe: + +- APIs fluidas. +- Encadenamiento de operaciones sobre colecciones. +- Acceso a estructuras de datos públicas. +- Navegación legítima por DTO. +- Composición de operaciones sobre valores inmutables. + +### Criterios de aplicación + +- Evita que una clase dependa de detalles internos innecesarios de sus colaboradores. +- Prefiere operaciones que expresen comportamientos del dominio. +- No añadas métodos delegadores triviales únicamente para ocultar una cadena de acceso. +- Evalúa si la navegación introduce un acoplamiento real. +- Prioriza la claridad y la estabilidad de los contratos. + +--- + +## 6. Encapsulamiento (Encapsulation) + +El encapsulamiento consiste en controlar el acceso al estado y a los detalles internos de un componente, exponiendo únicamente los contratos necesarios. + +### 6.1. Niveles de encapsulamiento + +1. **Datos:** controlar el acceso y modificación del estado. +2. **Implementación:** ocultar los mecanismos internos. +3. **Tipos:** desacoplar consumidores de implementaciones concretas cuando sea necesario. +4. **Diseño:** ocultar decisiones arquitectónicas que no deban afectar a los consumidores. + +No todos los componentes necesitan aplicar todos estos niveles. + +### 6.2. Protección de invariantes + +**Ejemplo problemático:** + +```typescript +class Order { + public items: Item[] = []; + public total = 0; +} +``` + +Un consumidor puede modificar el estado sin respetar las reglas del pedido. + +```typescript +order.items.push(item); +order.total = -999; +``` + +**Alternativa encapsulada:** + +```typescript +class Order { + private readonly items: Item[] = []; + private total: Money; + + constructor() { + this.total = Money.zero('EUR'); + } + + addItem(item: Item): void { + const nextTotal = this.total.add(item.total); + + this.validateTotal(nextTotal); + + this.items.push(item); + this.total = nextTotal; + } + + getTotal(): Money { + return this.total; + } + + private validateTotal(total: Money): void { + if (total.isNegative()) { + throw new InvalidOrderTotalError(); + } + } +} +``` + +Este ejemplo presupone que `Money` es inmutable y que las operaciones utilizadas preservan sus invariantes. + +### Consideraciones + +- Mantén privadas las estructuras internas cuyo acceso pueda comprometer invariantes. +- Expón operaciones de dominio para modificar estados protegidos. +- Utiliza propiedades de solo lectura cuando sea adecuado. +- Evita devolver referencias mutables que permitan modificar indirectamente el estado. +- No introduzcas getters o métodos delegadores innecesarios. +- No encapsules indiscriminadamente datos cuyo propósito legítimo sea ser públicos. + +### Encapsulamiento e inmutabilidad + +La inmutabilidad facilita razonar sobre el estado y evita modificaciones inesperadas. + +Prefiere valores inmutables cuando resulte apropiado, especialmente para objetos de valor. + +No obstante, las entidades y otros objetos pueden mantener estado mutable siempre que las modificaciones estén controladas. + +--- + +## 7. Polimorfismo (Polymorphism) + +El polimorfismo permite utilizar distintas implementaciones mediante un contrato común. + +Puede mejorar la extensibilidad cuando existen comportamientos alternativos bien definidos. + +**No debe utilizarse indiscriminadamente para sustituir todos los condicionales.** + +### 7.1. Implementación mediante condicionales + +```typescript +type ShippingType = 'standard' | 'express' | 'overnight'; + +function calculateShipping( + method: ShippingType, + orderValue: number, +): number { + if (method === 'standard') { + return orderValue < 50 ? 5 : 0; + } + + if (method === 'express') { + return 15; + } + + return 25; +} +``` + +Esta implementación es sencilla y puede ser perfectamente adecuada cuando los métodos de envío son pocos y estables. + +### 7.2. Alternativa polimórfica + +Cuando los métodos de envío incorporan reglas propias y evolucionan independientemente, puede resultar útil definir un contrato. Las implementaciones canónicas de `StandardShipping`, `ExpressShipping` y el consumidor `calculateShipping` se mantienen en [principios-solid.md](principios-solid.md), sección 2.3. Aquí se muestra únicamente una variante adicional para evitar mantener copias del mismo código. + +```typescript +interface ShippingMethod { + calculateCost(orderValue: number): number; +} +``` + +```typescript +class OvernightShipping implements ShippingMethod { + calculateCost(_orderValue: number): number { + return 25; + } +} +``` + +### 7.3. Cuándo utilizar polimorfismo + +Considera este enfoque cuando: + +- Existan varias implementaciones con comportamiento significativo. +- Cada alternativa tenga reglas propias. +- Sea necesario incorporar nuevos comportamientos sin modificar consumidores. +- Los condicionales relacionados aparezcan repetidamente. +- Las implementaciones deban probarse o evolucionar de forma independiente. + +Evítalo cuando: + +- El comportamiento sea muy sencillo. +- Las alternativas sean pocas y estables. +- La jerarquía incremente innecesariamente el número de componentes. +- El contrato común sea artificial. +- Una función o un mapa de operaciones proporcione una solución más clara. + +**El polimorfismo debe reducir el acoplamiento o simplificar la evolución, no limitarse a eliminar estructuras `if` o `switch`.** + +--- + +## 8. Objetos de valor y entidades (Value Objects vs Entities) + +Los objetos de valor y las entidades representan conceptos diferentes dentro del modelo de dominio. + +### 8.1. Objetos de valor (Value Objects) + +Los objetos de valor: + +- Se definen por sus atributos y significado. +- No poseen una identidad independiente relevante para el dominio. +- Deben ser inmutables desde la perspectiva de sus consumidores. +- Se comparan por sus valores. +- Pueden encapsular validaciones, invariantes y operaciones. + +Ejemplos habituales: + +- `Money` +- `Email` +- `Address` +- `DateRange` + +### Ejemplo: Money + +```typescript +class Money { + constructor( + private readonly amount: number, + private readonly currency: string, + ) { + if (!Number.isFinite(amount)) { + throw new InvalidMoneyAmountError(); + } + } + + equals(other: Money): boolean { + return ( + this.amount === other.amount && + this.currency === other.currency + ); + } + + add(other: Money): Money { + if (this.currency !== other.currency) { + throw new CurrencyMismatchError(); + } + + return new Money( + this.amount + other.amount, + this.currency, + ); + } +} +``` + +Este ejemplo ilustra la igualdad por valor y la inmutabilidad, pero no constituye una implementación monetaria completa. + +En sistemas de producción, evita errores de precisión monetaria utilizando una representación adecuada, como unidades mínimas enteras o tipos decimales. + +### Criterios para utilizar objetos de valor + +- Introduce objetos de valor cuando exista semántica de dominio. +- Encapsula validaciones e invariantes relevantes. +- Evita utilizar tipos primitivos intercambiables cuando puedan provocar errores. +- No crees objetos de valor para cualquier cadena o número. +- Considera tipos nominales de TypeScript cuando únicamente necesites diferenciar identificadores. +- No introduzcas abstracciones que no aporten seguridad, comportamiento o expresividad. + +### 8.2. Entidades (Entities) + +Las entidades: + +- Poseen una identidad que permanece aunque cambien sus atributos. +- Normalmente permiten modificar su estado mediante operaciones controladas. +- Se comparan por identidad cuando corresponde. +- Pueden encapsular reglas e invariantes del dominio. + +Ejemplos habituales: + +- `User` +- `Order` +- `Product` + +### Ejemplo: User + +```typescript +class User { + constructor( + private readonly id: UserId, + private email: Email, + private name: Name, + ) {} + + equals(other: User): boolean { + return this.id.equals(other.id); + } + + changeEmail(newEmail: Email): void { + this.email = newEmail; + } +} +``` + +El usuario conserva su identidad aunque cambie su dirección de correo electrónico. + +La operación `changeEmail` podría incorporar reglas adicionales, como comprobar restricciones de negocio, según las responsabilidades del modelo. + +### Diferencia fundamental + +| Característica | Objeto de valor | Entidad | +|---|---|---| +| Identidad propia | No | Sí | +| Comparación | Por valor | Por identidad | +| Mutabilidad | Preferentemente inmutable | Puede modificar su estado | +| Responsabilidad | Representar un concepto mediante sus valores | Representar un concepto con continuidad e identidad | +| Ejemplos | `Money`, `Email` | `User`, `Order` | + +No conviertas automáticamente todos los datos en entidades u objetos de valor. + +Las estructuras de transporte de datos también tienen un papel legítimo dentro de una arquitectura. + +--- + +## 9. Agregados (Aggregates) + +Un agregado es un conjunto de entidades y objetos de valor que se trata como una unidad de consistencia dentro de un dominio. + +Este concepto procede del diseño guiado por el dominio (*Domain-Driven Design*, DDD). + +### 9.1. Elementos de un agregado + +**Raíz del agregado (Aggregate Root):** + +- Constituye el punto principal de acceso al agregado. +- Protege sus invariantes. +- Controla las modificaciones de los elementos internos. +- Define los límites de consistencia. + +**Elementos internos:** + +- Pueden incluir entidades y objetos de valor. +- Participan en las reglas del agregado. +- No deberían modificarse desde el exterior eludiendo la raíz. + +### 9.2. Ejemplo: pedido + +Supongamos que un pedido tiene un importe máximo permitido. + +La raíz del agregado debe impedir que una operación deje el pedido en un estado inválido. + +```typescript +class Order { + private items: OrderItem[] = []; + + addItem(product: Product, quantity: number): void { + const item = new OrderItem(product, quantity); + + const nextItems = [...this.items, item]; + const nextTotal = this.calculateTotal(nextItems); + + this.validateTotal(nextTotal); + + this.items = nextItems; + } + + removeItem(itemId: ItemId): void { + const nextItems = this.items.filter( + (item) => !item.id.equals(itemId), + ); + + const nextTotal = this.calculateTotal(nextItems); + + this.validateTotal(nextTotal); + + this.items = nextItems; + } + + private calculateTotal(items: OrderItem[]): Money { + return items.reduce( + (total, item) => total.add(item.total), + Money.zero('EUR'), + ); + } + + private validateTotal(total: Money): void { + if (total.isGreaterThan(MAX_ORDER_VALUE)) { + throw new OrderTotalExceededError(); + } + } +} +``` + +El ejemplo presupone que los importes utilizan una moneda compatible y que `OrderItem` valida la cantidad y calcula correctamente su importe. + +### 9.3. Preservación de invariantes + +Observa que el ejemplo calcula el nuevo estado antes de modificar la colección. + +El orden es importante: + +1. Preparar el estado propuesto. +2. Calcular sus consecuencias. +3. Validar las invariantes. +4. Aplicar la modificación. + +De este modo, si la validación falla, el estado del pedido permanece sin cambios. + +**No modifiques primero el agregado para validar después, salvo que exista un mecanismo fiable que garantice la reversión del estado.** + +### 9.4. Acceso a través de la raíz + +**Incorrecto: modificación externa que evita las reglas** + +```typescript +order.items.push(new OrderItem(product, 2)); +``` + +Este acceso no debe estar disponible si permite incumplir las invariantes del agregado. + +**Correcto: operación controlada** + +```typescript +order.addItem(product, 2); +``` + +La raíz aplica las reglas necesarias antes de modificar el estado. + +### 9.5. Criterios para diseñar agregados + +- Define agregados alrededor de invariantes que deban mantenerse consistentes. +- Utiliza raíces de agregado cuando exista una necesidad real de controlar modificaciones conjuntas. +- Evita agregados excesivamente grandes. +- No incluyas entidades dentro de un mismo agregado únicamente porque estén relacionadas. +- Considera el coste transaccional y la concurrencia. +- Mantén referencias entre agregados mediante identificadores cuando sea apropiado. +- Distingue las operaciones que necesitan consistencia inmediata de las que admiten consistencia eventual. +- No introduzcas agregados DDD para modelos sencillos que no necesiten este nivel de organización. + +### 9.6. Agregados y persistencia + +La raíz del agregado controla las invariantes de dominio, pero no garantiza por sí sola la atomicidad de la persistencia. + +Cuando sea necesario: + +- Utiliza transacciones. +- Considera mecanismos de control de concurrencia. +- Evita modificaciones parciales de datos persistidos. +- Define explícitamente los límites de consistencia. +- Gestiona los conflictos entre operaciones concurrentes. + +**La consistencia del agregado requiere tanto un modelo de dominio correcto como una estrategia de persistencia adecuada.** + +--- + +## 10. Responsabilidades y servicios + +No todas las operaciones pertenecen naturalmente a una entidad. + +Algunas responsabilidades necesitan coordinar varios objetos o interactuar con servicios externos. + +### Servicios de dominio + +Representan operaciones del dominio que no pertenecen claramente a una única entidad u objeto de valor. + +Deben contener comportamiento de negocio y evitar dependencias innecesarias de infraestructura. + +### Servicios de aplicación + +Orquestan casos de uso. + +Pueden encargarse de: + +- Coordinar entidades y servicios de dominio. +- Controlar transacciones. +- Invocar repositorios. +- Gestionar operaciones externas. +- Aplicar políticas de autorización cuando corresponda. + +Evita trasladar todas las reglas de negocio a los servicios de aplicación si pertenecen naturalmente a las entidades u objetos de valor. + +### Servicios de infraestructura + +Implementan capacidades técnicas como: + +- Persistencia. +- Correo electrónico. +- Acceso a archivos. +- Comunicación con APIs externas. +- Servicios de mensajería. + +Cuando resulte apropiado, utiliza contratos que permitan mantener desacopladas las políticas de dominio de los detalles de infraestructura. + +### Evitar servicios todopoderosos + +Una clase denominada `ApplicationManager`, `BusinessService` o `DataProcessor` puede estar acumulando responsabilidades independientes. + +Revisa: + +- Si contiene operaciones sin relación clara. +- Si necesita demasiadas dependencias. +- Si cambia por motivos muy diferentes. +- Si concentra reglas que deberían pertenecer a otros objetos. + +No dividas automáticamente los servicios grandes sin identificar previamente responsabilidades coherentes. + +--- + +## 11. Criterios para evitar la sobreingeniería + +Un diseño orientado a objetos no es necesariamente mejor por incorporar más clases, interfaces o patrones. + +Antes de introducir una abstracción, considera: + +1. ¿Qué problema concreto resuelve? +2. ¿Qué responsabilidad representa? +3. ¿Reduce realmente el acoplamiento? +4. ¿Simplifica las pruebas o el mantenimiento? +5. ¿Existe una alternativa más directa? +6. ¿Su coste estructural está justificado? + +### Evita especialmente + +- Interfaces con una única implementación creadas sin necesidad de desacoplamiento. +- Jerarquías de herencia destinadas únicamente a compartir código trivial. +- Factorías que solo encapsulan constructores sencillos. +- Servicios que únicamente delegan llamadas. +- Objetos de valor sin semántica propia. +- Agregados sin invariantes relevantes. +- Patrones introducidos para necesidades hipotéticas. +- Refactorizaciones destinadas exclusivamente a cumplir límites numéricos. + +### Favorece + +- Responsabilidades claras. +- Contratos comprensibles. +- Dependencias controladas. +- Composición cuando aporte flexibilidad. +- Encapsulamiento de invariantes. +- Simplicidad estructural. +- Evolución incremental del diseño. + +--- + +## 12. Lista de comprobación de diseño orientado a objetos + +### Responsabilidades + +- [ ] ¿Cada objeto tiene una responsabilidad identificable? +- [ ] ¿Sus comportamientos pertenecen al concepto que representa? +- [ ] ¿He evitado clases que acumulen responsabilidades independientes? +- [ ] ¿Las dependencias son necesarias? +- [ ] ¿La organización facilita la evolución del sistema? + +### Encapsulamiento + +- [ ] ¿Las invariantes están protegidas? +- [ ] ¿El estado mutable se modifica mediante operaciones controladas? +- [ ] ¿He evitado exponer referencias mutables que comprometan el estado? +- [ ] ¿Los contratos públicos son claros? +- [ ] ¿Los datos se exponen cuando su finalidad lo justifica? + +### Composición y herencia + +- [ ] ¿La composición mejora el diseño en este caso? +- [ ] ¿Las relaciones de herencia representan especializaciones legítimas? +- [ ] ¿Los subtipos respetan los contratos del tipo base? +- [ ] ¿He evitado jerarquías artificiales? +- [ ] ¿Las abstracciones responden a necesidades reales? + +### Objetos de valor y entidades + +- [ ] ¿He distinguido correctamente identidad y valor? +- [ ] ¿Los objetos de valor protegen sus invariantes? +- [ ] ¿Las entidades conservan su identidad al modificar sus atributos? +- [ ] ¿He evitado encapsular primitivas innecesariamente? +- [ ] ¿Las operaciones de dominio están correctamente ubicadas? + +### Agregados + +- [ ] ¿Existe una necesidad real de utilizar un agregado? +- [ ] ¿La raíz protege las invariantes? +- [ ] ¿Las modificaciones se realizan mediante operaciones controladas? +- [ ] ¿Los límites de consistencia están correctamente definidos? +- [ ] ¿He considerado transacciones y concurrencia cuando corresponde? +- [ ] ¿He evitado agregados excesivamente grandes? + +### Complejidad + +- [ ] ¿La solución es suficientemente sencilla? +- [ ] ¿He evitado patrones innecesarios? +- [ ] ¿Existe una alternativa más directa? +- [ ] ¿La estructura facilita las pruebas? +- [ ] ¿He respetado la arquitectura existente? +- [ ] ¿La modificación mantiene el comportamiento y los contratos requeridos? + +--- + +## Principio final + +> "Los objetos deben organizarse alrededor de responsabilidades reales y proteger las reglas del dominio, no existir únicamente para satisfacer patrones de diseño." + +El diseño orientado a objetos debe facilitar la comprensión, la evolución y la verificación del software. + +Utiliza entidades, objetos de valor, servicios, composición, herencia y agregados cuando aporten ventajas concretas. + +**Una arquitectura sencilla, cohesionada y con contratos bien definidos es preferible a una estructura artificialmente compleja que únicamente aparenta seguir principios de diseño orientado a objetos.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/patrones-de-diseño.md b/.agents/skills/rdx-solid/referencias/patrones-de-diseño.md new file mode 100644 index 0000000..755775c --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/patrones-de-diseño.md @@ -0,0 +1,1284 @@ +# Patrones de diseño (Design Patterns) + +Los patrones de diseño son soluciones reutilizables a problemas recurrentes de diseño de software. + +Proporcionan un vocabulario común que facilita comunicar decisiones arquitectónicas, reconocer estructuras y organizar responsabilidades. + +Este documento desarrolla los criterios definidos en el `SKILL.md` principal. + +**Los patrones deben utilizarse cuando resuelvan un problema real, no como objetivos de implementación ni como demostración de sofisticación técnica.** + +## 1. Principios fundamentales + +### No fuerces la utilización de patrones + +> "Permite que los patrones surjan de las necesidades del diseño y de la refactorización, en lugar de imponerlos anticipadamente." + +Un patrón debe resolver un problema existente o responder a un requisito de evolución suficientemente definido. + +No introduzcas abstracciones destinadas exclusivamente a necesidades hipotéticas. + +### Cuándo utilizar un patrón + +Antes de aplicarlo, comprueba: + +1. **Problema identificado:** ¿Existe un problema concreto que el patrón resuelve? +2. **Adecuación:** ¿El patrón representa correctamente las responsabilidades y relaciones? +3. **Simplicidad:** ¿Mejora el diseño sin introducir complejidad innecesaria? +4. **Mantenibilidad:** ¿Facilita comprender, modificar o probar el código? +5. **Conocimiento del equipo:** ¿La estructura resultará comprensible para otros desarrolladores? +6. **Alternativas:** ¿Existe una función, composición u objeto más sencillo que resuelva adecuadamente el problema? + +### Principios de aplicación + +- Prioriza soluciones directas cuando sean suficientes. +- Utiliza patrones conocidos cuando reduzcan complejidad o acoplamiento. +- No introduzcas jerarquías de clases para comportamientos triviales. +- No crees interfaces sin una responsabilidad o contrato identificable. +- Prefiere composición cuando aporte flexibilidad. +- Respeta los patrones y convenciones ya establecidos en el proyecto. +- No refactorices código estable únicamente para incorporar un patrón diferente. +- Valora el coste de mantenimiento de cada abstracción. + +**Un patrón correcto debe simplificar el razonamiento sobre el sistema, no limitarse a reorganizar código en más archivos.** + +--- + +## 2. Patrones creacionales (Creational Patterns) + +Los patrones creacionales proporcionan mecanismos para construir objetos y controlar su creación. + +### 2.1. Singleton + +**Propósito:** garantizar que exista una única instancia de una clase dentro de un ámbito determinado. + +**Posibles aplicaciones:** + +- Recursos compartidos que requieren una única instancia por proceso. +- Coordinadores cuyo ciclo de vida debe estar controlado. +- Componentes con restricciones explícitas de unicidad. + +**Advertencia:** Singleton suele utilizarse excesivamente. + +No debe ser la solución predeterminada para: + +- Configuración. +- Logging. +- Acceso a bases de datos. +- Clientes HTTP. +- Servicios de aplicación. + +En muchos casos, resulta preferible crear una instancia compartida durante la inicialización de la aplicación e inyectarla donde sea necesaria. + +#### Ejemplo clásico + +```typescript id="n0dk11" +class Logger { + private static instance: Logger | undefined; + + private constructor() {} + + static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + + return Logger.instance; + } + + log(message: string): void { + console.log(message); + } +} +``` + +#### Alternativa mediante composición + +```typescript id="ptj27s" +class Logger { + log(message: string): void { + console.log(message); + } +} + +const logger = new Logger(); + +const orderService = new OrderService(logger); +const paymentService = new PaymentService(logger); +``` + +Este enfoque permite compartir una instancia sin ocultar las dependencias de los consumidores. + +#### Criterios de utilización + +- Utiliza Singleton únicamente cuando la unicidad forme parte del requisito. +- Evita convertir dependencias compartidas en estado global accesible desde cualquier lugar. +- Considera el impacto sobre las pruebas y el aislamiento. +- Distingue entre una instancia única por proceso y una instancia única en todo un sistema distribuido. +- No utilices Singleton para resolver problemas que pertenecen a la gestión del ciclo de vida de dependencias. + +--- + +### 2.2. Factory (Factoría) + +**Propósito:** encapsular la creación de objetos cuando esta requiera decisiones, validaciones o configuraciones específicas. + +**Cuándo utilizarlo:** + +- La creación depende de parámetros o configuraciones. +- Existen varias implementaciones de un contrato. +- La construcción requiere lógica significativa. +- Se necesita centralizar una política de creación. + +#### Ejemplo + +```typescript id="0n7hgs" +interface Notification { + send(message: string): Promise; +} + +class EmailNotification implements Notification { + async send(message: string): Promise { + await sendEmail(message); + } +} + +class SmsNotification implements Notification { + async send(message: string): Promise { + await sendSms(message); + } +} + +class PushNotification implements Notification { + async send(message: string): Promise { + await sendPush(message); + } +} +``` + +```typescript id="4f1mr6" +type NotificationType = 'email' | 'sms' | 'push'; + +function createNotification(type: NotificationType): Notification { + switch (type) { + case 'email': + return new EmailNotification(); + + case 'sms': + return new SmsNotification(); + + case 'push': + return new PushNotification(); + } +} +``` + +#### Consideraciones + +En TypeScript, una función factoría suele ser suficiente. + +No es necesario crear una clase `NotificationFactory` si únicamente encapsula una selección sencilla. + +El uso de `switch` dentro de una factoría es legítimo. No contradice los principios SOLID cuando centraliza adecuadamente una decisión de construcción. + +Si las implementaciones deben incorporarse dinámicamente, puede ser preferible un registro explícito. + +#### Evita + +- Factorías que únicamente llaman a un constructor sin añadir valor. +- Jerarquías de factorías innecesarias. +- Registros dinámicos para un conjunto pequeño y estable de alternativas. +- Ocultar dependencias importantes detrás de mecanismos de creación difíciles de rastrear. + +--- + +### 2.3. Builder (Constructor por etapas) + +**Propósito:** construir objetos complejos mediante una secuencia controlada de operaciones. + +**Cuándo utilizarlo:** + +- Existen numerosos parámetros opcionales. +- La construcción requiere varias etapas. +- Deben validarse combinaciones de parámetros. +- La preparación de datos de prueba es compleja. + +#### Ejemplo + +```typescript id="z082g9" +interface UserOptions { + name: string; + email: string; + age?: number; +} + +class UserBuilder { + private name: string | undefined; + private email: string | undefined; + private age: number | undefined; + + withName(name: string): this { + this.name = name; + return this; + } + + withEmail(email: string): this { + this.email = email; + return this; + } + + withAge(age: number): this { + this.age = age; + return this; + } + + build(): UserOptions { + if (!this.name || !this.email) { + throw new Error('Faltan los datos obligatorios'); + } + + return { + name: this.name, + email: this.email, + ...(this.age === undefined ? {} : { age: this.age }), + }; + } +} +``` + +```typescript id="t31gdk" +const userOptions = new UserBuilder() + .withName('Alice') + .withEmail('alice@example.com') + .build(); +``` + +La validación impide generar un resultado sin los datos obligatorios. + +#### Alternativa más sencilla + +Para muchos casos, un objeto de opciones resulta suficiente: + +```typescript id="l2dwgv" +const userOptions: UserOptions = { + name: 'Alice', + email: 'alice@example.com', +}; +``` + +#### Criterios de utilización + +- Utiliza Builder cuando aporte claridad a un proceso de construcción complejo. +- Prefiere objetos de parámetros para construcciones sencillas. +- Evita utilizar aserciones de no nulidad (`!`) para ocultar datos obligatorios ausentes. +- Define correctamente las validaciones de construcción. +- No dupliques innecesariamente las reglas del constructor o del dominio. + +--- + +### 2.4. Prototype (Prototipo) + +**Propósito:** crear nuevos objetos a partir de otros existentes mediante clonación. + +**Cuándo utilizarlo:** + +- La construcción original es costosa. +- Se necesitan copias con pequeñas variaciones. +- El objeto dispone de una semántica de clonación definida. +- Se deben reproducir configuraciones complejas. + +#### Ejemplo + +```typescript id="sc008k" +interface DocumentMetadata { + tags: string[]; + author: string; +} + +class Document { + constructor( + readonly title: string, + readonly content: string, + readonly metadata: DocumentMetadata, + ) {} + + clone(): Document { + return new Document( + this.title, + this.content, + { + author: this.metadata.author, + tags: [...this.metadata.tags], + }, + ); + } +} +``` + +Este ejemplo copia explícitamente los datos mutables de `metadata`. + +#### Precauciones + +Una copia superficial puede conservar referencias compartidas a objetos anidados. + +```typescript id="kz4l4u" +// Copia superficial: los objetos anidados siguen compartidos. +const copy = { ...original }; +``` + +Antes de clonar, determina: + +- Qué elementos deben copiarse. +- Qué referencias pueden compartirse. +- Si existen recursos externos. +- Si deben preservarse las invariantes. +- Qué significa exactamente la identidad de la copia. + +`structuredClone()` puede ser útil para determinados datos compatibles, pero no sustituye universalmente a una operación de clonación de dominio. + +No introduzcas Prototype cuando construir directamente un objeto resulte más sencillo. + +--- + +## 3. Patrones estructurales (Structural Patterns) + +Los patrones estructurales organizan relaciones entre objetos y permiten combinar capacidades. + +### 3.1. Adapter (Adaptador) + +**Propósito:** permitir que componentes con interfaces incompatibles colaboren mediante un contrato común. + +**Cuándo utilizarlo:** + +- Integración con bibliotecas de terceros. +- Migración de código legado. +- Adaptación de APIs externas. +- Sustitución de proveedores. +- Separación entre dominio e infraestructura. + +#### Ejemplo + +Una biblioteca heredada proporciona este contrato: + +```typescript id="4bdqn8" +class OldPaymentApi { + makePayment(cents: number): boolean { + return legacyPaymentProvider.charge(cents); + } +} +``` + +La aplicación utiliza otro contrato: + +```typescript id="29npml" +interface PaymentGateway { + charge(amount: Money): ChargeResult; +} +``` + +El adaptador transforma ambas representaciones: + +```typescript id="nv310r" +class OldPaymentAdapter implements PaymentGateway { + constructor( + private readonly oldApi: OldPaymentApi, + ) {} + + charge(amount: Money): ChargeResult { + const cents = amount.toCents(); + const success = this.oldApi.makePayment(cents); + + return success + ? ChargeResult.success() + : ChargeResult.failed(); + } +} +``` + +#### Criterios de utilización + +- Mantén el contrato de dominio independiente del proveedor externo. +- Centraliza las conversiones necesarias. +- Comprueba diferencias de unidades, tipos y semántica. +- Traduce correctamente los errores y resultados. +- Evita filtrar detalles del proveedor hacia las capas consumidoras. + +En el ejemplo, debe verificarse que el importe esté expresado en una moneda y una unidad admitidas por el proveedor. + +**Un adaptador no debe limitarse a cambiar nombres si existen diferencias semánticas relevantes entre los contratos.** + +--- + +### 3.2. Bridge (Puente) + +**Propósito:** separar dos dimensiones de variación para que puedan evolucionar de manera independiente. + +**Cuándo utilizarlo:** + +- Existen dos jerarquías conceptuales que pueden combinarse. +- La herencia produciría numerosas subclases. +- Se necesita sustituir implementaciones independientemente de sus consumidores. + +#### Ejemplo + +Un sistema genera informes que pueden utilizar distintos formatos de salida. + +```typescript id="30ev2g" +interface ReportRenderer { + render(content: string): Promise; +} +``` + +```typescript id="0fxjr3" +class PdfRenderer implements ReportRenderer { + async render(content: string): Promise { + await generatePdf(content); + } +} + +class HtmlRenderer implements ReportRenderer { + async render(content: string): Promise { + await generateHtml(content); + } +} +``` + +```typescript id="12xjux" +class Report { + constructor( + private readonly renderer: ReportRenderer, + ) {} + + async generate(content: string): Promise { + await this.renderer.render(content); + } +} +``` + +El informe no depende directamente de un formato concreto. + +En un sistema con varios tipos de informes y varios formatos, esta separación puede evitar múltiples combinaciones mediante herencia. + +#### Criterios de utilización + +- Comprueba que existan realmente dos dimensiones independientes de variación. +- Evita utilizar Bridge para una única implementación estable. +- No crees jerarquías adicionales si una composición sencilla ya resuelve el problema. +- Favorece contratos pequeños y explícitos. + +--- + +### 3.3. Decorator (Decorador) + +**Propósito:** añadir comportamiento a un componente mediante composición, preservando su contrato. + +**Cuándo utilizarlo:** + +- Se necesitan capacidades combinables. +- Existen operaciones transversales. +- Se desea ampliar un comportamiento sin modificar su implementación. +- Debe evitarse una jerarquía extensa de subclases. + +#### Ejemplo + +```typescript id="6r5r7y" +interface Notifier { + send(message: string): Promise; +} + +class EmailNotifier implements Notifier { + async send(message: string): Promise { + await sendEmail(message); + } +} +``` + +Decorador que añade SMS: + +```typescript id="awibpt" +class SmsDecorator implements Notifier { + constructor( + private readonly wrapped: Notifier, + ) {} + + async send(message: string): Promise { + await this.wrapped.send(message); + await sendSms(message); + } +} +``` + +Decorador que añade Slack: + +```typescript id="dn84cw" +class SlackDecorator implements Notifier { + constructor( + private readonly wrapped: Notifier, + ) {} + + async send(message: string): Promise { + await this.wrapped.send(message); + await sendSlackMessage(message); + } +} +``` + +Composición: + +```typescript id="v5qc8v" +const notifier = new SlackDecorator( + new SmsDecorator( + new EmailNotifier(), + ), +); + +await notifier.send('Alerta del sistema'); +``` + +#### Consideraciones importantes + +En este ejemplo, las notificaciones se ejecutan secuencialmente. + +Si falla una operación, las siguientes pueden no ejecutarse. + +En un sistema real debe definirse: + +- Si se permite el fallo parcial. +- Si deben efectuarse reintentos. +- Si las notificaciones son independientes. +- Si debe conservarse el orden. +- Cómo se registran los errores. + +No utilices Decorator automáticamente cuando una simple función coordinadora resulte más clara. + +--- + +### 3.4. Proxy + +**Propósito:** controlar el acceso a un objeto manteniendo un contrato compatible. + +**Cuándo utilizarlo:** + +- Carga diferida (*lazy loading*). +- Control de acceso. +- Caché. +- Registro de operaciones. +- Acceso remoto. +- Control de recursos costosos. + +#### Ejemplo: carga diferida + +```typescript id="6w85mj" +interface Image { + display(): void; +} +``` + +```typescript id="737xpj" +class RealImage implements Image { + constructor(private readonly filename: string) { + this.loadFromDisk(); + } + + private loadFromDisk(): void { + // Cargar la imagen. + } + + display(): void { + // Mostrar la imagen. + } +} +``` + +```typescript id="9xypzj" +class ImageProxy implements Image { + private realImage: RealImage | null = null; + + constructor(private readonly filename: string) {} + + display(): void { + if (!this.realImage) { + this.realImage = new RealImage(this.filename); + } + + this.realImage.display(); + } +} +``` + +La imagen real únicamente se crea cuando se necesita. + +#### Criterios de utilización + +- Define claramente qué comportamiento controla el proxy. +- Mantén los contratos relevantes del objeto real. +- Considera los efectos de la caché y la concurrencia. +- No ocultes fallos del objeto real. +- Evita proxies que únicamente deleguen operaciones sin aportar comportamiento. + +En aplicaciones modernas, ciertas bibliotecas y frameworks ya proporcionan mecanismos de carga diferida, caché o control de acceso. Reutilízalos cuando resulten apropiados. + +--- + +### 3.5. Composite (Compuesto) + +**Propósito:** tratar objetos individuales y agrupaciones de objetos mediante un contrato común. + +**Cuándo utilizarlo:** + +- Estructuras jerárquicas. +- Árboles de componentes. +- Directorios y archivos. +- Grupos de elementos. +- Expresiones compuestas. + +#### Ejemplo + +```typescript id="b9ae4m" +interface PriceComponent { + getPrice(): number; +} +``` + +```typescript id="69wct2" +class Product implements PriceComponent { + constructor(private readonly price: number) {} + + getPrice(): number { + return this.price; + } +} +``` + +```typescript id="41ojbs" +class Box implements PriceComponent { + private readonly children: PriceComponent[] = []; + + add(component: PriceComponent): void { + this.children.push(component); + } + + getPrice(): number { + return this.children.reduce( + (total, child) => total + child.getPrice(), + 0, + ); + } +} +``` + +Uso: + +```typescript id="3v8qdz" +const smallBox = new Box(); + +smallBox.add(new Product(10)); +smallBox.add(new Product(20)); + +const bigBox = new Box(); + +bigBox.add(smallBox); +bigBox.add(new Product(50)); + +console.log(bigBox.getPrice()); // 80 +``` + +#### Consideraciones + +- Evita referencias circulares cuando la estructura sea un árbol. +- Define quién controla la modificación de los elementos. +- No expongas colecciones mutables que comprometan invariantes. +- Considera la profundidad y el coste de recorrer la jerarquía. +- Utiliza objetos de valor apropiados cuando existan importes monetarios reales. + +Composite es útil cuando la estructura jerárquica forma parte del problema. No es necesario para colecciones planas sencillas. + +--- + +## 4. Patrones de comportamiento (Behavioral Patterns) + +Los patrones de comportamiento organizan las interacciones, decisiones y responsabilidades entre componentes. + +### 4.1. Strategy (Estrategia) + +**Propósito:** definir diferentes implementaciones de un comportamiento mediante un contrato común. + +**Cuándo utilizarlo:** + +- Existen varios algoritmos alternativos. +- Las reglas pueden seleccionarse dinámicamente. +- Los algoritmos evolucionan independientemente. +- Se necesita probar cada política por separado. + +#### Ejemplo + +```typescript id="u10rrb" +interface PricingStrategy { + calculate(basePrice: number): number; +} +``` + +```typescript id="0ivv9t" +class RegularPricing implements PricingStrategy { + calculate(basePrice: number): number { + return basePrice; + } +} +``` + +```typescript id="fpmp3v" +class PremiumDiscount implements PricingStrategy { + calculate(basePrice: number): number { + return basePrice * 0.8; + } +} +``` + +```typescript id="cvl9d1" +class BlackFridayPricing implements PricingStrategy { + calculate(basePrice: number): number { + return basePrice * 0.5; + } +} +``` + +El carrito utiliza la estrategia seleccionada: + +```typescript id="q0uv1j" +class ShoppingCart { + constructor( + private readonly pricing: PricingStrategy, + ) {} + + calculateTotal(items: Item[]): number { + const basePrice = items.reduce( + (total, item) => total + item.price, + 0, + ); + + return this.pricing.calculate(basePrice); + } +} +``` + +Los ejemplos numéricos ilustran el patrón. Para importes monetarios reales deben utilizarse representaciones y reglas de redondeo adecuadas. + +#### Alternativa mediante funciones + +En TypeScript, una función puede ser suficiente: + +```typescript id="xkmb4o" +type PricingStrategy = (basePrice: number) => number; + +const regularPricing: PricingStrategy = (price) => price; +const premiumPricing: PricingStrategy = (price) => price * 0.8; +``` + +#### Criterios de utilización + +- Prefiere funciones cuando únicamente necesites intercambiar algoritmos sencillos. +- Utiliza objetos cuando las estrategias requieran dependencias, estado o comportamiento adicional. +- No crees una jerarquía de estrategias para uno o dos condicionales triviales. +- Mantén coherentes las precondiciones y resultados de las implementaciones. + +--- + +### 4.2. Observer (Observador) + +**Propósito:** notificar a varios consumidores cuando se produce un evento o cambio de estado. + +**Cuándo utilizarlo:** + +- Sistemas de eventos. +- Actualizaciones reactivas. +- Publicación y suscripción. +- Comunicación desacoplada entre componentes. + +#### Ejemplo + +```typescript id="nt8slp" +interface OrderPlacedEvent { + type: 'ORDER_PLACED'; + orderId: string; +} + +type OrderObserver = (event: OrderPlacedEvent) => void; +``` + +```typescript id="q2mtwi" +class OrderEventEmitter { + private readonly observers = new Set(); + + subscribe(observer: OrderObserver): () => void { + this.observers.add(observer); + + return () => { + this.observers.delete(observer); + }; + } + + notify(event: OrderPlacedEvent): void { + for (const observer of this.observers) { + observer(event); + } + } +} +``` + +Uso: + +```typescript id="e0uhri" +const emitter = new OrderEventEmitter(); + +const unsubscribe = emitter.subscribe((event) => { + console.log(`Pedido realizado: ${event.orderId}`); +}); + +emitter.notify({ + type: 'ORDER_PLACED', + orderId: 'ORDER-001', +}); + +unsubscribe(); +``` + +El ejemplo utiliza notificaciones síncronas. Si un observador lanza una excepción, puede interrumpir la entrega a los siguientes. + +#### Consideraciones de diseño + +- Define cuándo se emiten los eventos. +- Evita emitir eventos de negocio confirmados antes de completar la operación correspondiente. +- Establece cómo se gestionan los errores de los observadores. +- Permite cancelar suscripciones para evitar retención innecesaria de recursos. +- Diferencia los eventos internos de los eventos distribuidos. +- Determina si las notificaciones deben ser síncronas o asíncronas. +- Considera idempotencia, reintentos y entrega duplicada cuando exista mensajería distribuida. + +Observer no sustituye automáticamente a una arquitectura de eventos. + +Para comunicación entre procesos o servicios pueden ser necesarios mecanismos de persistencia de eventos, colas o patrones como *Transactional Outbox*. + +#### Evita + +- Utilizar eventos para ocultar dependencias que deberían ser explícitas. +- Crear flujos difíciles de rastrear. +- Encadenar numerosos observadores con efectos secundarios implícitos. +- Introducir un bus de eventos global para operaciones sencillas. + +--- + +### 4.3. Template Method (Método plantilla) + +**Propósito:** definir la estructura general de un algoritmo y permitir que determinadas etapas varíen mediante especialización. + +**Cuándo utilizarlo:** + +- Existe un proceso estable con pasos claramente definidos. +- Varias implementaciones comparten la misma secuencia. +- Las diferencias pueden representarse mediante operaciones especializadas. + +#### Ejemplo + +```typescript id="0qnk1j" +abstract class DataExporter { + export(data: ExportRecord[]): void { + this.validate(data); + + const content = this.format(data); + + this.write(content); + } + + private validate(data: ExportRecord[]): void { + if (!Array.isArray(data)) { + throw new Error('Datos de exportación no válidos'); + } + } + + protected abstract format(data: ExportRecord[]): string; + + protected abstract write(content: string): void; +} +``` + +```typescript id="jcwu1g" +class CsvExporter extends DataExporter { + protected format(data: ExportRecord[]): string { + return data.map((record) => record.toCsv()).join('\n'); + } + + protected write(content: string): void { + writeFile('export.csv', content); + } +} +``` + +```typescript id="jzeyc0" +class JsonExporter extends DataExporter { + protected format(data: ExportRecord[]): string { + return JSON.stringify(data); + } + + protected write(content: string): void { + writeFile('export.json', content); + } +} +``` + +#### Alternativa mediante composición + +Cuando las variaciones pueden expresarse mediante colaboradores independientes: + +```typescript id="x7v9ni" +interface DataFormatter { + format(data: ExportRecord[]): string; +} + +interface DataWriter { + write(content: string): void; +} +``` + +```typescript id="9rqgh3" +class DataExporter { + constructor( + private readonly formatter: DataFormatter, + private readonly writer: DataWriter, + ) {} + + export(data: ExportRecord[]): void { + const content = this.formatter.format(data); + + this.writer.write(content); + } +} +``` + +La composición permite sustituir el formateador y el mecanismo de escritura por separado. + +#### Criterios de utilización + +- Utiliza Template Method cuando la herencia represente correctamente un algoritmo estable. +- Prefiere composición cuando las etapas deban combinarse independientemente. +- Evita clases base con numerosos métodos protegidos. +- No introduzcas herencia únicamente para compartir unas pocas instrucciones. +- Comprueba que las especializaciones respetan los contratos de la clase base. + +--- + +### 4.4. Command (Comando) + +**Propósito:** encapsular una solicitud u operación como una unidad independiente. + +**Cuándo utilizarlo:** + +- Historial de operaciones. +- Deshacer y rehacer. +- Colas de trabajo. +- Ejecución diferida. +- Registro de acciones. +- Operaciones que requieren auditoría. + +#### Ejemplo + +```typescript id="ld2of3" +interface Command { + execute(): void; + undo(): void; +} +``` + +```typescript id="57hwjz" +class AddItemCommand implements Command { + private executed = false; + + constructor( + private readonly cart: Cart, + private readonly item: Item, + ) {} + + execute(): void { + this.cart.add(this.item); + this.executed = true; + } + + undo(): void { + if (!this.executed) { + return; + } + + this.cart.remove(this.item); + this.executed = false; + } +} +``` + +```typescript id="77kn41" +class CommandHistory { + private readonly history: Command[] = []; + + execute(command: Command): void { + command.execute(); + this.history.push(command); + } + + undo(): void { + const command = this.history.at(-1); + + if (!command) { + return; + } + + command.undo(); + this.history.pop(); + } +} +``` + +#### Precauciones importantes + +El ejemplo presupone que: + +- La operación `add` se completa correctamente o lanza una excepción. +- El elemento puede identificarse y eliminarse de forma inequívoca. +- No se modifican los mismos datos concurrentemente. +- Cada comando se ejecuta y deshace según el ciclo previsto. + +En aplicaciones reales, estas garantías deben analizarse expresamente. + +**Deshacer una operación no equivale necesariamente a ejecutar la operación contraria.** + +Por ejemplo: + +- Deshacer un envío de correo electrónico puede ser imposible. +- Revertir un pago requiere una nueva operación de negocio. +- Eliminar un elemento puede requerir conservar su posición original. +- Una operación distribuida puede exigir acciones compensatorias. + +### Criterios de utilización + +- Encapsula comandos cuando necesites representar operaciones independientes. +- Utiliza funciones cuando una simple llamada sea suficiente. +- Define los errores y las garantías de ejecución. +- Distingue comandos reversibles de acciones compensables. +- Evita implementar sistemas de historial o colas sin una necesidad identificada. + +--- + +## 5. Reconocimiento de patrones + +### Análisis mediante cuatro dimensiones + +Cuando analices código nuevo o bibliotecas desconocidas, considera: + +1. **Problema que resuelve:** creación, estructura o comportamiento. +2. **Ámbito:** objeto, clase, módulo o sistema. +3. **Momento de aplicación:** configuración, inicialización, compilación o ejecución. +4. **Acoplamiento:** qué dependencias introduce y cuáles elimina. + +Estas dimensiones ayudan a reconocer patrones incluso cuando su implementación no coincide exactamente con la descripción clásica. + +### Preguntas adicionales + +- ¿Qué responsabilidades separa? +- ¿Qué contrato proporciona? +- ¿Qué variaciones permite? +- ¿Qué coste introduce? +- ¿Qué alternativa sería más sencilla? +- ¿Qué ocurriría si elimináramos el patrón? + +**Reconocer un patrón no significa que sea necesario introducirlo.** + +--- + +## 6. Patrones en TypeScript y arquitecturas modernas + +Los lenguajes y frameworks modernos proporcionan mecanismos que pueden sustituir implementaciones clásicas basadas en clases. + +### 6.1. Funciones como estrategias + +Las funciones de primera clase permiten intercambiar algoritmos sin crear jerarquías. + +```typescript id="sqn7la" +type DiscountCalculator = (price: number) => number; + +const noDiscount: DiscountCalculator = (price) => price; + +const premiumDiscount: DiscountCalculator = (price) => price * 0.8; + +function calculatePrice( + price: number, + discount: DiscountCalculator, +): number { + return discount(price); +} +``` + +Este enfoque puede resultar suficiente cuando las estrategias carecen de estado o dependencias complejas. + +### 6.2. Funciones factoría + +Una función puede encapsular la creación de objetos sin necesidad de una clase específica. + +```typescript id="8fujam" +function createLogger(environment: 'development' | 'production'): Logger { + if (environment === 'development') { + return new ConsoleLogger(); + } + + return new StructuredLogger(); +} +``` + +### 6.3. Composición explícita + +La composición puede realizarse directamente durante la inicialización de una aplicación. + +```typescript id="w528ki" +const logger = new ConsoleLogger(); +const repository = new PostgresOrderRepository(); + +const orderService = new OrderService( + repository, + logger, +); +``` + +No es obligatorio utilizar un contenedor de dependencias cuando la composición manual sea suficiente. + +### 6.4. Patrones propios de frameworks + +Cuando el proyecto utilice React, Express, NestJS u otros frameworks: + +- Respeta los mecanismos idiomáticos del framework. +- No reproduzcas patrones clásicos que el framework ya resuelve. +- Evita introducir capas adicionales sin responsabilidades concretas. +- Mantén la separación entre lógica de negocio y detalles técnicos cuando corresponda. +- No fuerces el uso de clases en escenarios donde funciones, componentes o hooks sean más apropiados. + +Por ejemplo, en React puede resultar más adecuado utilizar composición de componentes y hooks que aplicar literalmente jerarquías de herencia, Decorator u Observer. + +--- + +## 7. Antipatrones que deben evitarse + +| Antipatrón | Problema | Enfoque recomendado | +|---|---|---| +| **God Object (Objeto todopoderoso)** | Una clase concentra responsabilidades independientes. | Separar responsabilidades coherentes. | +| **Spaghetti Code (Código espagueti)** | Flujo y dependencias difíciles de comprender. | Simplificar estructuras y reorganizar responsabilidades. | +| **Golden Hammer (Martillo de oro)** | Utilizar siempre el mismo patrón. | Elegir soluciones según el problema concreto. | +| **Premature Optimization (Optimización prematura)** | Optimizar sin necesidad ni evidencia. | Medir y priorizar problemas reales. | +| **Copy-Paste Programming (Programación por copia y pega)** | Duplicación de conocimiento o reglas. | Identificar abstracciones justificadas. | +| **Overengineering (Sobreingeniería)** | Introducir complejidad para requisitos hipotéticos. | Aplicar YAGNI y KISS. | +| **Speculative Generality (Generalización especulativa)** | Crear extensibilidad sin necesidad real. | Diseñar para requisitos existentes o definidos. | +| **Service Locator indiscriminado** | Ocultar dependencias mediante búsquedas globales. | Preferir dependencias explícitas cuando corresponda. | +| **Abstraction Explosion (Explosión de abstracciones)** | Demasiadas interfaces, clases y capas triviales. | Eliminar intermediarios sin responsabilidad real. | + +### La regla de tres + +La regla de tres puede ayudar a detectar oportunidades de reutilización, pero no debe interpretarse como un requisito obligatorio. + +- No extraigas automáticamente una abstracción por encontrar tres fragmentos parecidos. +- No esperes a una tercera duplicación cuando ya exista una regla de negocio común que deba mantenerse coherente. +- Comprueba que el código duplicado represente el mismo conocimiento. +- Prefiere una pequeña duplicación comprensible antes que una abstracción incorrecta. + +--- + +## 8. Cómo decidir si utilizar un patrón + +Antes de introducir un patrón nuevo: + +1. Identifica el problema concreto. +2. Comprueba las soluciones existentes en el proyecto. +3. Define qué responsabilidad debe quedar aislada. +4. Evalúa una solución directa sin patrón. +5. Compara complejidad, acoplamiento y mantenibilidad. +6. Introduce el patrón únicamente si aporta una ventaja clara. +7. Verifica que no modifica involuntariamente el comportamiento existente. + +### Señales de que un patrón puede ser útil + +- Existen variaciones de comportamiento que evolucionan independientemente. +- Se repiten decisiones de creación complejas. +- Las dependencias impiden probar componentes. +- Una integración externa contamina los contratos de dominio. +- Una estructura jerárquica necesita operaciones uniformes. +- Existen responsabilidades transversales combinables. + +### Señales de que puede ser innecesario + +- Solo existe una implementación sencilla. +- La variación es hipotética. +- El patrón añade numerosos archivos sin simplificar las operaciones. +- Los consumidores necesitan conocer demasiados detalles del patrón. +- Una función o un objeto literal resuelve el problema de forma clara. +- El código original ya es comprensible y fácil de modificar. + +--- + +## 9. Lista de comprobación de patrones de diseño + +### Selección + +- [ ] ¿Existe un problema real que justifique el patrón? +- [ ] ¿Conozco una alternativa más sencilla? +- [ ] ¿El patrón representa correctamente el dominio? +- [ ] ¿Reduce el acoplamiento o mejora la cohesión? +- [ ] ¿La solución será comprensible para otros desarrolladores? + +### Implementación + +- [ ] ¿Los contratos están correctamente definidos? +- [ ] ¿Las implementaciones respetan los comportamientos esperados? +- [ ] ¿He evitado clases e interfaces innecesarias? +- [ ] ¿Las dependencias son explícitas cuando corresponde? +- [ ] ¿Se respetan las convenciones del proyecto? + +### Mantenimiento + +- [ ] ¿El patrón facilita incorporar cambios reales? +- [ ] ¿Se puede probar sin infraestructura innecesaria? +- [ ] ¿Introduce complejidad operativa adicional? +- [ ] ¿La estructura sigue siendo adecuada si se incorporan nuevos requisitos? +- [ ] ¿He evitado refactorizaciones ajenas al alcance? + +### Verificación + +- [ ] ¿Se conserva el comportamiento observable? +- [ ] ¿Los contratos existentes siguen siendo compatibles? +- [ ] ¿Se han ejecutado las pruebas pertinentes? +- [ ] ¿Los errores y estados excepcionales están correctamente tratados? +- [ ] ¿La nueva abstracción aporta una mejora verificable? + +--- + +## 10. Referencia rápida + +| Patrón | Problema principal | Precaución | +|---|---|---| +| **Singleton** | Instancia única controlada. | Evitar estado global oculto. | +| **Factory** | Construcción variable o compleja. | No crear factorías triviales. | +| **Builder** | Construcción por etapas. | No sustituir innecesariamente objetos de opciones. | +| **Prototype** | Clonación de objetos. | Comprobar referencias mutables. | +| **Adapter** | Interfaces incompatibles. | Preservar la semántica del contrato. | +| **Bridge** | Variaciones independientes. | Evitar jerarquías innecesarias. | +| **Decorator** | Comportamientos combinables. | Controlar orden y efectos secundarios. | +| **Proxy** | Control de acceso o carga. | Mantener transparencia contractual. | +| **Composite** | Estructuras jerárquicas. | Proteger invariantes y evitar ciclos. | +| **Strategy** | Algoritmos intercambiables. | Considerar funciones sencillas. | +| **Observer** | Notificaciones a consumidores. | Gestionar errores y suscripciones. | +| **Template Method** | Algoritmos con etapas variables. | Considerar composición como alternativa. | +| **Command** | Operaciones como unidades independientes. | Definir correctamente reversión y ejecución. | + +--- + +## Principio final + +> "Los patrones de diseño son herramientas para resolver problemas recurrentes, no requisitos que deban imponerse a todas las implementaciones." + +Utiliza patrones cuando permitan expresar responsabilidades con mayor claridad, controlar dependencias y facilitar la evolución del software. + +No conviertas la identificación de patrones en una obligación de aplicarlos. + +**La mejor solución es la más sencilla que satisface correctamente los requisitos y mantiene el software comprensible, verificable y preparado para evolucionar.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/principios-solid.md b/.agents/skills/rdx-solid/referencias/principios-solid.md new file mode 100644 index 0000000..a5124e4 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/principios-solid.md @@ -0,0 +1,804 @@ +# Principios SOLID + +## Introducción + +Los principios SOLID constituyen un conjunto de criterios de diseño orientados a desarrollar software flexible, mantenible, comprensible y verificable. + +Su aplicación adecuada permite: + +- Reducir el acoplamiento innecesario. +- Mejorar la cohesión de clases y módulos. +- Facilitar la evolución de funcionalidades. +- Proteger contratos y comportamientos existentes. +- Simplificar las pruebas y el mantenimiento. +- Controlar el impacto de los cambios. + +Estos principios deben aplicarse con criterio profesional y teniendo en cuenta las necesidades reales del sistema. + +**SOLID no exige utilizar siempre interfaces, herencia, patrones de diseño ni arquitecturas complejas.** + +El objetivo es mejorar la estructura del software, no maximizar el número de abstracciones. + +Este documento desarrolla los criterios establecidos en el `SKILL.md` principal. + +--- + +## 1. S — Principio de responsabilidad única (Single Responsibility Principle, SRP) + +> "Una clase debería tener una, y solo una, razón para cambiar." + +### 1.1. Qué problema resuelve + +SRP ayuda a evitar clases o módulos que acumulan responsabilidades independientes. + +Estas estructuras suelen presentar: + +- Dificultad para comprender su comportamiento. +- Cambios frecuentes por motivos diferentes. +- Pruebas complejas. +- Dependencias excesivas. +- Mayor riesgo de regresiones. +- Acoplamiento entre funcionalidades que deberían evolucionar por separado. + +### 1.2. Cómo aplicarlo + +Cada clase o módulo debe representar una responsabilidad coherente. + +Una responsabilidad no equivale necesariamente a un único método, operación o dato. + +Una clase puede realizar varias operaciones relacionadas y seguir cumpliendo SRP. + +**Ejemplo: responsabilidades mezcladas** + +```typescript id="lzj3ab" +// MAL: Mezcla reglas de negocio, persistencia y generación de documentos. +class Order { + calculateTotal(): Money { + // Calcular el importe del pedido. + } + + async saveToDatabase(): Promise { + // Persistir el pedido. + } + + generateInvoice(): Invoice { + // Generar la factura. + } +} +``` + +En este caso, la clase puede cambiar por motivos independientes: + +- Modificación de reglas de cálculo. +- Cambios en la persistencia. +- Cambios en la generación de facturas. + +**Alternativa: responsabilidades diferenciadas** + +```typescript id="u0b51p" +class Order { + private readonly items: OrderItem[] = []; + + addItem(item: OrderItem): void { + this.items.push(item); + } + + calculateTotal(): Money { + return calculateOrderTotal(this.items); + } +} +``` + +```typescript id="9drm12" +class OrderRepository { + async save(order: Order): Promise { + // Persistir el pedido. + } +} +``` + +```typescript id="e1iz3a" +class InvoiceGenerator { + generate(order: Order): Invoice { + // Generar una factura a partir del pedido. + } +} +``` + +La entidad `Order` mantiene las operaciones relacionadas con sus reglas de negocio, mientras que la persistencia y la generación de documentos pertenecen a responsabilidades diferenciadas. + +Los ejemplos son ilustrativos y presuponen los tipos y colaboradores necesarios. + +### 1.3. Preguntas para detectar problemas + +- ¿Tiene esta clase varios motivos independientes para cambiar? +- ¿Sus operaciones responden a una responsabilidad coherente? +- ¿Distintos requisitos obligan a modificar partes no relacionadas? +- ¿La clase depende de demasiados componentes externos? +- ¿Sería más sencillo probar y mantener determinadas responsabilidades por separado? + +### 1.4. Evitar una interpretación excesivamente estricta + +No dividas una clase únicamente porque su descripción incluya la palabra «y». + +Por ejemplo, una entidad `Order` puede: + +- Añadir artículos. +- Eliminar artículos. +- Calcular importes. +- Comprobar restricciones. +- Cambiar de estado. + +Todas estas operaciones pueden pertenecer a una única responsabilidad: gestionar el comportamiento del pedido. + +**SRP busca separar motivos de cambio independientes, no reducir todas las clases a una única operación.** + +--- + +## 2. O — Principio abierto/cerrado (Open/Closed Principle, OCP) + +> "Las entidades de software deberían estar abiertas a la extensión, pero cerradas a la modificación." + +### 2.1. Qué problema resuelve + +OCP busca reducir la necesidad de modificar código estable cada vez que se incorpora una variante de comportamiento. + +Esto resulta especialmente útil cuando: + +- Existen múltiples implementaciones de una capacidad. +- Se incorporan nuevas variantes con frecuencia. +- Los componentes tienen contratos estables. +- Modificar lógica existente supone un riesgo elevado. + +### 2.2. Cómo aplicarlo + +Cuando exista una necesidad real de extensión, diseña contratos que permitan incorporar nuevos comportamientos sin modificar innecesariamente los consumidores. + +**Ejemplo: cálculo de gastos de envío** + +```typescript id="e33kiw" +// Implementación sencilla basada en condicionales. +type ShippingType = 'standard' | 'express'; + +class ShippingCalculator { + calculate(type: ShippingType, orderValue: number): number { + if (type === 'standard') { + return orderValue < 50 ? 5 : 0; + } + + return 15; + } +} +``` + +Esta implementación no es necesariamente incorrecta. + +Si los tipos de envío son pocos y estables, puede ser la solución más sencilla y mantenible. + +Sin embargo, cuando las políticas de envío evolucionan independientemente, puede resultar conveniente introducir abstracciones. + +### 2.3. Alternativa extensible + +```typescript id="0kb5qt" +interface ShippingMethod { + calculateCost(orderValue: number): number; +} +``` + +```typescript id="mky8hb" +class StandardShipping implements ShippingMethod { + calculateCost(orderValue: number): number { + return orderValue < 50 ? 5 : 0; + } +} +``` + +```typescript id="p9o7s1" +class ExpressShipping implements ShippingMethod { + calculateCost(_orderValue: number): number { + return 15; + } +} +``` + +Para incorporar un nuevo método: + +```typescript id="bw5t7r" +class SameDayShipping implements ShippingMethod { + calculateCost(_orderValue: number): number { + return 25; + } +} +``` + +Un consumidor puede utilizar el contrato común: + +```typescript id="d8t4vk" +function calculateShipping( + method: ShippingMethod, + orderValue: number, +): number { + return method.calculateCost(orderValue); +} +``` + +Este diseño permite incorporar nuevas políticas sin modificar el cálculo del consumidor. + +No obstante, el mecanismo que selecciona o registra las implementaciones puede necesitar cambios según la arquitectura utilizada. + +### 2.4. Aplicación pragmática + +- Introduce puntos de extensión cuando exista una necesidad identificada. +- Mantén estables los contratos que tengan consumidores. +- Favorece composición y polimorfismo cuando reduzcan el acoplamiento. +- Evita crear interfaces para comportamientos que probablemente no variarán. +- No introduzcas extensibilidad hipotética. +- Acepta modificaciones de código existente cuando sean la solución más clara. + +### 2.5. OCP a nivel arquitectónico + +A nivel de arquitectura, OCP favorece la existencia de módulos con responsabilidades claras y contratos estables. + +Esto permite incorporar funcionalidades nuevas con un impacto controlado. + +Sin embargo, **no significa que toda funcionalidad nueva deba implementarse exclusivamente añadiendo archivos o clases**. + +Modificar código existente es legítimo cuando cambian sus requisitos o responsabilidades. + +El objetivo es minimizar modificaciones innecesarias en componentes estables, no prohibirlas. + +--- + +## 3. L — Principio de sustitución de Liskov (Liskov Substitution Principle, LSP) + +> "Los subtipos deben poder sustituir a sus tipos base sin alterar la corrección del programa." + +### 3.1. Qué problema resuelve + +LSP evita jerarquías o implementaciones que incumplen las expectativas establecidas por un contrato común. + +Cuando se vulnera este principio, suelen aparecer: + +- Comprobaciones del tipo concreto en los consumidores. +- Condicionales especiales para determinadas implementaciones. +- Errores inesperados al sustituir componentes. +- Contratos inconsistentes. +- Dependencias del comportamiento interno de cada implementación. + +### 3.2. El contrato es más que una firma + +Dos implementaciones no son necesariamente sustituibles por tener los mismos métodos. + +La sustitución requiere respetar el comportamiento esperado, incluyendo: + +- Precondiciones. +- Postcondiciones. +- Invariantes. +- Semántica de los resultados. +- Comportamiento ante errores. +- Garantías relevantes para los consumidores. + +### 3.3. Ejemplo de incumplimiento + +Supongamos que el contrato establece que un descuento nunca puede ser negativo. + +```typescript id="j3zfk5" +interface DiscountPolicy { + getDiscount(orderValue: number): number; +} +``` + +Una implementación correcta: + +```typescript id="tr7txy" +class StandardDiscount implements DiscountPolicy { + getDiscount(_orderValue: number): number { + return 5; + } +} +``` + +Una implementación incorrecta: + +```typescript id="q0laxd" +class InvalidDiscount implements DiscountPolicy { + getDiscount(_orderValue: number): number { + return -5; + } +} +``` + +Aunque ambas cumplen la interfaz TypeScript, la segunda incumple el contrato semántico si este exige descuentos no negativos. + +### 3.4. Protección del contrato + +El contrato puede expresarse mediante documentación, tipos, validaciones y pruebas. + +```typescript id="e1iu3o" +class FixedDiscount implements DiscountPolicy { + constructor(private readonly amount: number) { + if (!Number.isFinite(amount) || amount < 0) { + throw new InvalidDiscountError(); + } + } + + getDiscount(_orderValue: number): number { + return this.amount; + } +} +``` + +La validación impide crear una política de descuento fijo con un importe negativo. + +No obstante, validar únicamente esta clase no garantiza que todas las implementaciones de `DiscountPolicy` cumplan el contrato. + +Para ello pueden utilizarse pruebas de contrato compartidas y validaciones en los límites apropiados. + +### 3.5. Reglas de sustitución + +Una implementación sustitutiva debe: + +- Respetar las invariantes del contrato. +- No exigir precondiciones más estrictas que las comprometidas por el tipo base. +- No proporcionar postcondiciones más débiles. +- Mantener el significado de los resultados. +- Respetar las garantías de error relevantes. +- No introducir efectos secundarios incompatibles con el contrato. + +### 3.6. Ejemplo con repositorios + +```typescript id="60hovx" +interface UserRepository { + findById(id: UserId): Promise; + + save(user: User): Promise; +} +``` + +Las implementaciones: + +```typescript id="kp45w8" +class InMemoryUserRepository implements UserRepository { + // Implementación en memoria. +} +``` + +```typescript id="q2v45i" +class PostgresUserRepository implements UserRepository { + // Implementación sobre PostgreSQL. +} +``` + +Pueden sustituirse cuando respetan el mismo contrato funcional. + +Esto incluye: + +- Significado de `null` cuando no existe el usuario. +- Garantías de persistencia. +- Restricciones de integridad. +- Tratamiento de errores. +- Comportamientos relevantes para los consumidores. + +**Compartir una interfaz no garantiza automáticamente la sustituibilidad.** + +--- + +## 4. I — Principio de segregación de interfaces (Interface Segregation Principle, ISP) + +> "Los clientes no deberían verse obligados a depender de métodos que no utilizan." + +### 4.1. Qué problema resuelve + +ISP evita interfaces excesivamente amplias que obligan a los consumidores o implementaciones a conocer capacidades innecesarias. + +Los síntomas habituales incluyen: + +- Interfaces con numerosas operaciones independientes. +- Implementaciones parciales. +- Métodos vacíos. +- Excepciones de funcionalidad no soportada. +- Dependencias innecesarias. +- Cambios en interfaces que afectan a consumidores no relacionados. + +### 4.2. Ejemplo problemático + +```typescript id="d9qvm1" +interface WarehouseDevice { + printLabel(orderId: string): void; + scanBarcode(): string; + packageItem(orderId: string): void; +} +``` + +Una impresora sencilla no debería verse obligada a implementar operaciones que no admite. + +```typescript id="z83zrv" +class BasicPrinter implements WarehouseDevice { + printLabel(_orderId: string): void { + // Imprimir etiqueta. + } + + scanBarcode(): string { + throw new Error('Operación no soportada'); + } + + packageItem(_orderId: string): void { + throw new Error('Operación no soportada'); + } +} +``` + +### 4.3. Alternativa con interfaces cohesionadas + +```typescript id="80gu9m" +interface LabelPrinter { + printLabel(orderId: string): void; +} +``` + +```typescript id="e4lqq1" +interface BarcodeScanner { + scanBarcode(): string; +} +``` + +```typescript id="a113zj" +interface ItemPackager { + packageItem(orderId: string): void; +} +``` + +Una impresora implementa únicamente su capacidad real: + +```typescript id="b9ksk8" +class BasicPrinter implements LabelPrinter { + printLabel(_orderId: string): void { + // Imprimir etiqueta. + } +} +``` + +Los consumidores pueden depender de las capacidades que necesitan. + +### 4.4. Aplicación pragmática + +- Diseña interfaces alrededor de necesidades reales de los consumidores. +- Agrupa operaciones que representen una responsabilidad coherente. +- Divide interfaces cuando obliguen a depender de capacidades no utilizadas. +- Evita crear una interfaz independiente para cada método sin una razón clara. +- No fragmentes contratos cohesionados únicamente para reducir su tamaño. + +### 4.5. Señales de revisión + +Revisa una interfaz cuando: + +- Varias implementaciones contienen métodos vacíos. +- Algunas operaciones lanzan excepciones de no implementación de forma sistemática. +- Los consumidores utilizan subconjuntos completamente independientes. +- Los cambios afectan a componentes que no necesitan la funcionalidad modificada. + +Sin embargo, un método opcional o una operación no soportada no demuestra por sí solo una vulneración de ISP. + +Evalúa el contrato y el contexto antes de modificarlo. + +**ISP trata de reducir dependencias innecesarias, no de maximizar el número de interfaces.** + +--- + +## 5. D — Principio de inversión de dependencias (Dependency Inversion Principle, DIP) + +> "Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones." + +### 5.1. Qué problema resuelve + +DIP busca evitar que las reglas de negocio queden acopladas innecesariamente a detalles técnicos. + +Ejemplos: + +- Bases de datos concretas. +- Proveedores de correo electrónico. +- APIs externas. +- Sistemas de almacenamiento. +- Frameworks. +- Implementaciones de infraestructura. + +Este acoplamiento puede dificultar las pruebas, la evolución o la sustitución de componentes. + +### 5.2. Ejemplo de dependencia concreta + +```typescript id="r5k4a2" +class OrderService { + private readonly emailService = new SendGridEmailService(); + + async confirmOrder(email: string): Promise { + await this.emailService.send(email, 'Pedido confirmado'); + } +} +``` + +La clase conoce directamente al proveedor de correo electrónico. + +Si cambia el proveedor, puede ser necesario modificar el servicio de aplicación. + +### 5.3. Dependencia de una abstracción + +Define un contrato que represente la capacidad necesaria. + +```typescript id="k8p2ag" +interface EmailService { + send(to: string, message: string): Promise; +} +``` + +El servicio depende del contrato: + +```typescript id="5g7pvd" +class OrderService { + constructor( + private readonly emailService: EmailService, + ) {} + + async confirmOrder(email: string): Promise { + await this.emailService.send(email, 'Pedido confirmado'); + } +} +``` + +Las implementaciones concretas pueden variar: + +```typescript id="gcl3yt" +const sendGridOrderService = new OrderService( + new SendGridEmailService(), +); +``` + +```typescript id="k85qhq" +const sesOrderService = new OrderService( + new SesEmailService(), +); +``` + +En una prueba puede utilizarse una implementación controlada: + +```typescript id="f4yb6z" +const testOrderService = new OrderService( + new FakeEmailService(), +); +``` + +El servicio de aplicación no necesita conocer los detalles internos de cada proveedor. + +### 5.4. Inversión de dependencias e inyección de dependencias + +Son conceptos relacionados, pero diferentes. + +**Inversión de dependencias (DIP):** + +Principio de diseño que establece cómo deben organizarse las dependencias entre políticas y detalles. + +**Inyección de dependencias (DI):** + +Técnica mediante la cual un componente recibe sus colaboradores desde el exterior. + +Puede realizarse mediante: + +- Constructores. +- Parámetros. +- Factorías. +- Composición explícita. +- Contenedores de dependencias. + +**Aplicar DIP no exige utilizar un contenedor de inyección de dependencias.** + +### 5.5. Evitar abstracciones innecesarias + +No es obligatorio crear una interfaz para cada clase. + +Una dependencia concreta puede resultar apropiada cuando: + +- El componente es sencillo y estable. +- No existe una necesidad real de sustitución. +- No introduce acoplamiento arquitectónico problemático. +- La abstracción adicional no aporta claridad ni capacidad de evolución. +- Se trata de un objeto de valor, una utilidad pura u otro componente concreto cuyo contrato es suficientemente estable. + +La dependencia de abstracciones debe justificarse por su utilidad, no por el cumplimiento mecánico de DIP. + +### 5.6. Regla de dependencias arquitectónicas + +En arquitecturas limpias, hexagonales o equivalentes, las dependencias del código fuente deben orientarse hacia las políticas de alto nivel. + +Representación simplificada: + +```text id="y99n58" +Infraestructura → Aplicación → Dominio + exterior medio interior +``` + +Esta representación expresa la dirección deseada de las dependencias en una arquitectura organizada de esa forma. + +En la práctica, la infraestructura puede implementar contratos definidos en la aplicación o el dominio. + +Lo importante es que las reglas de negocio no dependan innecesariamente de los detalles de infraestructura. + +### 5.7. Aplicación pragmática + +- Mantén independientes las reglas de negocio respecto a detalles técnicos cuando resulte necesario. +- Define contratos en los límites donde aporten desacoplamiento. +- Utiliza inyección de dependencias cuando facilite la composición o las pruebas. +- No introduzcas interfaces y adaptadores para todas las operaciones. +- Evita contenedores complejos en aplicaciones que puedan resolverse mediante composición directa. +- Respeta la arquitectura existente salvo que exista una razón técnica o funcional para modificarla. + +--- + +## 6. Aplicación de SOLID a nivel arquitectónico + +Los principios SOLID pueden orientar el diseño de módulos, subsistemas y límites arquitectónicos. + +No obstante, su aplicación a nivel de arquitectura requiere interpretar correctamente cada principio. + +| Principio | Aplicación arquitectónica | +|---|---| +| **SRP** | Módulos y subsistemas con responsabilidades cohesionadas y motivos de cambio relacionados. | +| **OCP** | Contratos y puntos de extensión que permiten incorporar capacidades sin modificar innecesariamente componentes estables. | +| **LSP** | Implementaciones sustituibles que respetan los contratos funcionales y no funcionales relevantes. | +| **ISP** | Interfaces y contratos entre módulos adaptados a las necesidades reales de sus consumidores. | +| **DIP** | Separación entre políticas de negocio y detalles técnicos mediante dependencias orientadas adecuadamente. | + +### 6.1. SRP en módulos y subsistemas + +Un módulo puede contener numerosas clases y operaciones siempre que representen una responsabilidad coherente. + +No debe interpretarse que cada módulo o contexto delimitado (*bounded context*) tenga que realizar una única operación. + +### 6.2. OCP en arquitectura + +Favorece módulos estables que puedan ampliarse mediante nuevos componentes cuando exista una necesidad real de extensión. + +No significa que las funcionalidades nuevas deban implementarse exclusivamente añadiendo módulos. + +### 6.3. LSP en contratos distribuidos + +En sistemas distribuidos, compartir un contrato técnico no garantiza sustituibilidad. + +Deben considerarse también: + +- Semántica de las operaciones. +- Garantías de consistencia. +- Tratamiento de errores. +- Idempotencia cuando corresponda. +- Compatibilidad de versiones. +- Restricciones operativas relevantes. + +Dos microservicios que exponen la misma API no son necesariamente intercambiables. + +### 6.4. ISP entre módulos + +Los contratos deben exponer las capacidades necesarias sin obligar a los consumidores a depender de funcionalidades ajenas. + +Esto no exige crear un servicio o API independiente para cada operación. + +### 6.5. DIP en arquitectura + +Las reglas de negocio deben mantenerse desacopladas de detalles de infraestructura cuando esa separación facilite la evolución del sistema. + +La arquitectura concreta debe adaptarse a los requisitos y complejidad reales. + +**SOLID no obliga a utilizar Clean Architecture, arquitectura hexagonal o microservicios en cualquier proyecto.** + +--- + +## 7. Errores habituales al aplicar SOLID + +### Aplicar SRP como "una clase, un método" + +Una responsabilidad puede incluir numerosas operaciones relacionadas. + +Dividirlas indiscriminadamente puede reducir la cohesión. + +### Aplicar OCP mediante abstracciones especulativas + +No crees extensibilidad para requisitos hipotéticos. + +Introduce puntos de extensión cuando exista una necesidad identificada. + +### Considerar LSP únicamente como compatibilidad de tipos + +Compartir una interfaz no garantiza cumplir el mismo contrato semántico. + +Comprueba invariantes, resultados y expectativas. + +### Aplicar ISP creando demasiadas interfaces + +Las interfaces deben representar capacidades coherentes. + +No dividas contratos únicamente para reducir el número de métodos. + +### Aplicar DIP mediante interfaces para todas las clases + +DIP no consiste en prohibir dependencias concretas. + +Consiste en evitar que las políticas importantes dependan innecesariamente de detalles técnicos. + +### Introducir patrones sin necesidad + +SOLID no exige utilizar automáticamente: + +- Repositorios. +- Factorías. +- Estrategias. +- Adaptadores. +- Contenedores de dependencias. +- Jerarquías de herencia. + +Utiliza estos mecanismos cuando resuelvan un problema concreto. + +### Refactorizar código estable sin justificación + +No modifiques una implementación que funciona correctamente únicamente para ajustarla a una interpretación más rígida de SOLID. + +Evalúa el beneficio esperado, el riesgo y el alcance de la tarea. + +--- + +## 8. Referencia rápida + +| Principio | Idea fundamental | Señal de revisión | +|---|---|---| +| **SRP** | Una responsabilidad coherente y motivos de cambio relacionados. | Clase que acumula responsabilidades independientes. | +| **OCP** | Permitir extensiones justificadas sin modificar innecesariamente código estable. | Variantes frecuentes que obligan a cambiar múltiples componentes. | +| **LSP** | Las implementaciones deben respetar el contrato común. | Comportamientos incompatibles o comprobaciones especiales por tipo. | +| **ISP** | Los consumidores dependen únicamente de las capacidades necesarias. | Interfaces amplias con operaciones irrelevantes o implementaciones parciales. | +| **DIP** | Evitar dependencias innecesarias entre políticas de alto nivel y detalles técnicos. | Reglas de negocio fuertemente acopladas a infraestructura concreta. | + +--- + +## 9. Lista de comprobación SOLID + +### Responsabilidad única (SRP) + +- [ ] ¿Cada clase o módulo tiene una responsabilidad coherente? +- [ ] ¿Existen motivos de cambio independientes que justifiquen una separación? +- [ ] ¿He evitado fragmentar responsabilidades relacionadas? +- [ ] ¿La organización facilita comprender y mantener el código? + +### Abierto/cerrado (OCP) + +- [ ] ¿Existe una necesidad real de extensión? +- [ ] ¿Puedo incorporar variantes sin modificar innecesariamente código estable? +- [ ] ¿Las abstracciones introducidas están justificadas? +- [ ] ¿He evitado diseñar para requisitos hipotéticos? + +### Sustitución de Liskov (LSP) + +- [ ] ¿Las implementaciones respetan el contrato común? +- [ ] ¿Se mantienen las invariantes? +- [ ] ¿Las precondiciones y postcondiciones son compatibles? +- [ ] ¿Los consumidores pueden utilizar otra implementación sin tratamientos especiales? +- [ ] ¿Las diferencias de comportamiento están correctamente definidas? + +### Segregación de interfaces (ISP) + +- [ ] ¿Cada interfaz representa capacidades coherentes? +- [ ] ¿Los consumidores dependen de operaciones que realmente necesitan? +- [ ] ¿Existen implementaciones forzadas a soportar operaciones irrelevantes? +- [ ] ¿He evitado fragmentar interfaces artificialmente? + +### Inversión de dependencias (DIP) + +- [ ] ¿Las reglas de negocio dependen innecesariamente de detalles técnicos? +- [ ] ¿Las abstracciones están situadas en límites apropiados? +- [ ] ¿La inyección de dependencias aporta ventajas concretas? +- [ ] ¿He evitado interfaces y adaptadores innecesarios? +- [ ] ¿Se respeta la arquitectura existente y sus contratos? + +--- + +## Principio final + +> "Los principios SOLID deben facilitar el cambio, reducir el acoplamiento y mejorar la comprensión del software, no convertirse en una obligación de introducir más clases e interfaces." + +Aplica SOLID con criterio profesional, considerando las necesidades del dominio, el tamaño del sistema, sus requisitos de evolución y el coste de mantenimiento. + +**Una solución sencilla, cohesionada y correctamente desacoplada es preferible a una arquitectura artificialmente compleja que únicamente aparenta cumplir SOLID.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/tdd.md b/.agents/skills/rdx-solid/referencias/tdd.md new file mode 100644 index 0000000..d8f07c2 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/tdd.md @@ -0,0 +1,567 @@ +# Desarrollo guiado por pruebas (Test-Driven Development) + +El desarrollo guiado por pruebas (TDD) es una metodología que utiliza pruebas automatizadas para orientar la implementación y evolución del software. + +El objetivo no es simplemente aumentar el número de pruebas, sino diseñar código verificable, expresar claramente los comportamientos esperados y reducir el riesgo de introducir regresiones. + +Este documento desarrolla los principios definidos en el `SKILL.md` principal. + +**TDD es la metodología preferente cuando resulta apropiada, pero no debe imponerse de forma mecánica si existe una alternativa de verificación más adecuada.** + +## 1. El ciclo fundamental de TDD + +```text +ROJO → VERDE → REFACTORIZAR → ROJO → ... +``` + +Cada iteración debe ser pequeña y centrarse en un comportamiento concreto. + +### 1.1. Fase ROJO (RED) + +Escribe una prueba que falle y describa el comportamiento que deseas implementar. + +La prueba debe: + +- Utilizar lenguaje del dominio, evitando detalles técnicos innecesarios. +- Describir **QUÉ** debe ocurrir, no **CÓMO** debe implementarse. +- Representar un ejemplo concreto y verificable. +- Fallar por la ausencia del comportamiento esperado, no por errores accidentales de configuración. +- Expresar claramente el resultado esperado. + +```typescript +// MAL: Descripción demasiado abstracta. +it('can add numbers', () => { + expect(add(2, 3)).toBe(5); +}); + +// BIEN: Ejemplo concreto. +it('returns 5 when adding 2 and 3', () => { + expect(add(2, 3)).toBe(5); +}); +``` + +Aunque las descripciones de pruebas son cadenas de texto, sigue las convenciones del proyecto. Si se consideran documentación del comportamiento, pueden redactarse en castellano. + +**Comprueba que la prueba falla por el motivo esperado antes de continuar.** + +Una prueba que falla por un error de importación, una dependencia ausente o una configuración incorrecta no demuestra necesariamente que el comportamiento esté pendiente de implementar. + +### 1.2. Fase VERDE (GREEN) + +Escribe la implementación más sencilla que permita superar la prueba. + +Evita introducir funcionalidades, configuraciones o abstracciones que todavía no sean necesarias. + +Existen dos estrategias habituales. + +**Estrategia 1: Fake It (simular inicialmente el resultado)** + +Consiste en utilizar una implementación provisional mínima para superar un ejemplo concreto. + +```typescript +function add(a: number, b: number): number { + return 5; +} +``` + +Esta implementación únicamente satisface un ejemplo específico. + +Puede utilizarse como paso temporal durante el aprendizaje de TDD o cuando se busca descubrir una generalización mediante pruebas adicionales. + +**No debe conservarse como implementación final si el comportamiento requerido exige una solución general.** + +**Estrategia 2: Obvious Implementation (implementación evidente)** + +Cuando la solución es sencilla y conocida, implementa directamente el comportamiento. + +```typescript +function add(a: number, b: number): number { + return a + b; +} +``` + +No es necesario utilizar *Fake It* si la implementación correcta resulta evidente. + +### Criterios para elegir la estrategia + +- Utiliza *Fake It* cuando necesites avanzar incrementalmente hacia una generalización. +- Utiliza *Obvious Implementation* cuando el comportamiento sea sencillo y conocido. +- Evita construir una arquitectura completa a partir de un único ejemplo. +- No introduzcas casos hipotéticos sin una necesidad identificada. +- Asegúrate de que el conjunto de pruebas comprueba el comportamiento realmente requerido. + +**La simplicidad debe evaluarse respecto al comportamiento esperado, no únicamente respecto a la cantidad de código.** + +### 1.3. Fase REFACTORIZAR (REFACTOR) + +Una vez que las pruebas pasan, mejora el diseño sin modificar el comportamiento observable. + +Revisa: + +- Duplicación de conocimiento o reglas de negocio. +- Métodos o clases con responsabilidades mezcladas. +- Nombres poco expresivos. +- Condiciones difíciles de comprender. +- Dependencias innecesarias. +- Abstracciones prematuras. +- Estructuras que dificulten el mantenimiento. + +La refactorización debe realizarse en incrementos pequeños, verificando que las pruebas continúan pasando. + +**No refactorices únicamente para satisfacer métricas arbitrarias de líneas, atributos o número de métodos.** + +El diseño puede evolucionar durante la refactorización, pero también requiere decisiones previas cuando sea necesario definir contratos, límites entre módulos o responsabilidades arquitectónicas. + +--- + +## 2. Las tres leyes del TDD + +Durante la aplicación estricta del ciclo TDD: + +1. **No escribas código de producción** salvo para conseguir que una prueba que falla pase correctamente. +2. **No escribas más código de prueba** del necesario para demostrar el fallo del comportamiento esperado. +3. **No escribas más código de producción** del necesario para superar la prueba que falla. + +Estas leyes favorecen los incrementos pequeños y evitan implementar funcionalidades no solicitadas. + +No deben interpretarse como una prohibición universal de escribir código cuando TDD no sea la metodología apropiada. + +### Excepciones justificadas + +Puede utilizarse otro procedimiento cuando: + +- Se modifique configuración, documentación o infraestructura sin comportamiento directamente comprobable mediante pruebas unitarias. +- Se trabaje con código legado sin infraestructura de pruebas adecuada. +- Se realicen prototipos exploratorios. +- Sea necesario corregir urgentemente un problema y no resulte viable escribir primero una prueba. +- Se efectúen modificaciones estructurales cubiertas por pruebas existentes. + +En estos casos: + +1. Identifica el comportamiento que debe mantenerse o modificarse. +2. Selecciona el mecanismo de verificación más adecuado. +3. Realiza cambios pequeños y controlados. +4. Ejecuta las comprobaciones disponibles. +5. Añade pruebas de regresión cuando resulte técnicamente viable. + +**No utilizar TDD no significa trabajar sin verificación.** + +--- + +## 3. La regla de tres + +La regla de tres es una heurística para identificar oportunidades de abstracción. + +Tradicionalmente propone esperar hasta observar varias repeticiones antes de extraer código común. + +Su propósito es evitar crear abstracciones incorrectas a partir de similitudes superficiales. + +```typescript +// Primera aparición: implementar directamente. + +// Segunda aparición: comprobar si existe conocimiento compartido. + +// Tercera aparición: reconsiderar si conviene extraer una abstracción. +``` + +### Aplicación recomendada + +- No extraigas automáticamente código después de la tercera repetición. +- No esperes obligatoriamente a tres repeticiones cuando ya exista una regla de negocio compartida. +- Comprueba que las implementaciones representen el mismo concepto. +- Evita abstraer código que únicamente se parece sintácticamente. +- Valora el coste de mantenimiento de la duplicación frente al coste de la abstracción. + +**La duplicación de conocimiento puede ser más peligrosa que la duplicación de instrucciones.** + +Por ejemplo, dos implementaciones independientes de una misma regla fiscal pueden justificar una extracción inmediata. + +En cambio, tres operaciones similares pertenecientes a dominios diferentes pueden mantenerse separadas. + +> "Un poco de duplicación es mejor que una abstracción equivocada." + +--- + +## 4. Triangulación + +La triangulación consiste en utilizar varios ejemplos concretos para orientar la implementación hacia una solución general. + +Cada prueba adicional restringe las implementaciones posibles y ayuda a descubrir reglas que satisfacen todos los casos. + +### Ejemplo + +Primera prueba: + +```typescript +it('returns 5 when adding 2 and 3', () => { + expect(add(2, 3)).toBe(5); +}); +``` + +Una implementación provisional podría devolver siempre `5`. + +Segunda prueba: + +```typescript +it('returns 9 when adding 4 and 5', () => { + expect(add(4, 5)).toBe(9); +}); +``` + +Esta prueba demuestra que devolver una constante no satisface el comportamiento general. + +La implementación debe evolucionar: + +```typescript +function add(a: number, b: number): number { + return a + b; +} +``` + +### Criterios de triangulación + +- Añade ejemplos que aporten información nueva. +- Incluye casos límite cuando sean relevantes. +- Evita pruebas redundantes que no aumenten la confianza. +- No generes combinaciones arbitrarias sin relación con los requisitos. +- Utiliza la triangulación para descubrir comportamientos, no para retrasar artificialmente una solución evidente. + +La triangulación es especialmente útil cuando las reglas del dominio son complejas o todavía no están completamente definidas. + +--- + +## 5. Prioridad de las transformaciones (Transformation Priority Premise) + +La *Transformation Priority Premise* propone avanzar desde implementaciones simples hacia otras más complejas mediante transformaciones pequeñas. + +Es una herramienta de aprendizaje y razonamiento, no un algoritmo obligatorio de implementación. + +| Prioridad orientativa | Transformación | +|---|---| +| 1 | Estructura vacía → ausencia de valor (`nil`/`null`) | +| 2 | Ausencia de valor → constante | +| 3 | Constante → variable | +| 4 | Comportamiento incondicional → condicional | +| 5 | Valor escalar → colección | +| 6 | Instrucción → recursión | +| 7 | Valor → valor modificado | + +### Criterios de aplicación + +- Prefiere transformaciones pequeñas cuando reduzcan el riesgo. +- No introduzcas complejidad antes de que resulte necesaria. +- No fuerces una secuencia concreta de transformaciones. +- Utiliza directamente una implementación evidente cuando sea más clara. +- Evalúa la transformación según las características del lenguaje y del dominio. + +**La prioridad no debe interpretarse como una clasificación universal de complejidad o calidad.** + +--- + +## 6. Patrón Preparar-Actuar-Verificar (Arrange-Act-Assert) + +La referencia canónica sobre AAA y nomenclatura es [testing.md](testing.md), secciones 3 y 4. Aquí se conserva su aplicación al ciclo TDD y al diseño de la prueba en orden inverso. + +El patrón AAA estructura las pruebas en tres fases: + +1. **Preparar (Arrange):** establecer el estado y los datos necesarios. +2. **Actuar (Act):** ejecutar el comportamiento que se desea comprobar. +3. **Verificar (Assert):** comprobar el resultado esperado. + +```typescript +it('applies a 10 percent discount to the order total', () => { + // Preparar: Crear el pedido y el descuento. + const order = new Order(); + order.addItem({ price: 100 }); + + const discount = new PercentDiscount(10); + + // Actuar: Calcular el importe final. + const total = order.calculateTotal(discount); + + // Verificar: Comprobar el resultado. + expect(total).toBe(90); +}); +``` + +El ejemplo presupone que el dominio utiliza importes numéricos. En un sistema con reglas monetarias específicas puede ser preferible utilizar objetos de valor como `Money`. + +### Buenas prácticas + +- Mantén claramente identificable el comportamiento probado. +- Evita configuraciones extensas que oculten la intención. +- Utiliza funciones auxiliares o factorías de pruebas cuando simplifiquen la preparación. +- No introduzcas abstracciones de pruebas innecesarias. +- Mantén las verificaciones relacionadas con el comportamiento esperado. + +No es obligatorio escribir comentarios para separar las tres fases cuando la estructura resulte evidente. + +--- + +## 7. Escribir pruebas en orden inverso + +En ocasiones resulta útil diseñar una prueba comenzando por la verificación. + +El procedimiento consiste en: + +1. **ASSERT:** definir qué resultado debe comprobarse. +2. **ACT:** identificar qué operación produce ese resultado. +3. **ARRANGE:** preparar únicamente los datos y dependencias necesarios. + +### Ejemplo + +Primero, define la expectativa: + +```typescript +expect(total).toBe(90); +``` + +Después, identifica la operación: + +```typescript +const total = order.calculateTotal(discount); +``` + +Por último, prepara el escenario: + +```typescript +const order = new Order(); +order.addItem({ price: 100 }); + +const discount = new PercentDiscount(10); +``` + +Esta técnica ayuda a evitar configuraciones innecesarias y favorece pruebas centradas en resultados observables. + +No es obligatorio utilizarla cuando el comportamiento ya esté suficientemente claro. + +--- + +## 8. Nomenclatura de las pruebas + +Los nombres deben describir comportamientos concretos utilizando el lenguaje del dominio. + +### Principios + +- Describe el comportamiento esperado. +- Utiliza ejemplos concretos cuando aporten claridad. +- Evita referencias innecesarias a detalles internos. +- Mantén una relación clara entre el nombre y la verificación realizada. +- Utiliza nombres coherentes con las convenciones del proyecto. +- Facilita identificar el fallo sin necesidad de inspeccionar toda la prueba. + +```typescript +// MAL: Se centra en un detalle interno. +it('sets the data property to 1', () => { + // ... +}); +``` + +```typescript +// BIEN: Describe un comportamiento observable. +it('recognizes "mom" as a palindrome', () => { + expect(isPalindrome('mom')).toBe(true); +}); +``` + +### Una responsabilidad de prueba por caso + +Como orientación, cada prueba debe verificar un comportamiento concreto. + +Esto no significa limitarla a una única instrucción `expect`. + +Una misma operación puede requerir comprobar varios efectos relacionados. + +```typescript +it('creates an order with the expected initial state', () => { + const order = createOrder(); + + expect(order.status).toBe('pending'); + expect(order.items).toHaveLength(0); + expect(order.total).toBe(0); +}); +``` + +Estas verificaciones pertenecen al mismo comportamiento inicial. + +Evita agrupar escenarios independientes dentro de una única prueba extensa. + +--- + +## 9. TDD clásico y Mockist + +Existen diferentes estilos de TDD. Ninguno debe imponerse universalmente. + +### 9.1. TDD clásico (Detroit/Chicago) + +Se centra principalmente en los estados y resultados observables. + +Características: + +- Prefiere utilizar objetos reales cuando resulta práctico. +- Evita simulaciones innecesarias. +- Comprueba resultados y cambios de estado. +- Permite que el diseño evolucione mediante el comportamiento. + +Resulta especialmente apropiado para: + +- Funciones puras. +- Objetos de dominio. +- Algoritmos. +- Reglas de negocio. +- Componentes que pueden probarse sin infraestructura externa. + +El uso de objetos reales no convierte automáticamente una prueba en una prueba de integración. + +### 9.2. TDD Mockist (London) + +Se centra especialmente en las interacciones y colaboración entre componentes. + +Características: + +- Utiliza dobles de prueba para sustituir determinadas dependencias. +- Permite verificar colaboraciones relevantes. +- Facilita aislar componentes. +- Puede favorecer diseños basados en interfaces y responsabilidades explícitas. + +Resulta útil cuando: + +- Existen dependencias externas. +- Deben verificarse interacciones relevantes. +- Es necesario controlar respuestas o errores de colaboradores. +- La infraestructura dificulta ejecutar pruebas rápidas y deterministas. + +### Criterios para utilizar mocks + +- No simules dependencias únicamente por costumbre. +- Evita simular objetos de dominio sencillos que puedan utilizarse directamente. +- Utiliza dobles de prueba en los límites con bases de datos, servicios externos, sistemas de archivos u otras dependencias cuando resulte apropiado. +- No hagas que las pruebas dependan excesivamente de llamadas internas irrelevantes. +- Comprueba que las simulaciones respetan los contratos reales de sus dependencias. +- Complementa las pruebas aisladas con pruebas de integración cuando sea necesario. + +**Una prueba que verifica demasiados detalles de implementación puede fallar ante una refactorización correcta, aunque el comportamiento observable no haya cambiado.** + +Selecciona el enfoque que proporcione mayor confianza con un coste razonable de mantenimiento. + +--- + +## 10. TDD en código legado + +El código legado puede carecer de pruebas, presentar dependencias difíciles de aislar o contener comportamientos implícitos. + +En estos escenarios, no siempre resulta apropiado comenzar con una prueba unitaria convencional. + +### Procedimiento recomendado + +1. Identifica el comportamiento que debe modificarse. +2. Analiza sus consumidores, contratos y dependencias. +3. Comprueba qué pruebas y herramientas de verificación existen. +4. Cuando sea viable, escribe pruebas de caracterización para documentar el comportamiento actual. +5. Introduce los cambios de forma incremental. +6. Añade pruebas de regresión para el comportamiento corregido o incorporado. +7. Ejecuta las verificaciones disponibles. + +### Pruebas de caracterización + +Una prueba de caracterización describe el comportamiento actual de un componente, aunque este no sea necesariamente el comportamiento ideal. + +Su objetivo es detectar cambios accidentales durante una refactorización. + +No debe utilizarse para convertir automáticamente un error conocido en un requisito permanente. + +Cuando el objetivo sea corregir un error, distingue entre: + +- Comportamiento existente que debe preservarse. +- Comportamiento defectuoso que debe corregirse. +- Comportamiento nuevo que debe incorporarse. + +--- + +## 11. Pruebas de regresión + +Al corregir un defecto reproducible, es recomendable incorporar una prueba que detecte su reaparición. + +### Procedimiento + +1. Reproduce el defecto. +2. Identifica el comportamiento esperado. +3. Escribe una prueba que falle por el defecto, cuando sea viable. +4. Corrige la implementación. +5. Comprueba que la prueba pasa. +6. Ejecuta las pruebas relacionadas para detectar regresiones. + +Evita modificar o eliminar pruebas existentes únicamente para conseguir que la nueva implementación pase. + +Si el comportamiento anterior era incorrecto y las pruebas lo reflejaban, actualízalas justificando el cambio de requisito o la corrección del defecto. + +--- + +## 12. Errores habituales al aplicar TDD + +| Error | Criterio de corrección | +|---|---| +| Escribir código antes de una prueba durante un ciclo TDD | Recuperar el ciclo Rojo-Verde-Refactorizar. | +| Escribir demasiadas pruebas antes de implementar | Trabajar en incrementos de comportamiento pequeños. | +| Implementar más funcionalidad de la necesaria | Limitarse a los requisitos y ejemplos relevantes. | +| Omitir sistemáticamente la refactorización | Revisar el diseño una vez superadas las pruebas. | +| Probar detalles internos | Priorizar comportamientos y contratos observables. | +| Utilizar nombres abstractos | Describir escenarios y resultados concretos. | +| Extraer abstracciones demasiado pronto | Esperar a comprender la responsabilidad compartida. | +| Aplicar literalmente la regla de tres | Evaluar duplicación de conocimiento y coste de mantenimiento. | +| Abusar de mocks | Utilizar objetos reales cuando resulten más sencillos y fiables. | +| Mantener implementaciones artificiales de *Fake It* | Generalizar cuando las pruebas y requisitos lo exijan. | +| Confiar únicamente en pruebas unitarias | Incorporar pruebas de integración o E2E cuando corresponda. | +| Modificar pruebas para ocultar defectos | Investigar el fallo y preservar los requisitos correctos. | +| Exigir TDD en cualquier circunstancia | Seleccionar la estrategia de verificación adecuada al contexto. | + +--- + +## 13. Lista de comprobación de TDD + +### Antes de implementar + +- [ ] ¿Comprendo el comportamiento esperado? +- [ ] ¿Existen criterios de aceptación claros? +- [ ] ¿Qué nivel de prueba resulta más apropiado? +- [ ] ¿Puedo escribir primero una prueba que reproduzca el comportamiento pendiente? +- [ ] ¿Conozco el comportamiento existente que debo preservar? +- [ ] ¿Puedo utilizar la infraestructura de pruebas del proyecto? + +### Durante la implementación + +- [ ] ¿La prueba falla inicialmente por el motivo esperado? +- [ ] ¿La implementación satisface el comportamiento requerido? +- [ ] ¿Estoy evitando funcionalidades no solicitadas? +- [ ] ¿Las pruebas siguen siendo deterministas? +- [ ] ¿Estoy probando comportamientos en lugar de detalles internos? +- [ ] ¿He evitado mocks y abstracciones innecesarias? + +### Durante la refactorización + +- [ ] ¿Las pruebas continúan pasando? +- [ ] ¿Se conserva el comportamiento observable? +- [ ] ¿Las abstracciones introducidas representan conceptos reales? +- [ ] ¿He mejorado la comprensión sin fragmentar artificialmente el código? +- [ ] ¿He evitado cambios ajenos al alcance de la tarea? + +### Al finalizar + +- [ ] ¿Se satisfacen los criterios de aceptación? +- [ ] ¿Se han ejecutado las pruebas relevantes? +- [ ] ¿Se han incorporado pruebas de regresión cuando corresponde? +- [ ] ¿Se han verificado los contratos y comportamientos afectados? +- [ ] ¿He identificado las pruebas que no pudieron ejecutarse? +- [ ] ¿Puedo comunicar con precisión qué se ha verificado y qué permanece pendiente? + +--- + +## Principio final + +> "TDD es una herramienta para descubrir y verificar comportamientos, no un objetivo independiente de la calidad del software." + +Utiliza el ciclo Rojo-Verde-Refactorizar cuando permita desarrollar de forma segura, incremental y comprensible. + +No conviertas las reglas metodológicas en restricciones que impidan adoptar una solución técnicamente más apropiada. + +**El objetivo es producir software correcto, verificable, sencillo y mantenible, utilizando las pruebas como una herramienta para mejorar tanto el diseño como la confianza en el comportamiento del sistema.** \ No newline at end of file diff --git a/.agents/skills/rdx-solid/referencias/testing.md b/.agents/skills/rdx-solid/referencias/testing.md new file mode 100644 index 0000000..eb2bf38 --- /dev/null +++ b/.agents/skills/rdx-solid/referencias/testing.md @@ -0,0 +1,1200 @@ +# Estrategia de pruebas (Testing Strategy) + +Las pruebas de software permiten verificar comportamientos, detectar regresiones y proporcionar confianza durante el desarrollo y evolución de un sistema. + +Su objetivo no es maximizar el número de pruebas ni alcanzar métricas de cobertura arbitrarias, sino comprobar que el software satisface correctamente sus requisitos. + +Este documento desarrolla las prácticas de pruebas establecidas en el `SKILL.md` principal y complementa las recomendaciones de [tdd.md](tdd.md). + +**Selecciona las pruebas según el comportamiento, el riesgo y el nivel de confianza necesario. No impongas una distribución fija de tipos de pruebas a todos los proyectos.** + +## 1. La pirámide de pruebas (Testing Pyramid) + +La pirámide de pruebas es un modelo que propone combinar diferentes niveles de verificación. + +Tradicionalmente se representa de la siguiente forma: + +```text id="gjbpx1" + /\ + / \ + / E2E\ + /------\ + / \ + /Integración\ + /------------\ + / \ + / Unitarias \ + /__________________\ +``` + +La representación sugiere mantener una base amplia de pruebas rápidas y un conjunto más reducido de pruebas que requieren integrar numerosos componentes. + +### Interpretación correcta + +- Las pruebas unitarias suelen proporcionar retroalimentación rápida. +- Las pruebas de integración verifican colaboraciones y límites técnicos. +- Las pruebas E2E comprueban recorridos completos desde la perspectiva del usuario o consumidor. + +Sin embargo, la pirámide no establece cantidades obligatorias. + +La distribución adecuada depende de: + +- Arquitectura del sistema. +- Complejidad del dominio. +- Integraciones externas. +- Riesgo funcional. +- Criticidad de los procesos. +- Facilidad para ejecutar los distintos tipos de pruebas. +- Coste de mantenimiento. + +### Criterios de aplicación + +- Utiliza pruebas unitarias cuando permitan comprobar comportamientos de forma rápida y aislada. +- Incorpora pruebas de integración para los contratos y límites relevantes. +- Utiliza pruebas E2E cuando sea necesario comprobar un flujo completo. +- Evita duplicar indiscriminadamente los mismos escenarios en todos los niveles. +- No presupongas que una prueba de mayor nivel proporciona siempre más confianza. +- Selecciona el nivel más adecuado para detectar cada tipo de regresión. + +**Una estrategia de pruebas eficaz optimiza la confianza obtenida respecto al coste de ejecución y mantenimiento.** + +--- + +## 2. Tipos de pruebas + +### 2.1. Pruebas unitarias (Unit Tests) + +Las pruebas unitarias verifican unidades de comportamiento en condiciones controladas. + +Una unidad puede ser: + +- Una función. +- Una clase. +- Un objeto de dominio. +- Un módulo pequeño. +- Un conjunto cohesionado de componentes. + +No es obligatorio que una prueba unitaria compruebe exactamente una clase o función. + +### Características habituales + +- Ejecución rápida. +- Resultados deterministas. +- Preparación sencilla. +- Independencia de infraestructura externa cuando sea apropiado. +- Verificación de comportamientos concretos. + +### Ejemplo + +```typescript id="qv1r4u" +describe('Order', () => { + it('calculates the total of two items', () => { + const order = new Order(); + + order.addItem({ price: 100 }); + order.addItem({ price: 50 }); + + expect(order.calculateTotal()).toBe(150); + }); +}); +``` + +El ejemplo utiliza importes numéricos para ilustrar la estructura. + +En sistemas con reglas monetarias relevantes, utiliza una representación adecuada como `Money`. + +### Cuándo utilizarlas + +Son especialmente útiles para: + +- Reglas de negocio. +- Cálculos. +- Validaciones. +- Objetos de valor. +- Entidades. +- Transformaciones. +- Algoritmos. +- Transiciones de estado. + +### Criterios de aplicación + +- Comprueba comportamientos observables. +- Evita dependencias externas innecesarias. +- Utiliza objetos reales cuando sean sencillos y deterministas. +- No introduzcas mocks para todas las dependencias por obligación. +- Evita pruebas que dependan de métodos privados o detalles internos. +- No escribas pruebas triviales que únicamente reproduzcan una implementación sin aportar confianza. + +**Una prueba unitaria debe proporcionar confianza sobre un comportamiento, no verificar cómo está organizado internamente el código.** + +--- + +### 2.2. Pruebas de integración (Integration Tests) + +Las pruebas de integración verifican la colaboración entre componentes o sistemas. + +Pueden incluir: + +- Repositorios y bases de datos. +- APIs y servicios. +- Serialización y deserialización. +- Adaptadores. +- Sistemas de archivos. +- Colas de mensajería. +- Mecanismos de autenticación. +- Transacciones. +- Contratos entre módulos. + +### Características habituales + +- Mayor preparación que las pruebas unitarias. +- Dependencias reales o representativas. +- Verificación de límites entre componentes. +- Coste de ejecución variable. +- Detección de errores que no aparecen en pruebas completamente aisladas. + +### Ejemplo con persistencia + +```typescript id="170oc9" +describe('OrderRepository integration', () => { + let database: TestDatabase; + let repository: OrderRepository; + + beforeAll(async () => { + database = await createTestDatabase(); + repository = new PostgresOrderRepository(database.connection); + }); + + afterAll(async () => { + await database.close(); + }); + + it('persists and retrieves an order', async () => { + const order = createTestOrder(); + const expectedStatus = order.status; + const expectedTotal = order.calculateTotal(); + + await repository.save(order); + + const retrieved = await repository.findById(order.id); + + expect(retrieved).not.toBeNull(); + expect(retrieved?.id.equals(order.id)).toBe(true); + expect(retrieved?.status).toBe(expectedStatus); + expect(retrieved?.calculateTotal()).toEqual(expectedTotal); + }); +}); +``` + +El ejemplo presupone una infraestructura de pruebas que proporciona una base de datos controlada y un pedido con `status` público y `calculateTotal()`. El total esperado debe ser un valor numérico u objeto de valor inmutable; en otros modelos, captura una proyección independiente con todos los campos y líneas que el contrato exige persistir. + +Para un sistema real, deben establecerse mecanismos de aislamiento y limpieza de datos entre pruebas. + +### Criterios de aplicación + +- Verifica las integraciones que puedan producir fallos relevantes. +- Utiliza dependencias reales cuando sea necesario comprobar su comportamiento. +- Mantén entornos reproducibles. +- Evita depender de datos de producción. +- Aísla el estado de las pruebas. +- Comprueba errores y comportamientos de integración relevantes. +- No conviertas todas las pruebas en pruebas de integración si una prueba unitaria proporciona suficiente confianza. + +### Diferencia importante + +Una prueba que utiliza varios objetos reales no es automáticamente una prueba de integración de infraestructura. + +Del mismo modo, una prueba que simula todas las dependencias externas puede estar verificando únicamente la coordinación de un componente. + +**Clasifica las pruebas por el comportamiento y los límites que comprueban, no únicamente por el número de objetos utilizados.** + +--- + +### 2.3. Pruebas de extremo a extremo y aceptación (E2E / Acceptance Tests) + +Las pruebas E2E verifican recorridos completos desde la perspectiva del usuario o consumidor del sistema. + +Pueden atravesar: + +- Interfaz de usuario. +- APIs. +- Autenticación. +- Aplicación. +- Dominio. +- Persistencia. +- Integraciones. + +### Características habituales + +- Mayor número de componentes implicados. +- Preparación más compleja. +- Posible mayor tiempo de ejecución. +- Sensibilidad a cambios de interfaz o infraestructura. +- Capacidad para detectar problemas de integración completa. + +### Ejemplo + +```typescript id="mq08am" +test('the user can complete checkout', async ({ page }) => { + await page.goto('/products'); + + await page.getByRole('button', { + name: 'Añadir al carrito', + }).click(); + + await page.getByRole('button', { + name: 'Finalizar compra', + }).click(); + + await page.getByRole('button', { + name: 'Confirmar pedido', + }).click(); + + await expect( + page.getByRole('heading', { + name: 'Pedido confirmado', + }), + ).toBeVisible(); +}); +``` + +El ejemplo presupone un entorno de pruebas que permite confirmar pedidos sin ejecutar cargos reales. + +### Buenas prácticas + +- Prioriza recorridos críticos. +- Incorpora otros flujos cuando el riesgo lo justifique. +- Utiliza selectores accesibles y estables. +- Evita depender de textos o estructuras internas innecesarias cuando existan alternativas más robustas. +- Prepara datos de prueba controlados. +- Evita dependencias de servicios externos inestables. +- Utiliza entornos aislados para pagos, correos y otras operaciones sensibles. +- Comprueba resultados observables. + +No introduzcas números de tarjeta, credenciales o datos reales en las pruebas. + +### Pruebas de aceptación + +Las pruebas de aceptación comprueban requisitos desde la perspectiva del negocio o del usuario. + +Pueden implementarse mediante pruebas E2E, integración u otros niveles. + +**Aceptación y E2E no son necesariamente conceptos equivalentes.** + +--- + +## 3. Patrón Preparar-Actuar-Verificar (Arrange-Act-Assert) + +El patrón AAA organiza las pruebas en tres fases: + +1. **Preparar (Arrange):** establecer los datos y condiciones necesarias. +2. **Actuar (Act):** ejecutar el comportamiento. +3. **Verificar (Assert):** comprobar el resultado. + +### Ejemplo + +```typescript id="4e1tbg" +it('applies a 20 percent discount to premium users', () => { + // Preparar. + const user = new User({ isPremium: true }); + const cart = new Cart(user); + + cart.addItem({ price: 100 }); + + // Actuar. + const total = cart.calculateTotal(); + + // Verificar. + expect(total).toBe(80); +}); +``` + +### Criterios de aplicación + +- Mantén identificable la operación probada. +- Evita preparaciones extensas que oculten la intención. +- Utiliza factorías de prueba cuando simplifiquen el escenario. +- Agrupa verificaciones relacionadas. +- No introduzcas abstracciones innecesarias. +- No es obligatorio incluir comentarios AAA cuando la estructura resulte evidente. + +### Escribir AAA en orden inverso + +En ocasiones resulta más sencillo diseñar la prueba comenzando por el resultado esperado: + +1. **Assert:** ¿qué quiero comprobar? +2. **Act:** ¿qué operación debe producirlo? +3. **Arrange:** ¿qué datos y dependencias necesito? + +Este procedimiento ayuda a diseñar pruebas centradas en resultados. + +No es obligatorio utilizarlo para todas las pruebas. + +--- + +## 4. Nomenclatura de las pruebas + +Los nombres deben describir comportamientos concretos y verificables. + +### Nombres poco expresivos + +```typescript id="d5nbzl" +it('should work correctly', () => {}); +it('handles the edge case', () => {}); +it('sets the data property', () => {}); +``` + +### Nombres más adecuados + +```typescript id="rx85zs" +it('calculates a 20 percent discount for premium users', () => {}); +it('returns an error when the cart is empty', () => {}); +it('recognizes racecar as a palindrome', () => {}); +``` + +### Convenciones habituales + +**Descripción directa del comportamiento** + +```typescript id="wj1e44" +it('applies tax based on the shipping state', () => {}); +``` + +**When-Then** + +```typescript id="533rfi" +it('returns 5 when adding 2 and 3', () => {}); +``` + +**Given-When-Then** + +```typescript id="idfv3p" +describe('given a premium user', () => { + describe('when completing checkout', () => { + it('applies a 20 percent discount', () => {}); + }); +}); +``` + +Las cadenas de texto utilizadas como nombres de prueba pueden redactarse en castellano cuando el proyecto lo establezca. + +### Recomendaciones + +- Utiliza lenguaje del dominio. +- Describe resultados observables. +- Evita referencias innecesarias a propiedades privadas. +- Mantén una convención coherente. +- No anides bloques `describe` excesivamente. +- Evita nombres que dependan de una implementación concreta. + +### Una responsabilidad por prueba + +Como orientación, cada prueba debe comprobar un comportamiento concreto. + +Esto no implica limitarla a una única aserción. + +```typescript id="jkx484" +it('creates an order with its initial state', () => { + const order = createTestOrder(); + + expect(order.status).toBe('pending'); + expect(order.items).toHaveLength(0); + expect(order.total).toBe(0); +}); +``` + +Las tres comprobaciones corresponden al mismo comportamiento inicial. + +--- + +## 5. Dobles de prueba (Test Doubles) + +Los dobles de prueba permiten sustituir colaboradores durante la verificación de un componente. + +Su utilización debe responder a una necesidad concreta. + +### 5.1. Dummy + +Objeto que se proporciona porque una interfaz o constructor lo exige, pero que no interviene en el comportamiento probado. + +```typescript id="k3ajk6" +const dummyLogger: Logger = { + log(): void {}, +}; + +const service = new UserService(repository, dummyLogger); +``` + +Evita utilizar aserciones de tipo inseguras como `{} as Logger` cuando puedas proporcionar una implementación mínima válida. + +### 5.2. Stub + +Proporciona respuestas predefinidas. + +```typescript id="9qsma1" +const stubRepository: UserRepository = { + async findById(_id: UserId): Promise { + return createTestUser(); + }, + + async save(_user: User): Promise {}, +}; +``` + +Los stubs permiten controlar escenarios sin depender de infraestructura externa. + +### 5.3. Spy + +Registra las llamadas realizadas a una operación. + +```typescript id="h0x6i6" +const sentEmails: string[] = []; + +const emailSpy: EmailService = { + async send(to: string, _message: string): Promise { + sentEmails.push(to); + }, +}; +``` + +Verificación: + +```typescript id="1xz90c" +expect(sentEmails).toContain('user@example.com'); +``` + +### 5.4. Mock + +Permite definir o verificar interacciones esperadas. + +Ejemplo con Vitest: + +```typescript id="1bcpjq" +const sendMock = vi.fn(async (_to: string, _message: string) => {}); + +const emailService: EmailService = { + send: sendMock, +}; +``` + +Después de ejecutar el comportamiento: + +```typescript id="5qktnx" +expect(sendMock).toHaveBeenCalledWith( + 'user@example.com', + 'Pedido confirmado', +); +``` + +El ejemplo utiliza Vitest. En proyectos con Jest deben emplearse sus APIs equivalentes. + +No mezcles mecanismos de Jest y Vitest indiscriminadamente. + +### 5.5. Fake + +Es una implementación funcional simplificada de una dependencia. + +```typescript id="88n8bh" +class InMemoryUserRepository implements UserRepository { + private readonly users = new Map(); + + async save(user: User): Promise { + this.users.set(user.id.value, user); + } + + async findById(id: UserId): Promise { + return this.users.get(id.value) ?? null; + } +} +``` + +Esta implementación puede permitir pruebas rápidas sin utilizar una base de datos real. + +### Criterios para seleccionar dobles + +- Utiliza objetos reales cuando sean sencillos y deterministas. +- Utiliza stubs para controlar respuestas. +- Utiliza spies o mocks cuando las interacciones formen parte del comportamiento relevante. +- Utiliza fakes cuando una implementación simplificada facilite las pruebas. +- Evita simular detalles internos que no pertenecen al contrato. +- No introduzcas mocks únicamente para alcanzar un aislamiento artificial. + +**Una prueba con demasiadas simulaciones puede verificar únicamente las expectativas del propio test, sin proporcionar confianza suficiente sobre el sistema real.** + +--- + +## 6. Estrategia de pruebas por responsabilidad arquitectónica + +La estrategia debe adaptarse al diseño del proyecto. + +### 6.1. Dominio + +Comprueba: + +- Reglas de negocio. +- Objetos de valor. +- Entidades. +- Invariantes. +- Cálculos. +- Transiciones de estado. + +Las pruebas unitarias suelen resultar apropiadas cuando el dominio puede ejecutarse sin infraestructura externa. + +```typescript id="jxbk58" +describe('Money', () => { + it('adds amounts with the same currency', () => { + const first = Money.euros(10); + const second = Money.euros(20); + + const result = first.add(second); + + expect(result.equals(Money.euros(30))).toBe(true); + }); + + it('rejects amounts with different currencies', () => { + const euros = Money.euros(10); + const dollars = Money.dollars(10); + + expect(() => euros.add(dollars)).toThrow(CurrencyMismatchError); + }); +}); +``` + +### 6.2. Aplicación + +Comprueba: + +- Coordinación de casos de uso. +- Interacciones entre colaboradores. +- Políticas de aplicación. +- Tratamiento de errores. +- Resultados observables. + +```typescript id="7bzp6w" +describe('CreateOrderUseCase', () => { + it('persists a valid order', async () => { + const repository = new InMemoryOrderRepository(); + const useCase = new CreateOrderUseCase(repository); + + const result = await useCase.execute( + createValidOrderRequest(), + ); + + expect(result.success).toBe(true); + expect(await repository.count()).toBe(1); + }); +}); +``` + +No todas las pruebas de aplicación necesitan simular infraestructura. + +### 6.3. Infraestructura + +Comprueba: + +- Persistencia. +- Consultas. +- Adaptadores. +- Serialización. +- Integraciones. +- Transacciones. +- Comportamientos específicos de tecnología. + +```typescript id="gu3tuw" +describe('PostgresOrderRepository', () => { + it('persists and retrieves an order', async () => { + const repository = createTestPostgresRepository(); + const order = createTestOrder(); + const expectedStatus = order.status; + const expectedTotal = order.calculateTotal(); + + await repository.save(order); + + const retrieved = await repository.findById(order.id); + + expect(retrieved?.id.equals(order.id)).toBe(true); + expect(retrieved?.status).toBe(expectedStatus); + expect(retrieved?.calculateTotal()).toEqual(expectedTotal); + }); +}); +``` + +Para pruebas reales, el repositorio debe utilizar una conexión controlada y un mecanismo adecuado de aislamiento de datos. Este ejemplo presupone el mismo contrato ilustrativo de pedido (`status` y `calculateTotal()`) descrito en la sección 2.2; amplía la comparación a todos los campos y líneas persistidos del modelo real. + +### 6.4. Frontend + +Comprueba según corresponda: + +- Renderizado y comportamiento de componentes. +- Interacción del usuario. +- Formularios. +- Validaciones. +- Accesibilidad. +- Gestión de estados. +- Integración con APIs. +- Flujos críticos. + +Prioriza pruebas que interactúen mediante comportamientos observables y no mediante detalles internos del framework. + +### 6.5. Código compartido + +Comprueba: + +- Contratos. +- Esquemas. +- Serialización. +- Validaciones. +- Compatibilidad. +- Transformaciones. +- Invariantes comunes. + +La reutilización de un tipo entre frontend y backend no garantiza que los datos recibidos cumplan dicho tipo durante la ejecución. + +--- + +## 7. Pruebas de integración de alto valor + +Prioriza las pruebas de integración en los límites donde los errores puedan tener consecuencias importantes. + +### 7.1. Límites entre sistemas + +Ejemplos: + +- API y base de datos. +- Aplicación y cola. +- Servicio y proveedor externo. +- Serialización y validación. +- Autenticación y autorización. +- Persistencia y transacciones. + +### 7.2. Procesos críticos + +Presta especial atención a: + +- Pagos. +- Facturación. +- Autorización. +- Integridad de datos. +- Operaciones irreversibles. +- Procesos empresariales esenciales. + +### 7.3. Consultas complejas + +Comprueba consultas que incluyan: + +- Relaciones complejas. +- Agregaciones. +- Filtros relevantes. +- Restricciones de integridad. +- Paginación. +- Concurrencia. +- Comportamientos específicos del motor. + +No presupongas que una consulta funciona correctamente únicamente porque su lógica parece adecuada al inspeccionar el código. + +--- + +## 8. Pruebas de contrato (Contract Tests) + +Las pruebas de contrato permiten comprobar que diferentes implementaciones respetan un comportamiento común. + +Son especialmente útiles para: + +- Repositorios. +- Adaptadores. +- Proveedores externos. +- Implementaciones intercambiables. +- APIs entre aplicaciones. +- Contratos compartidos entre frontend y backend. + +### Ejemplo de contrato compartido + +El ejemplo presupone que `User` expone `id: UserId`, `name: string` y `email: string`, y que `createTestUser(overrides)` crea identificadores únicos y respeta los datos recibidos. Son contratos ilustrativos: adapta las proyecciones a las propiedades públicas o valores del modelo real. Captura los valores esperados antes de guardar; comprobar solo la identidad no detecta la pérdida de otros campos. + +```typescript id="5h26e3" +function testUserRepositoryContract( + createRepository: () => Promise, + resetRepository: () => Promise, +): void { + describe('UserRepository contract', () => { + let repository: UserRepository; + + beforeEach(async () => { + await resetRepository(); + repository = await createRepository(); + }); + + it('persists and retrieves a user', async () => { + const expectedName = 'Contract User 47'; + const expectedEmail = 'contract-47@example.com'; + const user = createTestUser({ + name: expectedName, + email: expectedEmail, + }); + + await repository.save(user); + + const retrieved = await repository.findById(user.id); + + expect(retrieved).not.toBeNull(); + expect(retrieved?.id.equals(user.id)).toBe(true); + expect(retrieved?.name).toBe(expectedName); + expect(retrieved?.email).toBe(expectedEmail); + }); + + it('keeps the persisted data of different users independent', async () => { + const first = createTestUser({ + name: 'First Contract User', + email: 'first-contract@example.com', + }); + const second = createTestUser({ + name: 'Second Contract User', + email: 'second-contract@example.com', + }); + + await repository.save(first); + await repository.save(second); + + const retrievedFirst = await repository.findById(first.id); + const retrievedSecond = await repository.findById(second.id); + + expect(retrievedFirst?.id.equals(first.id)).toBe(true); + expect(retrievedFirst?.name).toBe('First Contract User'); + expect(retrievedFirst?.email).toBe('first-contract@example.com'); + expect(retrievedSecond?.id.equals(second.id)).toBe(true); + expect(retrievedSecond?.name).toBe('Second Contract User'); + expect(retrievedSecond?.email).toBe('second-contract@example.com'); + }); + + it('returns null when the user does not exist', async () => { + const missingId = createMissingUserId(); + + const retrieved = await repository.findById(missingId); + + expect(retrieved).toBeNull(); + }); + }); +} +``` + +Aplicación a distintas implementaciones: + +```typescript id="2ov5k9" +testUserRepositoryContract( + async () => new InMemoryUserRepository(), + async () => resetInMemoryRepository(), +); +``` + +```typescript id="hhpi1w" +testUserRepositoryContract( + async () => createPostgresUserRepository(), + async () => resetTestDatabase(), +); +``` + +### Criterios importantes + +- Verifica comportamientos, no únicamente firmas. +- Comprueba resultados y errores esperados. +- Mantén las pruebas independientes. +- Utiliza una estrategia de limpieza adecuada. +- Comprueba las garantías compartidas por todas las implementaciones. +- Si `save` permite actualizar usuarios, añade una prueba que cambie un campo, guarde y lo recupere de nuevo. No presupongas una semántica de actualización que el contrato no haya definido. +- No presupongas que una implementación en memoria reproduce exactamente el comportamiento de una base de datos real. + +### Contratos entre servicios + +Cuando existan APIs entre frontend y backend o entre servicios, considera: + +- Validación de esquemas. +- Compatibilidad de versiones. +- Campos obligatorios y opcionales. +- Errores contractuales. +- Formatos de fecha y números. +- Identificadores. +- Cambios compatibles e incompatibles. + +Las pruebas de contrato deben complementar, no sustituir automáticamente, las pruebas de integración relevantes. + +--- + +## 9. Constructores de datos de prueba (Test Builders) + +Los *Test Builders* facilitan preparar objetos complejos para escenarios de prueba. + +### Cuándo utilizarlos + +- Los objetos requieren numerosos datos. +- Existen valores predeterminados útiles. +- Los escenarios necesitan variaciones frecuentes. +- La preparación se repite en numerosas pruebas. + +### Ejemplo + +```typescript id="2cdvvu" +interface OrderProps { + id: string; + customerId: string; + items: Item[]; + status: 'pending' | 'paid'; +} +``` + +```typescript id="qj5fhj" +class OrderBuilder { + private id = 'order-1'; + private customerId = 'customer-1'; + private items: Item[] = []; + private status: OrderProps['status'] = 'pending'; + + withId(id: string): this { + this.id = id; + return this; + } + + withItems(items: Item[]): this { + this.items = [...items]; + return this; + } + + paid(): this { + this.status = 'paid'; + return this; + } + + build(): Order { + return Order.create({ + id: this.id, + customerId: this.customerId, + items: [...this.items], + status: this.status, + }); + } +} +``` + +Uso: + +```typescript id="xyal0i" +const order = new OrderBuilder() + .withItems([{ sku: 'ABC', price: 100 }]) + .paid() + .build(); +``` + +### Buenas prácticas + +- Proporciona datos predeterminados válidos. +- Permite modificar únicamente los campos relevantes del escenario. +- Evita compartir referencias mutables entre pruebas. +- No utilices aserciones de tipo para ocultar datos obligatorios ausentes. +- Mantén los builders sencillos. +- No reproduzcas toda la lógica de producción dentro del builder. + +### Alternativa mediante factorías + +Para objetos sencillos, una función suele ser suficiente: + +```typescript id="mxnt4u" +function createTestUser( + overrides: Partial = {}, +): User { + return User.create({ + name: 'Test User', + email: 'test@example.com', + ...overrides, + }); +} +``` + +No introduzcas una clase Builder cuando una factoría de prueba clara resuelva adecuadamente el problema. + +--- + +## 10. Aislamiento y fiabilidad + +Una prueba debe producir resultados reproducibles bajo las mismas condiciones. + +### 10.1. Evitar estado compartido + +No permitas que una prueba dependa del orden de ejecución de otras. + +- Reinicia datos entre pruebas. +- Evita variables globales mutables compartidas. +- Limpia los recursos creados. +- Utiliza instancias independientes cuando corresponda. + +### 10.2. Controlar tiempo y aleatoriedad + +Cuando el comportamiento dependa de: + +- Fecha actual. +- Zona horaria. +- Temporizadores. +- Valores aleatorios. +- Identificadores generados. + +Utiliza mecanismos controlados para reproducir el escenario. + +No dependas del reloj real cuando pueda producir resultados inestables. + +### 10.3. Controlar concurrencia + +Las pruebas paralelas pueden interferir si comparten: + +- Base de datos. +- Archivos. +- Puertos. +- Variables globales. +- Recursos externos. + +Define mecanismos de aislamiento adecuados. + +### 10.4. Gestionar recursos + +Libera correctamente: + +- Conexiones. +- Servidores. +- Archivos temporales. +- Suscripciones. +- Temporizadores. +- Procesos externos. + +Utiliza los mecanismos de preparación y limpieza proporcionados por el framework de pruebas. + +### 10.5. Evitar pruebas inestables (Flaky Tests) + +Una prueba inestable puede pasar o fallar sin cambios relevantes en el código. + +Causas habituales: + +- Condiciones de carrera. +- Esperas temporales arbitrarias. +- Dependencias externas. +- Datos compartidos. +- Orden de ejecución. +- Operaciones asíncronas no esperadas. + +No soluciones la inestabilidad aumentando indiscriminadamente los tiempos de espera. + +Identifica y corrige la causa. + +**Una prueba inestable reduce la confianza en toda la batería de pruebas.** + +--- + +## 11. Pruebas asíncronas + +En aplicaciones TypeScript y Node.js, las pruebas deben esperar correctamente las operaciones asíncronas. + +### Ejemplo incorrecto + +```typescript id="f31do9" +it('saves the order', () => { + orderRepository.save(order); + + expect(orderRepository.count()).toBe(1); +}); +``` + +La operación puede continuar ejecutándose después de la aserción. + +### Alternativa + +```typescript id="3hz4v4" +it('saves the order', async () => { + await orderRepository.save(order); + + const count = await orderRepository.count(); + + expect(count).toBe(1); +}); +``` + +### Recomendaciones + +- Espera las promesas relevantes. +- No ignores rechazos. +- Evita temporizadores arbitrarios. +- Gestiona correctamente la limpieza asíncrona. +- Utiliza mecanismos de espera basados en condiciones cuando corresponda. +- Comprueba errores mediante aserciones apropiadas. + +Ejemplo: + +```typescript id="8wzl6r" +await expect( + orderRepository.save(invalidOrder), +).rejects.toThrow(InvalidOrderError); +``` + +--- + +## 12. Pruebas de regresión + +Cuando se corrige un defecto reproducible, conviene añadir una prueba que permita detectar su reaparición. + +### Procedimiento recomendado + +1. Reproduce el problema. +2. Identifica el comportamiento esperado. +3. Escribe una prueba que falle por el defecto, cuando sea viable. +4. Corrige la implementación. +5. Comprueba que la prueba pasa. +6. Ejecuta las verificaciones relacionadas. + +### Código legado + +Cuando no exista infraestructura adecuada: + +- Analiza el comportamiento actual. +- Utiliza pruebas de caracterización cuando resulte posible. +- Mantén los cambios acotados. +- Comprueba los contratos afectados. +- Añade pruebas de regresión cuando sea viable. + +No utilices la ausencia de pruebas como una razón automática para impedir correcciones necesarias. + +--- + +## 13. Cobertura de pruebas + +La cobertura indica qué partes del código han sido ejecutadas durante las pruebas. + +Puede ayudar a detectar áreas poco verificadas, pero no demuestra que los comportamientos sean correctos. + +### Consideraciones + +- Una cobertura elevada no garantiza pruebas de calidad. +- Una cobertura baja puede indicar riesgos relevantes. +- No escribas pruebas sin significado únicamente para aumentar porcentajes. +- Prioriza reglas de negocio, casos límite y procesos críticos. +- Evalúa la cobertura junto con la calidad de las aserciones y los escenarios. + +### Métricas habituales + +- Cobertura de líneas. +- Cobertura de funciones. +- Cobertura de ramas. +- Cobertura de instrucciones. + +Las métricas pueden utilizarse como indicadores de revisión. + +No establezcas umbrales universales sin considerar el tipo de sistema y los criterios específicos del proyecto. + +--- + +## 14. Errores habituales en las pruebas + +| Error | Problema | Recomendación | +|---|---|---| +| Probar detalles internos | Pruebas frágiles ante refactorizaciones correctas. | Verificar comportamientos observables. | +| Utilizar demasiados mocks | Las pruebas reproducen expectativas artificiales. | Utilizar objetos reales cuando corresponda. | +| Compartir estado | Resultados dependientes del orden de ejecución. | Aislar escenarios. | +| No incluir aserciones relevantes | Falsa confianza. | Verificar resultados significativos. | +| Probar únicamente código trivial | Bajo valor respecto al esfuerzo. | Priorizar lógica y riesgos. | +| Pruebas excesivamente lentas | Retroalimentación tardía. | Optimizar aislamiento y ejecución. | +| Dependencias externas inestables | Fallos no deterministas. | Utilizar entornos controlados. | +| Datos de producción | Riesgos de privacidad e integridad. | Utilizar datos sintéticos o preparados. | +| Modificar expectativas para hacer pasar pruebas | Ocultación de defectos. | Investigar el comportamiento correcto. | +| Eliminar pruebas que fallan | Pérdida de protección frente a regresiones. | Analizar y corregir la causa. | +| No esperar operaciones asíncronas | Resultados inconsistentes. | Utilizar correctamente `async/await`. | +| Aplicar una pirámide rígida | Pruebas inadecuadas para la arquitectura. | Ajustar la estrategia a los riesgos reales. | + +--- + +## 15. Disciplina de pruebas para agentes de IA + +Al modificar código o pruebas: + +- No cambies expectativas únicamente para conseguir que las pruebas pasen. +- No elimines pruebas que fallen sin analizar su causa. +- No desactives validaciones ni controles de seguridad para simplificar un escenario. +- No introduzcas mocks que oculten comportamientos incorrectos. +- No cambies herramientas de pruebas sin una necesidad justificada. +- Respeta las convenciones y frameworks existentes. +- Limita los cambios a los archivos relacionados con la tarea. +- Añade pruebas de regresión cuando corrijas defectos reproducibles y resulte viable. +- No inventes resultados de ejecución. +- No declares que una prueba ha pasado si no se ha ejecutado. + +### Cuando una prueba existente falla + +1. Comprueba si el fallo ya existía. +2. Analiza el comportamiento esperado. +3. Identifica si existe un defecto funcional o una expectativa obsoleta. +4. Corrige el código o la prueba según corresponda. +5. Justifica cualquier cambio contractual. +6. Ejecuta nuevamente las verificaciones pertinentes. + +### Cuando no pueden ejecutarse pruebas + +Indica: + +- Qué verificaciones no se ejecutaron. +- Por qué no pudieron ejecutarse. +- Qué otras comprobaciones se realizaron. +- Qué riesgos o incertidumbres permanecen. + +**La transparencia sobre las verificaciones realizadas forma parte de la calidad de la entrega.** + +--- + +## 16. Lista de comprobación de pruebas + +### Diseño + +- [ ] ¿Qué comportamiento necesito verificar? +- [ ] ¿Qué nivel de prueba resulta más apropiado? +- [ ] ¿La prueba aporta confianza real? +- [ ] ¿Estoy evitando comprobar detalles internos innecesarios? +- [ ] ¿El escenario representa un requisito o riesgo relevante? + +### Preparación + +- [ ] ¿Los datos de prueba son válidos y controlados? +- [ ] ¿Las dependencias están correctamente aisladas? +- [ ] ¿He evitado estado compartido? +- [ ] ¿Las operaciones asíncronas se esperan correctamente? +- [ ] ¿Los recursos se limpian al finalizar? + +### Verificación + +- [ ] ¿Las aserciones comprueban el comportamiento esperado? +- [ ] ¿Los casos límite relevantes están cubiertos? +- [ ] ¿Los errores esperados se verifican correctamente? +- [ ] ¿La prueba es determinista? +- [ ] ¿He evitado simulaciones innecesarias? + +### Integración + +- [ ] ¿Los contratos relevantes están comprobados? +- [ ] ¿Las integraciones críticas disponen de pruebas adecuadas? +- [ ] ¿Los datos y recursos externos están aislados? +- [ ] ¿Las implementaciones alternativas respetan los contratos? +- [ ] ¿Se han considerado transacciones y concurrencia cuando corresponde? + +### Finalización + +- [ ] ¿Se han ejecutado las pruebas relacionadas con los cambios? +- [ ] ¿Se han ejecutado las comprobaciones adicionales necesarias? +- [ ] ¿Las pruebas existentes siguen siendo válidas? +- [ ] ¿Se han incorporado pruebas de regresión cuando corresponde? +- [ ] ¿He comunicado las verificaciones no realizadas? +- [ ] ¿Puedo afirmar con precisión qué comportamiento se ha verificado? + +--- + +## Principio final + +> "Una buena prueba proporciona confianza sobre un comportamiento relevante y permite modificar el software sin introducir regresiones inesperadas." + +Utiliza pruebas unitarias, de integración, de contrato y E2E según las necesidades del sistema. + +Prioriza escenarios significativos, resultados observables y mecanismos reproducibles. + +No conviertas la cobertura, el número de pruebas o la estructura de la pirámide en objetivos independientes de la calidad del software. + +**La mejor estrategia de pruebas es la que proporciona confianza suficiente en los comportamientos importantes con un coste razonable de ejecución y mantenimiento.** \ No newline at end of file