Saltar a contenido

3.4.-Acoplamiento y cohesión

3.4 Acoplamiento y Cohesión en el Diseño de Software

Introducción

En el desarrollo de software, dos conceptos fundamentales que determinan la calidad del diseño son el acoplamiento y la cohesión. Estos principios nos ayudan a crear sistemas más mantenibles, comprensibles y flexibles.

Principio clave

Un buen diseño de software debe buscar bajo acoplamiento y alta cohesión.

Objetivos de un buen diseño: - Bajo acoplamiento: Minimizar las dependencias entre módulos - Alta cohesión: Maximizar la relación entre los elementos dentro de un mismo módulo

1. Cohesión

1.1. ¿Qué es la cohesión?

La cohesión es una medida que indica cuán relacionados están los elementos dentro de un mismo módulo, clase o componente. Una alta cohesión significa que los elementos del módulo están fuertemente relacionados y trabajan juntos para cumplir un propósito específico y bien definido.

1.2. Tipos de cohesión

La cohesión se puede clasificar en diferentes niveles, ordenados de menor a mayor calidad:

Nivel Tipo Calidad
1 Cohesión coincidental ❌ La peor
2 Cohesión lógica 🔴 Muy baja
3 Cohesión temporal 🟠 Baja
4 Cohesión procedimental 🟡 Media-baja
5 Cohesión comunicacional 🟢 Media
6 Cohesión secuencial 🔵 Alta
7 Cohesión funcional ✅ La mejor

1.2.1. Cohesión coincidental (la peor)

Los elementos están agrupados sin ninguna relación aparente. Es el nivel más bajo de cohesión.

Ejemplo:

class Utilidades {
    fun calcularIva(precio: Double): Double {
        return precio * 0.21
    }

    fun enviarEmail(destinatario: String, mensaje: String) {
        // código para enviar email
    }

    fun ordenarArray(array: List<Int>): List<Int> {
        return array.sorted()
    }
}

1.2.2. Cohesión lógica

Los elementos están relacionados porque realizan actividades del mismo tipo, aunque no necesariamente relacionadas.

Ejemplo:

class Operaciones {
    fun ejecutar(tipo: String, datos: List<Int>): Int {
        return when (tipo) {
            "suma" -> datos[0] + datos[1]
            "resta" -> datos[0] - datos[1]
            "multiplicacion" -> datos[0] * datos[1]
            else -> 0
        }
    }
}

1.2.3. Cohesión temporal

Los elementos están agrupados porque se ejecutan en el mismo momento temporal.

Ejemplo:

class Inicializador {
    fun inicializarSistema() {
        cargarConfiguracion()
        conectarBaseDatos()
        iniciarLogger()
        cargarCache()
    }

    private fun cargarConfiguracion() {}
    private fun conectarBaseDatos() {}
    private fun iniciarLogger() {}
    private fun cargarCache() {}
}

1.2.4. Cohesión procedimental

Los elementos están agrupados porque siguen una secuencia de pasos en un procedimiento.

Ejemplo:

class ProcesadorPedido {
    fun procesarPedido(pedido: Pedido) {
        validarStock(pedido)
        calcularTotal(pedido)
        aplicarDescuento(pedido)
        generarFactura(pedido)
    }

    private fun validarStock(pedido: Pedido) {}
    private fun calcularTotal(pedido: Pedido) {}
    private fun aplicarDescuento(pedido: Pedido) {}
    private fun generarFactura(pedido: Pedido) {}
}

1.2.5. Cohesión comunicacional

Los elementos trabajan sobre los mismos datos o contribuyen a la misma salida.

Ejemplo:

class InformeCliente {
    fun generarInforme(cliente: Cliente): String {
        val datos = obtenerDatosCliente(cliente)
        val estadisticas = calcularEstadisticas(datos)
        return formatearInforme(datos, estadisticas)
    }

    private fun obtenerDatosCliente(cliente: Cliente): DatosCliente = DatosCliente()
    private fun calcularEstadisticas(datos: DatosCliente): Estadisticas = Estadisticas()
    private fun formatearInforme(datos: DatosCliente, estadisticas: Estadisticas): String = ""
}

1.2.6. Cohesión secuencial

La salida de un elemento es la entrada del siguiente, formando una cadena de procesamiento.

Ejemplo:

class ProcesadorTexto {
    fun procesar(texto: String): String {
        val textoLimpio = eliminarEspacios(texto)
        val textoNormalizado = normalizar(textoLimpio)
        val textoValidado = validar(textoNormalizado)
        return textoValidado
    }

    private fun eliminarEspacios(texto: String): String = texto.trim()
    private fun normalizar(texto: String): String = texto.lowercase()
    private fun validar(texto: String): String = texto
}

1.2.7. Cohesión funcional (la mejor)

Todos los elementos del módulo están relacionados para cumplir una única función bien definida.

Ejemplo:

class CalculadoraImpuestos(private val tasaIva: Double) {

    fun calcularIva(baseImponible: Double): Double {
        return baseImponible * tasaIva
    }

    fun calcularTotalConIva(baseImponible: Double): Double {
        return baseImponible + calcularIva(baseImponible)
    }

    fun obtenerBaseDesdeTotal(totalConIva: Double): Double {
        return totalConIva / (1 + tasaIva)
    }
}

1.3. Ventajas de la alta cohesión

Beneficios principales:

  • Comprensibilidad: Es más fácil entender qué hace un módulo cuando todas sus partes están relacionadas
  • Mantenibilidad: Los cambios en un módulo cohesivo afectan a un área específica y bien definida
  • Reutilización: Los módulos cohesivos son más fáciles de reutilizar en otros contextos
  • Robustez: Menor probabilidad de efectos secundarios no deseados
  • Testabilidad: Más fácil de probar de forma independiente

2. Acoplamiento

2.1. ¿Qué es el acoplamiento?

El acoplamiento es una medida del grado de interdependencia entre módulos. Un bajo acoplamiento significa que los módulos son relativamente independientes entre sí, lo que facilita los cambios y el mantenimiento.

2.2. Tipos de acoplamiento

Ordenados de peor a mejor (de mayor a menor acoplamiento):

Nivel Tipo Fuerza Descripción
1 Acoplamiento de contenido ❌ Muy fuerte Modifica internos de otro módulo
2 Acoplamiento común 🔴 Fuerte Comparten datos globales
3 Acoplamiento externo 🟠 Medio-fuerte Dependen de formatos externos
4 Acoplamiento de control 🟡 Medio Controla el flujo de otro módulo
5 Acoplamiento de datos ✅ Débil Comunicación solo por parámetros

2.2.1. Acoplamiento de contenido (el peor)

Un módulo modifica o depende del funcionamiento interno de otro módulo.

Ejemplo (MAL):

class Cliente {
    var nombre: String = ""
    var saldo: Double = 0.0  // Debería ser privado
}

class GestorClientes {
    fun actualizarSaldo(cliente: Cliente, cantidad: Double) {
        // Accede directamente a los atributos internos
        cliente.saldo += cantidad  // Viola la encapsulación
    }
}

2.2.2. Acoplamiento común

Varios módulos comparten datos globales.

Ejemplo (MAL):

// Variable global (objeto companion o top-level)
object ConfiguracionGlobal {
    var modo: String = "produccion"
    var debug: Boolean = false
}

class ServicioA {
    fun procesar() {
        if (ConfiguracionGlobal.debug) {
            println("Modo debug activado")
        }
    }
}

class ServicioB {
    fun ejecutar() {
        if (ConfiguracionGlobal.modo == "produccion") {
            // hacer algo
        }
    }
}

2.2.3. Acoplamiento externo

Los módulos dependen de formatos externos impuestos (APIs, protocolos).

Ejemplo:

import java.net.URL

class ClienteAPI {
    fun obtenerDatos(): Map<String, Any> {
        // Acoplado al formato JSON de la API externa
        val response = URL("https://api.ejemplo.com/datos").readText()
        // Parsing JSON (requiere librería como Gson o kotlinx.serialization)
        return emptyMap()  // Simplificado para el ejemplo
    }
}

2.2.4. Acoplamiento de control

Un módulo controla el flujo de ejecución de otro pasándole información de control.

Ejemplo (MEJORABLE):

class Procesador {
    fun procesar(datos: List<String>, tipoProcesamiento: String): List<String> {
        return when (tipoProcesamiento) {
            "rapido" -> procesamientoRapido(datos)
            "completo" -> procesamientoCompleto(datos)
            else -> emptyList()
        }
    }

    private fun procesamientoRapido(datos: List<String>): List<String> = datos
    private fun procesamientoCompleto(datos: List<String>): List<String> = datos
}

2.2.5. Acoplamiento de datos (el mejor)

Los módulos se comunican solo mediante parámetros de datos simples.

Ejemplo (BIEN):

class CalculadoraDescuento {
    fun calcularDescuento(precioBase: Double, porcentaje: Double): Double {
        return precioBase * (porcentaje / 100)
    }
}

class CarritoCompra(private val calculadora: CalculadoraDescuento) {

    fun aplicarDescuento(precio: Double, descuento: Double): Double {
        return calculadora.calcularDescuento(precio, descuento)
    }
}

2.3. Consecuencias del alto acoplamiento

Problemas principales:

  • Efecto dominó: Un cambio en un módulo requiere cambios en otros módulos
  • Dificultad para probar: Es difícil probar módulos de forma aislada
  • Menor reutilización: Los módulos no pueden usarse independientemente
  • Mayor complejidad: El sistema se vuelve más difícil de entender y mantener
  • Rigidez: Dificulta la evolución y adaptación del sistema

2.4. Estrategias para reducir el acoplamiento

2.4.1. Uso de interfaces

interface ServicioNotificacion {
    fun enviar(destinatario: String, mensaje: String)
}

class EmailNotificacion : ServicioNotificacion {
    override fun enviar(destinatario: String, mensaje: String) {
        println("Enviando email a $destinatario: $mensaje")
    }
}

class SMSNotificacion : ServicioNotificacion {
    override fun enviar(destinatario: String, mensaje: String) {
        println("Enviando SMS a $destinatario: $mensaje")
    }
}

class GestorPedidos(private val notificador: ServicioNotificacion) {

    fun confirmarPedido(cliente: String) {
        // Bajo acoplamiento: no depende de una implementación específica
        notificador.enviar(cliente, "Pedido confirmado")
    }
}

2.4.2. Inyección de dependencias

class BaseDatos {
    fun guardar(datos: String) {
        println("Guardando en BD: $datos")
    }
}

class ServicioPedidos(private val bd: BaseDatos) {  // Dependencia inyectada

    fun crearPedido(pedido: String) {
        // Procesar pedido
        bd.guardar(pedido)
    }
}

// Uso
fun main() {
    val bd = BaseDatos()
    val servicio = ServicioPedidos(bd)  // Inyección de dependencia
    servicio.crearPedido("Pedido #123")
}

2.4.3. Patrón Facade

class SistemaFacturacion {
    fun generarFactura() {}
}

class SistemaInventario {
    fun actualizarStock() {}
}

class SistemaEnvios {
    fun programarEnvio() {}
}

/**
 * Simplifica la interacción con múltiples subsistemas
 */
class FachadaPedidos {
    private val facturacion = SistemaFacturacion()
    private val inventario = SistemaInventario()
    private val envios = SistemaEnvios()

    fun procesarPedidoCompleto(pedido: Pedido) {
        facturacion.generarFactura()
        inventario.actualizarStock()
        envios.programarEnvio()
    }
}

3. Relación entre Acoplamiento y Cohesión

3.1. El equilibrio ideal

El objetivo es lograr: - Alta cohesión: Cada módulo tiene una responsabilidad clara y única - Bajo acoplamiento: Los módulos son lo más independientes posible

3.2. Matriz de calidad del diseño

Cohesión Acoplamiento Bajo Acoplamiento Alto Acoplamiento
Alta Cohesión ✅ Excelente ⚠️ Bueno
Baja Cohesión ⚠️ Regular ❌ Malo

3.3. Ejemplo comparativo

Diseño MALO (baja cohesión, alto acoplamiento):

class Sistema {
    private val conexionBd = "mysql://localhost"

    fun procesarPedido(pedido: Pedido) {
        // Muchas responsabilidades mezcladas
        if (validarStock(pedido)) {
            calcularPrecio(pedido)
            actualizarBd(pedido)
            enviarEmail(pedido.cliente)
            generarFacturaPdf(pedido)
        }
    }

    private fun validarStock(pedido: Pedido): Boolean = true
    private fun calcularPrecio(pedido: Pedido) {}
    private fun actualizarBd(pedido: Pedido) {}
    private fun enviarEmail(cliente: String) {}
    private fun generarFacturaPdf(pedido: Pedido) {}
}

Diseño BUENO (alta cohesión, bajo acoplamiento):

class ValidadorStock {
    fun validar(pedido: Pedido): Boolean = true
}

class CalculadoraPrecios {
    fun calcular(pedido: Pedido): Double = 100.0
}

class RepositorioPedidos {
    fun guardar(pedido: Pedido) {}
}

class ServicioNotificaciones {
    fun notificarCliente(cliente: String, mensaje: String) {
        println("Notificando a $cliente: $mensaje")
    }
}

class GeneradorFacturas {
    fun generar(pedido: Pedido): ByteArray = ByteArray(0)
}

class ProcesadorPedidos(
    private val validador: ValidadorStock,
    private val calculadora: CalculadoraPrecios,
    private val repositorio: RepositorioPedidos,
    private val notificador: ServicioNotificaciones,
    private val generador: GeneradorFacturas
) {

    fun procesar(pedido: Pedido) {
        if (validador.validar(pedido)) {
            val precio = calculadora.calcular(pedido)
            repositorio.guardar(pedido)
            notificador.notificarCliente(
                pedido.cliente,
                "Pedido confirmado. Total: $precio"
            )
            generador.generar(pedido)
        }
    }
}

// Clases de datos necesarias
data class Pedido(val cliente: String, val items: List<String>)
data class Cliente(val nombre: String)
data class DatosCliente(val info: String = "")
data class Estadisticas(val total: Int = 0)

4. Principios de Diseño Relacionados

4.1. Principio de Responsabilidad Única (SRP)

Una clase debe tener una sola razón para cambiar. Este principio promueve la alta cohesión.

4.2. Principio de Inversión de Dependencias (DIP)

Depender de abstracciones, no de concreciones. Este principio reduce el acoplamiento.

4.3. Principio de Segregación de Interfaces (ISP)

Los clientes no deben depender de interfaces que no usan. Esto reduce el acoplamiento innecesario.

5. Métricas y Medición

5.1. Cómo medir la cohesión

LCOM (Lack of Cohesion of Methods)

Definición: Mide cuántos métodos de una clase utilizan los mismos atributos.

Interpretación: - LCOM bajo → ✅ Alta cohesión (bueno) - LCOM alto → ❌ Baja cohesión (malo)

Herramientas: - SonarQube - PMD - CodeClimate - IntelliJ IDEA Inspector

5.2. Cómo medir el acoplamiento

Métricas principales:

Métrica Nombre Descripción
Ca Acoplamiento aferente Número de clases externas que dependen de esta clase
Ce Acoplamiento eferente Número de clases externas de las que depende esta clase
I Inestabilidad I = Ce / (Ca + Ce), valores cercanos a 0 indican estabilidad

Interpretación de Inestabilidad (I): - I ≈ 0: Clase estable (muchas clases dependen de ella, depende de pocas) - I ≈ 1: Clase inestable (pocas clases dependen de ella, depende de muchas)

Objetivo: Clases de bajo nivel (utilidades, frameworks) deben tener I cercano a 0.

6. Conclusiones

Puntos clave para recordar:

  • ✅ La alta cohesión y el bajo acoplamiento son pilares fundamentales del buen diseño de software
  • ✅ Un diseño con alta cohesión hace que cada módulo tenga un propósito claro y único
  • ✅ Un diseño con bajo acoplamiento facilita el mantenimiento, las pruebas y la evolución del sistema
  • ✅ Aplicar estos principios desde el inicio del desarrollo ahorra tiempo y esfuerzo a largo plazo
  • ✅ Las refactorizaciones periódicas ayudan a mantener estos principios a lo largo del ciclo de vida del software

Matriz de calidad del diseño (recordatorio):

Cohesión Acoplamiento Bajo Acoplamiento Alto Acoplamiento
Alta Cohesión ✅ Excelente ⚠️ Bueno
Baja Cohesión ⚠️ Regular ❌ Malo

7. Recursos y Referencias

Libros recomendados

Artículos y recursos online

Herramientas de análisis

  • SonarQube: Análisis continuo de calidad de código
  • IntelliJ IDEA: Inspecciones de código integradas
  • PMD: Detector de problemas de código
  • CodeClimate: Análisis de mantenibilidad

Próximos temas: - Principios SOLID - Patrones de Diseño - Refactorización - Arquitectura de Software