Add testing strategy documentation for software verification practices

This commit is contained in:
David Arranz 2026-10-24 14:11:48 +02:00
parent fa9d95fa9e
commit 34b8f91538
15 changed files with 8906 additions and 0 deletions

View File

@ -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.

View File

@ -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<HttpResponse> {
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<void> {
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.

View File

@ -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.

View File

@ -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 (
<article>
<h3>{order.reference}</h3>
<button type="button" onClick={() => onSelect(order.id)}>
Ver pedido
</button>
</article>
);
}
```
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.

View File

@ -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<Customer | null>;
}
class LegacyCustomerAdapter implements CustomerDirectory {
constructor(private readonly legacyApi: LegacyCustomerApi) {}
async findCustomer(id: CustomerId): Promise<Customer | null> {
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.

View File

@ -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<typeof createOrderSchema>;
```
`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.

View File

@ -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.**

File diff suppressed because it is too large Load Diff

View File

@ -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<void> {
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<void> {
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<void> {
// Autenticación.
}
resetPassword(): Promise<void> {
// Recuperación de credenciales.
}
setTheme(theme: Theme): void {
// Preferencias.
}
sendEmail(message: string): Promise<void> {
// Notificaciones.
}
charge(amount: Money): Promise<void> {
// 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<void>;
rollback(): Promise<void>;
audit(): Promise<void>;
generateReport(): Promise<void>;
scheduleRecurring(): Promise<void>;
}
```
Una implementación puede verse obligada a declarar capacidades que no necesita o no admite.
#### Alternativa
```typescript
interface PaymentProcessor {
process(): Promise<void>;
}
```
```typescript
class StripePaymentProcessor implements PaymentProcessor {
async process(): Promise<void> {
// 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.**

View File

@ -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<User | null> {
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<void> {
validateOrder(order);
calculateTaxes(order);
await saveOrder(order);
}
async function processGuestOrder(order: Order): Promise<void> {
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<void> {
validateOrder(order);
calculateTaxes(order);
await saveOrder(order);
}
```
```typescript id="jds64w"
async function processUserOrder(order: Order): Promise<void> {
await persistValidatedOrder(order);
}
async function processGuestOrder(order: Order): Promise<void> {
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<void> {
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<void> {
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.**

View File

@ -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.**

File diff suppressed because it is too large Load Diff

View File

@ -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<void> {
// 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<void> {
// 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<User | null>;
save(user: User): Promise<void>;
}
```
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<void> {
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<void>;
}
```
El servicio depende del contrato:
```typescript id="5g7pvd"
class OrderService {
constructor(
private readonly emailService: EmailService,
) {}
async confirmOrder(email: string): Promise<void> {
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.**

View File

@ -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.**

File diff suppressed because it is too large Load Diff