6. Detectar baja cohesión y refactorizar para mejorar la estructura

La cohesión mide cuánta relación existe entre los elementos de una misma unidad de código. Cuando la cohesión cae, se vuelve difícil entender el propósito de la clase, aparecen bugs ocultos y el mantenimiento se torna riesgoso. Este tema propone una ruta para detectar la baja cohesión y refactorizar los componentes hasta recuperar un enfoque nítido y estable.

6.1 ¿Por qué es crucial identificar la baja cohesión?

Un componente con responsabilidades mezcladas suele tener métodos que solo comparten el nombre de la clase. Eso incrementa el acoplamiento accidental, reduce la capacidad de prueba y rompe el Principio de Responsabilidad Única del conjunto SOLID. Detectar temprano la baja cohesión evita refactorizaciones traumáticas y asegura que el diseño evolucione acompañando al dominio.

6.2 Indicadores visibles en clases y métodos

Algunos síntomas se pueden reconocer solo leyendo el código:

  • La clase se describe con más de una frase porque mezcla una historia de negocio, la persistencia de datos y la orquestación de eventos.
  • Los métodos acceden a subconjuntos disjuntos de atributos, lo que sugiere que existen clases escondidas.
  • Hay métodos públicos que no comparten ningún objetivo y obligan a dependencias innecesarias.
  • Se invocan recursos externos heterogéneos (colas, bases, APIs) desde la misma unidad sin un motivo claro.
  • Los nombres de la clase o de sus operaciones contienen palabras como “Manager”, “Helper”, “Utils” o “General”, revelando que la intención quedó indefinida.

6.3 Indicadores contextuales y de proceso

La baja cohesión también se detecta por el impacto en el trabajo diario:

  • Los cambios pequeños obligan a tocar muchos métodos y pruebas, generando regresiones inesperadas.
  • El tiempo de revisión aumenta porque el equipo necesita recordar excepciones y atajos de implementación.
  • La configuración de pruebas se vuelve compleja: hay que instanciar dependencias que no participan del escenario probado.
  • Surgen discusiones sobre el nombre del archivo o su ubicación en el paquete, signos de que el concepto no está claro.

6.4 Complementar la observación con métricas

Las métricas apoyan la intuición sin reemplazarla. Indicadores como Lack of Cohesion of Methods o el conteo de razones para cambiar ayudan a validar sospechas. Un LCOM alto implica que los métodos no comparten atributos, mientras que un registro de cambios fragmentado revela que la clase sirve a actores distintos del dominio. Las herramientas de análisis estático permiten monitorear tendencias y configurar alertas cuando se supera un umbral.

6.5 Caso práctico: una clase todo-terreno

La siguiente clase en Java representa un anti-ejemplo de cohesión. En apariencia resuelve la gestión de pedidos, pero combina logíca de negocio, persistencia y notificación.

class GestorPedido {
    private final DataSource dataSource;
    private final SmtpClient smtpClient;

    GestorPedido(DataSource dataSource, SmtpClient smtpClient) {
        this.dataSource = dataSource;
        this.smtpClient = smtpClient;
    }

    // Regla de negocio
    void aprobar(Pedido pedido) {
        if (pedido.total().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("El monto debe ser positivo");
        }
        pedido.marcarAprobado();
        guardar(pedido);
        notificarCliente(pedido);
    }

    // Persistencia
    void guardar(Pedido pedido) {
        try (Connection cn = dataSource.getConnection()) {
            PreparedStatement st = cn.prepareStatement(
                "UPDATE pedidos SET estado = ?, actualizado = ? WHERE id = ?");
            st.setString(1, pedido.estado().name());
            st.setTimestamp(2, Timestamp.from(Instant.now()));
            st.setLong(3, pedido.id());
            st.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("Error al persistir el pedido", e);
        }
    }

    // Comunicación
    void notificarCliente(Pedido pedido) {
        Email email = Email.from(pedido.emailCliente(),
            "Tu pedido " + pedido.id() + " fue aprobado",
            "Gracias por confiar en nosotros");
        smtpClient.enviar(email);
    }
}

El problema no es la longitud, sino la mezcla de motivos de cambio. Cualquier ajuste en la base de datos o en el canal de notificación obliga a editar la misma clase, generando un grafo de dependencias defectuoso.

6.6 Refactorizar por responsabilidades

Una refactorización efectiva persigue la especialización. El objetivo no es reducir líneas, sino separar colaboraciones que puedan evolucionar de manera independiente. Se pueden aplicar pasos graduales como Extract Method, Move Method y Extract Class.

class ServicioAprobacion {
    private final ValidadorPedido validador;
    private final RepositorioPedido repositorio;
    private final ServicioNotificacion notificador;

    ServicioAprobacion(ValidadorPedido validador,
                       RepositorioPedido repositorio,
                       ServicioNotificacion notificador) {
        this.validador = validador;
        this.repositorio = repositorio;
        this.notificador = notificador;
    }

    void aprobar(Pedido pedido) {
        validador.verificar(pedido);
        pedido.marcarAprobado();
        repositorio.guardar(pedido);
        notificador.notificarAprobacion(pedido);
    }
}

Las colaboraciones específicas encapsulan los detalles de cada dependencia:

class RepositorioPedido {
    private final DataSource dataSource;

    RepositorioPedido(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    void guardar(Pedido pedido) { /* Implementación enfocada en persistencia */ }
}

class ServicioNotificacion {
    private final SmtpClient smtpClient;

    ServicioNotificacion(SmtpClient smtpClient) {
        this.smtpClient = smtpClient;
    }

    void notificarAprobacion(Pedido pedido) { /* Enviar email */ }
}

El comando principal ahora delega en participantes cohesivos: las modificaciones en la persistencia o la notificación no arrastran al proceso de aprobación. A medida que el dominio crece se puede introducir una interfaz para abstraer la notificación y habilitar otras implementaciones como mensajes push o integraciones con Apache Kafka.

6.7 Plan de refactorización incremental

Para reorganizar un componente existente sin afectar la operación en producción conviene seguir un plan:

  • Escribir o actualizar pruebas de regresión que cubran los flujos sensibles. Marcos de prueba como JUnit ofrecen aserciones claras y extensión modular.
  • Aplicar refactorizaciones locales: mover métodos relacionados, renombrar para explicitar la intención y extraer objetos que representen conceptos del dominio.
  • Eliminar dependencias transitivas innecesarias. Si una clase solo pasaba parámetros para satisfacer a otra, ahora puede delegar directamente.
  • Revisar la configuración de inyección de dependencias o constructores para asegurarse de que cada colaborador se cree donde agrega valor.
  • Medir el resultado: comparar métricas antes y después, validar que la complejidad ciclomática y el LCOM mejoren.

6.8 Checklist rápido para mantener alta cohesión

Antes de cerrar una iteración, repasar las siguientes preguntas ayuda a preservar el equilibrio:

  • ¿Todos los métodos sirven al mismo actor o caso de uso del dominio?
  • ¿Cambiaría esta clase si se modifica un detalle de infraestructura? Si la respuesta es afirmativa, debería delegarlo.
  • ¿El nombre del componente describe su propósito sin enumerar acciones inconexas?
  • ¿Los métodos públicos pueden probarse con pocos datos ficticios porque dependen de un contexto acotado?
  • ¿La historia que cuenta la clase se entiende en una revisión de código de cinco minutos?

La cohesión elevada convierte a la arquitectura en una colección de piezas claras y confiables. Al monitorear los indicadores y refactorizar de manera incremental, el diseño se mantiene flexible para incorporar cambios sin sacrificar la estabilidad.