Cuando construimos software, la diferencia entre un sistema que evoluciona con facilidad y otro que se convierte en un lastre rara vez está en el algoritmo concreto que escribimos. Está en cómo organizamos las piezas y en cómo dependen unas de otras. El acoplamiento y la cohesión son las dos métricas conceptuales más importantes para razonar sobre esa organización: nos dicen cuán entrelazados están nuestros módulos y cuán enfocada está cada pieza en una única tarea. Dominar estos conceptos es el primer paso para tomar decisiones arquitectónicas conscientes en lugar de acumular complejidad accidental. En esta lección los estudiaremos en profundidad, veremos cómo se manifiestan en código Java real y cómo refactorizar para mejorarlos, terminando con una regla práctica fundamental: la Ley de Demeter.

Contenido

  1. Acoplamiento: qué es y por qué importa
  2. Tipos de acoplamiento (de peor a mejor)
  3. Cohesión: alta frente a baja
  4. La relación entre acoplamiento y cohesión
  5. Separación de responsabilidades (SoC)
  6. Refactorización guiada: de alto acoplamiento a bajo acoplamiento
  7. La Ley de Demeter (principio de mínimo conocimiento)
  8. Errores comunes y consejos
  9. Ejercicios
  10. Conclusión

  1. Acoplamiento: qué es y por qué importa

El acoplamiento mide el grado de interdependencia entre dos módulos: cuánto necesita conocer un módulo sobre los detalles internos de otro para funcionar. Cuando dos componentes están fuertemente acoplados, un cambio en uno obliga a cambiar el otro.

Conceptos clave:

  • Bajo acoplamiento (deseable): los módulos se comunican a través de interfaces estables y mínimas. Un cambio interno en un módulo no afecta a los demás.
  • Alto acoplamiento (problemático): los módulos dependen de detalles internos, tipos concretos o estructuras de datos de otros módulos.
  • El acoplamiento nunca puede ser cero: si los módulos no se comunicaran, no formarían un sistema. El objetivo es minimizarlo y hacerlo explícito.

Consecuencias del alto acoplamiento:

  • Fragilidad: cambios localizados provocan fallos en cascada en lugares inesperados.
  • Rigidez: es difícil modificar el sistema porque cada cambio arrastra a otros.
  • Baja reutilización: no se puede extraer un módulo sin arrastrar sus dependencias.
  • Dificultad para testear: no se puede probar un módulo de forma aislada.

  1. Tipos de acoplamiento (de peor a mejor)

Históricamente (Stevens, Myers y Constantine, 1974) el acoplamiento se clasifica en una escala. Esta tabla los ordena del más dañino al más sano:

Tipo Descripción Valoración
De contenido Un módulo modifica o depende de los datos internos de otro Muy malo
Común Varios módulos comparten estado global mutable Malo
Externo Dependencia de un formato o protocolo externo impuesto Malo
De control Un módulo controla el flujo de otro pasándole banderas Regular
De marca (stamp) Se pasa una estructura completa cuando solo se necesita parte Mejorable
De datos Se pasan solo los datos estrictamente necesarios Bueno
De mensaje Comunicación solo mediante mensajes/interfaces sin parámetros internos Óptimo

Ejemplo de acoplamiento de control (uno de los más frecuentes y evitables):

// MAL: el llamante controla el flujo interno mediante una bandera
public class GeneradorInformes {
    public String generar(Datos datos, boolean esPdf) {
        if (esPdf) {
            return generarPdf(datos);
        } else {
            return generarHtml(datos);
        }
    }
}

Explicación del problema: el parámetro boolean esPdf es una bandera de control. El código que llama tiene que conocer la lógica interna del método para saber qué hace cada valor. Además, cada nuevo formato (CSV, XML) obliga a añadir parámetros o ramas, rompiendo el método existente.

// BIEN: acoplamiento de datos/mensaje mediante polimorfismo
public interface GeneradorInformes {
    String generar(Datos datos);
}

public class GeneradorPdf implements GeneradorInformes {
    public String generar(Datos datos) { /* ... */ return "pdf"; }
}

public class GeneradorHtml implements GeneradorInformes {
    public String generar(Datos datos) { /* ... */ return "html"; }
}

Explicación de la mejora: ahora el llamante elige una implementación concreta y la usa a través de la interfaz GeneradorInformes. No conoce los detalles internos. Añadir un nuevo formato es crear una nueva clase, sin tocar las existentes.

  1. Cohesión: alta frente a baja

La cohesión mide cuán relacionados y enfocados están los elementos dentro de un mismo módulo. Una clase con alta cohesión hace una sola cosa bien; una con baja cohesión mezcla responsabilidades dispares.

Tipos de cohesión (de peor a mejor):

Tipo Qué agrupa Valoración
Coincidental Elementos sin relación, agrupados por azar Muy mala
Lógica Tareas similares por categoría pero distintas en propósito Mala
Temporal Cosas que se ejecutan en el mismo momento (p. ej. arranque) Regular
Procedimental Pasos de un procedimiento, en cierto orden Mejorable
Comunicacional Operaciones sobre los mismos datos Buena
Funcional Todo contribuye a una única tarea bien definida Óptima

Ejemplo de baja cohesión:

// MAL: una clase que hace de todo (clase "Dios")
public class Utilidades {
    public void guardarUsuario(Usuario u) { /* acceso a BD */ }
    public String formatearFecha(Date d) { /* formato */ return ""; }
    public void enviarEmail(String destino) { /* SMTP */ }
    public double calcularImpuesto(double base) { return base * 0.21; }
}

Explicación del problema: esta clase agrupa acceso a base de datos, formateo, envío de correo y cálculo fiscal. No hay ninguna relación entre estas tareas: es cohesión coincidental. Cualquier desarrollador que necesite una sola de estas funciones queda acoplado a toda la clase.

// BIEN: cada clase tiene una responsabilidad única (cohesión funcional)
public class RepositorioUsuarios {
    public void guardar(Usuario u) { /* acceso a BD */ }
}

public class ServicioEmail {
    public void enviar(String destino, String cuerpo) { /* SMTP */ }
}

public class CalculadoraImpuestos {
    public double calcular(double base) { return base * 0.21; }
}

  1. La relación entre acoplamiento y cohesión

Estas dos métricas suelen moverse en direcciones opuestas y constituyen el principio rector del buen diseño modular:

Objetivo: alta cohesión interna y bajo acoplamiento externo.

graph LR
    subgraph "Diseño deficiente"
        A1[Módulo A] <--> B1[Módulo B]
        A1 <--> C1[Módulo C]
        B1 <--> C1
    end
    subgraph "Buen diseño"
        A2[Módulo A] --> I[Interfaz]
        B2[Módulo B] --> I
        C2[Módulo C] --> I
    end

Explicación del diagrama: a la izquierda, todos los módulos se conocen entre sí directamente (red densa = alto acoplamiento). A la derecha, los módulos solo dependen de una interfaz estable: las dependencias son pocas y dirigidas. Si un módulo cambia internamente, mientras respete la interfaz, los demás no se enteran.

  1. Separación de responsabilidades (SoC)

La separación de responsabilidades (Separation of Concerns) es el principio que sustenta tanto la alta cohesión como el bajo acoplamiento: cada parte del sistema debe ocuparse de un único aspecto ("concern") y nada más.

Aspectos típicos que conviene separar:

  • Presentación (interfaz de usuario, serialización de respuestas).
  • Lógica de negocio (reglas del dominio).
  • Persistencia (acceso a datos).
  • Aspectos transversales (logging, seguridad, transacciones).

Un patrón clásico de SoC es la arquitectura en capas:

// Capa de presentación: solo orquesta y traduce HTTP <-> dominio
@RestController
public class PedidoController {
    private final ServicioPedidos servicio;
    public PedidoController(ServicioPedidos servicio) { this.servicio = servicio; }

    @PostMapping("/pedidos")
    public RespuestaPedido crear(@RequestBody PeticionPedido peticion) {
        Pedido pedido = servicio.crearPedido(peticion.getItems());
        return RespuestaPedido.desde(pedido);
    }
}

// Capa de negocio: reglas del dominio, sin saber nada de HTTP ni de SQL
public class ServicioPedidos {
    private final RepositorioPedidos repositorio;
    public ServicioPedidos(RepositorioPedidos repositorio) { this.repositorio = repositorio; }

    public Pedido crearPedido(List<Item> items) {
        Pedido pedido = new Pedido(items);
        pedido.validar();
        return repositorio.guardar(pedido);
    }
}

Explicación: el PedidoController solo conoce el protocolo web y delega de inmediato. El ServicioPedidos contiene las reglas y no sabe si lo invoca un controlador REST, una cola de mensajes o un test. Cada capa puede evolucionar y probarse de forma independiente.

  1. Refactorización guiada: de alto acoplamiento a bajo acoplamiento

Veamos una refactorización completa. Partimos de una clase que crea sus propias dependencias internamente:

// ANTES: acoplamiento fuerte a implementaciones concretas
public class ServicioNotificaciones {
    private final ClienteSmtp smtp = new ClienteSmtp("smtp.empresa.com");

    public void notificar(String usuario, String mensaje) {
        String email = new RepositorioUsuariosMySql().buscarEmail(usuario);
        smtp.enviar(email, mensaje);
    }
}

Problemas detectados:

  • Crea ClienteSmtp y RepositorioUsuariosMySql con new: imposible sustituirlos por dobles de prueba.
  • Depende de clases concretas, no de abstracciones.
  • No se puede testear sin un servidor SMTP y una base de datos reales.
// DESPUÉS: inyección de dependencias contra interfaces
public interface PasarelaEmail {
    void enviar(String destino, String cuerpo);
}

public interface RepositorioUsuarios {
    String buscarEmail(String usuario);
}

public class ServicioNotificaciones {
    private final PasarelaEmail email;
    private final RepositorioUsuarios usuarios;

    // Las dependencias se reciben desde fuera (inversión de control)
    public ServicioNotificaciones(PasarelaEmail email, RepositorioUsuarios usuarios) {
        this.email = email;
        this.usuarios = usuarios;
    }

    public void notificar(String usuario, String mensaje) {
        String destino = usuarios.buscarEmail(usuario);
        email.enviar(destino, mensaje);
    }
}

Explicación de la mejora paso a paso:

  1. Definimos interfaces (PasarelaEmail, RepositorioUsuarios) que describen qué se necesita, no cómo se implementa.
  2. El servicio recibe sus colaboradores por el constructor (inyección de dependencias). Ya no usa new.
  3. En producción inyectaremos las implementaciones reales; en los tests, dobles ligeros. Esto reduce el acoplamiento a su forma de mensaje y permite el testeo aislado.

  1. La Ley de Demeter (principio de mínimo conocimiento)

La Ley de Demeter es una regla heurística que limita el acoplamiento: "habla solo con tus amigos cercanos, no con extraños". Un método de un objeto solo debería invocar métodos de:

  • el propio objeto (this),
  • objetos que recibe como parámetros,
  • objetos que crea él mismo,
  • sus atributos directos.

El síntoma de violación más visible es el encadenamiento de llamadas (a.getB().getC().hacer()), también llamado "tren de llamadas".

// VIOLA la Ley de Demeter: navegamos por la estructura interna de otros objetos
public class CalculadoraDescuento {
    public double calcular(Pedido pedido) {
        String pais = pedido.getCliente().getDireccion().getPais();
        if (pais.equals("ES")) return 0.10;
        return 0.0;
    }
}

Explicación del problema: CalculadoraDescuento conoce la estructura interna de Pedido, de Cliente y de Direccion. Si cualquiera de esas clases cambia su estructura, este método se rompe. El acoplamiento es transitivo y oculto.

// CUMPLE la Ley de Demeter: cada objeto expone lo que necesita
public class Pedido {
    private final Cliente cliente;
    public boolean esNacional() { return cliente.esNacional(); }
}

public class Cliente {
    private final Direccion direccion;
    public boolean esNacional() { return direccion.esEnPais("ES"); }
}

public class CalculadoraDescuento {
    public double calcular(Pedido pedido) {
        return pedido.esNacional() ? 0.10 : 0.0;
    }
}

Explicación de la mejora: en lugar de pedir datos y decidir fuera ("ask"), le decimos al objeto que haga la pregunta de negocio que le compete ("tell"). Cada clase oculta su estructura interna. Esto se conoce como principio "Tell, Don't Ask" y es la cara práctica de la Ley de Demeter.

Matiz importante: la Ley de Demeter aplica a objetos con comportamiento de dominio. No debe aplicarse de forma dogmática a APIs fluidas o builders (new Builder().con(x).con(y).build()), donde el encadenamiento es intencional y cada método devuelve el mismo tipo.

Errores Comunes y Consejos

  • Confundir pocas líneas con alta cohesión. Una clase pequeña puede seguir siendo incoherente si mezcla aspectos. La cohesión es semántica, no de tamaño.
  • Crear interfaces para todo "por si acaso". Una abstracción innecesaria es complejidad accidental. Introduce interfaces cuando exista una variación real o una frontera de testeo.
  • Esconder el acoplamiento en estado global o singletons. El acoplamiento común (estado compartido mutable) es de los más difíciles de depurar; prefiere pasar dependencias explícitas.
  • Aplicar la Ley de Demeter a estructuras de datos puras (DTO). Acceder a campos de un DTO sin lógica no la viola; la ley protege objetos con comportamiento.
  • Consejo: ante un cambio, observa cuántos ficheros tienes que tocar. Si son muchos para un cambio conceptualmente pequeño, tienes un problema de acoplamiento o cohesión.
  • Consejo: usa la regla "Tell, Don't Ask" como detector rápido de violaciones de Demeter.

Ejercicios

Ejercicio 1. Identifica el tipo de acoplamiento y refactoriza:

public class Calculadora {
    public double operar(double a, double b, int tipo) {
        if (tipo == 1) return a + b;
        if (tipo == 2) return a - b;
        if (tipo == 3) return a * b;
        return 0;
    }
}

Ejercicio 2. La siguiente clase tiene baja cohesión. Sepárala en clases con responsabilidad única:

public class GestorTienda {
    public void procesarPago(double importe) { /* ... */ }
    public void actualizarInventario(String producto, int cantidad) { /* ... */ }
    public void registrarLog(String mensaje) { /* ... */ }
}

Ejercicio 3. Reescribe este método para que cumpla la Ley de Demeter:

public double precioConEnvio(Factura factura) {
    return factura.getPedido().getTotal() + factura.getPedido().getEnvio().getCoste();
}

Soluciones

Solución 1. Es acoplamiento de control (el int tipo dirige el flujo). Refactorizamos a polimorfismo:

public interface Operacion {
    double aplicar(double a, double b);
}
public class Suma implements Operacion {
    public double aplicar(double a, double b) { return a + b; }
}
public class Resta implements Operacion {
    public double aplicar(double a, double b) { return a - b; }
}
public class Multiplicacion implements Operacion {
    public double aplicar(double a, double b) { return a * b; }
}
// Uso: Operacion op = new Suma(); double r = op.aplicar(2, 3);

Ahora añadir operaciones no requiere modificar código existente y desaparecen las banderas.

Solución 2. Separamos en tres clases con cohesión funcional:

public class ServicioPagos {
    public void procesarPago(double importe) { /* ... */ }
}
public class ServicioInventario {
    public void actualizar(String producto, int cantidad) { /* ... */ }
}
public class ServicioRegistro {
    public void registrar(String mensaje) { /* ... */ }
}

Cada servicio puede evolucionar, probarse y reutilizarse de forma independiente.

Solución 3. Encapsulamos el cálculo dentro de los objetos que poseen los datos:

public class Pedido {
    private double total;
    private Envio envio;
    public double precioTotalConEnvio() { return total + envio.getCoste(); }
}
public double precioConEnvio(Factura factura) {
    return factura.precioTotalConEnvio(); // Factura delega en su Pedido
}

El llamante ya no navega por la estructura interna; pide directamente el dato de negocio.

Conclusión

En esta lección hemos visto que el buen diseño se reduce, en buena medida, a una idea: maximizar la cohesión y minimizar el acoplamiento. Hemos clasificado los tipos de acoplamiento y cohesión, entendido que la separación de responsabilidades es el principio que los habilita, y practicado refactorizaciones reales usando interfaces e inyección de dependencias. Por último, la Ley de Demeter nos ha dado una regla operativa para detectar dependencias ocultas. Estos conceptos son la base sobre la que se construyen los principios SOLID, que estudiaremos en la siguiente lección y que no son más que formulaciones concretas y accionables de estas mismas ideas.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados