Diseño de software

[Modularización #1] ¿Qué es modularizar? (cohesión y acoplamiento)

Cuanto más crece el proyecto, más miedo da modificar un solo archivo. Es difícil saber hasta dónde se propagará el impacto.

5 min de lectura
Imagen de portada de [Modularización #1] ¿Qué es modularizar? (cohesión y acoplamiento)

Cuanto más crece el proyecto, más miedo da modificar un solo archivo. Es difícil saber hasta dónde se propagará el impacto.

Las compilaciones se ralentizan y el alcance de las revisiones de código se amplía. Los nuevos miembros del equipo tampoco saben por dónde empezar a leer.

La modularización es la solución más antigua para este problema. En una frase: consiste en dividir grandes bloques de código en unidades que puedan entenderse y reemplazarse de forma independiente.

En este artículo resumimos qué divide exactamente la modularización, los criterios de un buen módulo —alta cohesión y bajo acoplamiento— y cuándo conviene dividirlo.

Empecemos por el resumen.

  1. La modularización agrupa el código que «cambia junto» y establece límites.
  2. Un buen módulo tiene alta cohesión y bajo acoplamiento.
  3. El objetivo principal es reducir el coste de los cambios, no acelerar la compilación.
  4. Dividir sin límites claros solo aumenta la complejidad.

¿Qué divide exactamente la modularización?

Organizar carpetas y dividir módulos no es lo mismo.

Una carpeta solo organiza archivos. El código de cualquier carpeta puede usar libremente el de otra.

Un módulo añade un límite obligatorio. Desde fuera solo se puede usar la interfaz que el módulo declara como pública (public).

La esencia de la modularización no es ordenar archivos, sino hacer que el compilador imponga qué debe conocerse desde fuera y qué puede permanecer oculto.

Este límite permite dos cosas.

  • Comprensión independiente: puede usarse mirando solo la interfaz, sin conocer el interior del módulo.
  • Reemplazo independiente: si la interfaz es la misma, se puede cambiar toda la implementación sin afectar al exterior.

En 1972, David Parnas presentó en un artículo el concepto de ocultación de información (information hiding). El criterio para dividir módulos no son las funcionalidades, sino las «decisiones de diseño que queremos ocultar».


Criterios de un buen módulo: cohesión y acoplamiento

Si dividir módulos los hizo menos manejables, normalmente estas dos métricas están desequilibradas.

La cohesión indica cuánto se relaciona el código dentro de un módulo. Cuanto mayor, mejor.

El acoplamiento indica cuánto dependen entre sí los módulos. Cuanto menor, mejor.

Hay una forma sencilla de evaluarlo.

Pregunta Señal
¿Hay que modificar varios módulos a la vez para corregir una funcionalidad? Acoplamiento alto
¿Hay código sin relación mezclado dentro del módulo? Cohesión baja
¿Al eliminar un módulo desaparece exactamente esa funcionalidad? Está bien dividido

El criterio clave es el cambio. El código que cambia junto debe estar en un módulo; el que cambia por otros motivos, en otro.

Es un principio conocido, ¿verdad? La «razón del cambio» del principio de responsabilidad única (SRP) de SOLID se amplía al nivel de los módulos.

Fuera del módulo solo se muestra la interfaz pública.
Fuera del módulo solo se muestra la interfaz pública.

¿Qué ventajas ofrece modularizar?

El coste de los cambios es lo primero que disminuye.

Con límites claros, el impacto de una modificación queda confinado al módulo. El revisor puede concluir que el exterior es seguro si la interfaz pública del módulo no cambió.

También facilita el trabajo dividido por equipos. Asignar responsables por módulo reduce los conflictos por invadir las áreas de trabajo ajenas.

Las pruebas también se vuelven más ligeras. Se puede extraer y probar un solo módulo sin iniciar toda la aplicación.

La velocidad de compilación también mejora: solo hay que recompilar el módulo modificado. Pero esto es una consecuencia, no el objetivo. Si los límites están mal diseñados, habrá que recompilarlo todo cada vez.

En Swift, los límites se manifiestan mediante los modificadores de acceso.

// Solo se exponen protocolos fuera del módulo
public protocol PriceFormatter {
    func format(_ amount: Int) -> String
}

// La implementación se oculta dentro del módulo
final class KoreanPriceFormatter: PriceFormatter {
    func format(_ amount: Int) -> String { "\(amount)or" }
}

Desde fuera basta con conocer PriceFormatter. Lo ideal es que, aunque cambie la implementación interna, el código externo al módulo ni siquiera necesite recompilarse.

Los límites se dibujan primero en una pizarra, antes que en el código.
Los límites se dibujan primero en una pizarra, antes que en el código.

¿Cuándo dividir y cuándo esperar?

La modularización no es gratuita. Crear límites implica costes de diseño de interfaces, gestión de versiones y configuración del proyecto.

Situación Criterio
Una modificación afecta a código de varios equipos o áreas Es hora de dividir
La compilación lenta interrumpe con frecuencia el flujo de desarrollo Divide, pero diseña primero los límites de cambio
Producto inicial cuyos límites de dominio aún cambian con frecuencia Espera hasta que los límites se estabilicen
Aplicación pequeña desarrollada por una sola persona Bastan la organización por carpetas y los modificadores de acceso

Modularizar con límites incorrectos es peor que no hacerlo. Si dos módulos siempre cambian juntos, el límite es incorrecto. Lo adecuado es fusionarlos.

Así se pregunta en una entrevista

P. Explique la cohesión y el acoplamiento, y describa su relación.

La cohesión es la relación entre los elementos internos de un módulo; el acoplamiento, el grado de dependencia entre módulos. Un buen diseño busca alta cohesión y bajo acoplamiento: al reunir el código relacionado, disminuye naturalmente lo que se intercambia con el exterior, por lo que el acoplamiento baja. Es una relación complementaria.

P. ¿Qué criterio usaría para dividir módulos?

Usaría la razón del cambio. El código que cambia junto por el mismo motivo debe estar en un módulo, y el que cambia por motivos distintos debe separarse. El límite es correcto cuando puede responderse qué decisión de diseño oculta el módulo, no solo enumerar sus funcionalidades.


En el próximo artículo abordaremos los problemas inevitables después de dividir módulos: la dirección de las dependencias entre ellos y las dependencias circulares.

Diseñar las relaciones después de dividir es realmente difícil. Con la intuición sobre cohesión y acoplamiento de este artículo, será mucho más sencillo.

Artículos recomendados