metis-desk/.agents/skills/rdx-solid/referencias/complejidad.md

28 KiB

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

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

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

// 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

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.

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

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.

async function persistValidatedOrder(order: Order): Promise<void> {
  validateOrder(order);
  calculateTaxes(order);
  await saveOrder(order);
}
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

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

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:

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.