Acoplamiento ¿Cómo se produce?

Diferentes formas en que aumentamos el acoplamiento — a veces sin ser conscientes de ello

Eduardo Betanzos
11 min readNov 25, 2021

El término acoplamiento es ampliamente conocido en el mundo del desarrollo e ingeniería de software. Seguramente todos hemos escuchado la regla de alta cohesión y bajo acoplamiento en algún punto de nuestra carrera. Yo tuve la suerte de conocerla muy temprano cuando estudiaba Ingeniería Informática — allá por el año 2006 — y la desdicha de no comprenderla hasta varios años más tarde.

En este artículo analizaremos algunas formas en las que podemos aumentar el acoplamiento en nuestros sistemas, muchas veces sin darnos cuenta, y así contar con herramientas que nos permitan saber cómo mitigar este fenómeno.

¿Qué es?

¿A qué nos referimos cuando hablamos de acoplamiento? Si tuviera que dar una mi definición diría que:

El acoplamiento es la medida de dependencia de un módulo respecto a otros módulos dentro de un sistema software.

Quizá resulte una definición un tanto confusa a la primera lectura, pero si nos detenemos un minuto a pensar en ello, y la volvemos a leer, notaremos que en realidad es muy simple: qué tanto depende un módulo de otros.

Por módulo podríamos entender cualquier porción de un programa empaquetada cohesivamente — por lo menos a eso aspiramos cuando aplicamos los principios básicos de diseño de software — como una unidad funcional: funciones/métodos, clases, paquetes, bibliotecas de clases, componentes desplegables de un sistema distribuido, etc.

En su libro Structured Design [1] primera edición 1975 —, Larry Constantine — quien desarrolló los conceptos de cohesión y acoplamiento — y Edward Yourdon nos brindan un amplísimo tratado teórico sobre, entre muchas otras cosas, el acoplamiento. Ellos lo definen como:

«…es una medición de la fuerza de la interconexión. »— Structured Design [1, p 76]

refiriéndose a módulos.

El problema del acoplamiento

En términos simples, el acoplamiento está estrechamente relacionado con la complejidad.

Los seres humanos tenemos una muy baja capacidad cuantitativa, por lo que mientras más conceptos, o elementos, tengamos que manejar para comprender y solucionar un problema, la probabilidad de cometer errores aumentará de modo exponencial.

Según el psicólogo norteamericano George Miller [2], el número mágico [3] que cuantifica nuestra capacidad promedio de procesamiento es 7 ± 2.

Figura 1: Curva de error para la resolución normal de problemas — fuente: Structured Design [1, p 64].

Entonces ¿cómo podemos lidiar con estas limitaciones? Lo que resulta más obvio es partir la solución en unidades más manejables: módulos. Pero estos módulos deben — generalmente — comunicarse unos con otros para que el nuevo sistema modularizado pueda realizar las mismas tareas que realizaba cuando estaba compuesto de solo un gran módulo.

¡Todo luce bien! Tenemos una solución dividida en partes más pequeñas y menos complejas de manejar. Pero ahora tenemos una nueva fuente de error: las conexiones entre los módulos.

Si estas conexiones aumentan en número y/o complejidad — y ya vimos que para nosotros hay una relación directa entre cantidad y complejidad — para lograr entender el funcionamiento de un módulo A, también será necesario entender el funcionamiento de todos aquellos módulos con los que A está conectado y, por ende, de los cuales depende para realizar su trabajo. En otras palabras, estaríamos volviendo al punto de partida: el sistema es muy complejo y difícil de manejar.

Cómo se produce

Para analizar cómo se produce el acoplamiento vamos a tomar como punto de partida el trabajo de Constantine y Yourdon en Structured Design [1], en el que definen los principales factores que influencian este acoplamiento.

Estos son, ordenados de mayor a menor de acuerdo con su grado de impacto:

  1. Tipo de conexión entre módulos
  2. Complejidad de la interfaz
  3. Tipo de flujo de información sobre la conexión
  4. Momento de enlace [binding time] de la conexión

Para este artículo solo abordaré escenarios relacionados a los dos primeros puntos. Quizá más adelante analice el resto de los puntos — y otros temas relacionados con el acoplamiento — en otro artículo.

1. Tipo de conexión entre módulos

Al hablar del tipo de conexión Constantine y Yourdon se refieren principalmente a la cantidad de estas — dentro del sistema —, pero también a su variedad.

Sin entrar en muchos detalles teóricos — básicamente porque en el libro están bastante bien explicados, aunque nos son tan simples de entender — podríamos decir que el escenario más deseable es aquel en que todas las conexiones dentro de un sistema se hacen a la única interfaz de cualquier módulo y el menos deseable sería cuando estas conexiones se hacen a un gran número de interfaces de cada módulo.

Esto está justificado con el hecho de que:

  1. la complejidad de usar un módulo aumenta en la medida en que este requiera que sus clientes usen y conozcan una cantidad elevada de interfaces para lograr obtener el beneficio deseado; y
  2. la complejidad de comprender el funcionamiento interno de un módulo — desde el punto de vista de su programador — aumenta en la medida en que este tiene un elevado número de conexiones que serán necesarias entender, también.

Variables de instancia o de clase

Mi lenguaje de programación principal es Java, el que, como seguramente todos sabrán, es un lenguaje orientado a objetos basado en clases, por lo que quizá algunos de los ejemplos que ponga no apliquen tal cual a tú lenguaje, pero de seguro podrás extrapolarlos haciendo algún ajuste.

Cada vez que hacemos uso de un identificador público externo como tipo de dato de alguno de los atributos de nuestra clase — sean de tipo static o no — estaremos creando una nueva dependencia.

Código 1: Acoplamiento por atributos de la clase.

En el ejemplo del Código 1 vemos que la clase Game dependería de las clases Board, Player, Console y List aunque el caso de List no nos importa mucho puesto que pertenece al core del lenguaje — lo que se conoce como API estándar. Luego veremos como cualquier tipo de dato, aunque pertenezca al API estándar del lenguaje, sí importa desde otra perspectiva.

En mi opinion, las dependencias al API estándar de los lenguajes no son problemáticas puesto que estas generalmente son muy estables. Por tal motivo yo no las tomo tanto en cuenta a la hora de analizar el acoplamiento de una clase — a no ser que la cantidad de dependencias sea considerablemente grande y entonces me plantee problemas de cohesión.

Parámetros a métodos (incluyendo constructores)

Otra fuente clara de creación de dependencia hacia otras clases son los parámetros a métodos — obviamente incluyendo los constructores.

Esto ocurre cuando el tipo del parámetro del método no está siendo usado como atributo de la clase, por lo que sería una nueva dependencia.

El Código 2 muestra un ejemplo de esto, ya que la clase Board no conocía a la clase Reporter hasta que se introdujo el método generateReport().

Código 2: Acoplamiento por parámetros a métodos.

En el caso de los constructores ocurre algo similar. Aunque lo más probable es que los parámetros que reciba un constructor sean para satisfacer dependencias ya existentes en la clase — porque ya están definidas por los atributos — puede darse el caso de que este necesite algunos adicionales.

Por ejemplo, cuando una clase hereda de otra y su padre require en su único constructor una dependencia que no va a ser usada directamente por su hijo.

El Código 3 muestra esto. La clase Game hereda de AbstractGame y este a su vez requiere por constructor la configuración Settings la cual no va a ser usada directamente en Game.

Código 3: Acoplamiento por parámetros a constructores.

Operador new

Al hacer uso del operador new — empleado en Java como en muchos otros lenguajes para reservar espacio en memoria, generalmente para un objeto — podemos estar creando una nueva dependencia si la clase no conocía previamente el tipo de dato del objeto que estamos creando.

Esto sí es bastante común y es uno de los escenarios que menos tomamos en cuenta al analizar el acoplamiento.

Código 4: Acoplamiento por operador new

Como se aprecia en el Código 4, las líneas 9 y 11 están provocando que la clase Game tenga una nueva dependencia con la clase IAPlayer que hasta ese momento no conocía.

Herencia

La herencia es otro de los casos que menos tomamos en consideración cuando de acoplamiento hablamos, pero es uno de los mecanismos de los lenguajes de programación — donde está soportada — que más acoplamiento producen.

Heredar de una clase sí o sí implicará que la clase hija necesite conocer mucho del funcionamiento del padre, incluso a nivel de implementación.

Violación de la Ley de Demeter

La Ley de Demeter [4] o Principio de Mínimo Conocimiento o No Hables con Extraños propone una serie de restricciones para evitar el aumento del acoplamiento entre clases.

Lo que viene a plantear, muy resumidamente, es que dentro de un método de una clase solo se pueden invocar métodos de: la propia clase, de los parámetros del método, de cualquier objeto instanciado dentro del método y de los atributos de la clase.

Un ejemplo de la violación de estas reglas se da cuando incurrimos en el code smell — lo siento, no tengo una buena traducción para este término, pero, por demás, es bien conocido por la comunidad, incluso la hispanohablante — Cadena de Mensajes [5][6]: a.b().c().d().

Código 5: Acoplamiento al violar la Ley de Demeter.

Como se puede observar en la línea 8 del Código 5, la implementación del método moveRightBigToe() viola claramente esta ley puesto que:

  • el método getRightLeg() de la clase Limbs devuelve una instancia de la clase Leg, que no es atributo de la clase HumanBody, ni parámetro del método ni ha sido creada dentro de este
  • el método getFoot() devuelve una instancia de la clase Foot, que no es atributo de la clase HumanBody, ni parámetro del método ni ha sido creada dentro de este
  • el método getBigToe() devuelve una instancia de la clase Toe, que no es atributo de la clase HumanBody, ni parámetro del método ni ha sido creada dentro de este; y
  • sobre esta instancia de la clase Toe se invoca el método move()

Este ejemplo tan simple ha provocado que la clase HumanBody esté acoplada también a las clases Leg, Foot y Toe, ya que sobre instancias de cada una de ellas — las cuales son consideradas por la ley como “extrañas” — se invocan métodos.

Bonus

Los casos anteriormente mencionados quizá son los más notorios a la hora de analizar donde estamos creando nuevas dependencias entre módulos — y por consiguiente más acoplamiento — pero hay otros que también podemos hacer notar:

  • imports: Aunque estaba implícitamente incluido en varios de los casos anteriores y no siempre todas las dependencias las podemos ver en estos, pueden ser un punto de partida sencillo para analizar el acoplamiento de las clases
  • anotaciones: Cada vez que hacemos uso de una anotación estamos igualmente creando una dependencia con un elemento externo
  • uso de elementos de ámbito global: En Java — por ejemplo — Math.PI

2. Complejidad de la interfaz

Por complejidad — Constantine y Yourdon — se refieren a complejidad en términos humanos, como ya vimos.

En el apartado anterior partimos del hecho de que un mayor número de interfaces, tanto expuestas como usadas por un módulo, aumenta la complejidad de este.

No obstante, una interfaz lo suficientemente compleja — en término humanos — provocará un grado mayor de acoplamiento entre módulos que si esta fuera sencilla.

Esto nos invita a reflexionar que, aunque un módulo defina una única interfaz mediante la cual se exponga toda su funcionalidad — y también vimos que este sería el escenario ideal—, si esta es demasiado compleja igualmente causará un aumento del nivel de acoplamiento.

Pero ¿qué hace que una interfaz sea compleja?

Número de elementos involucrados

Primero que todo pongamos un ejemplo.

Código 6: Interfaz con muchos parámetros de entrada.

El Código 6 muestra la interfaz que expone la clase Canvas por medio del método drawRactangle(...). Mediante esta interfaz el usuario de la clase puede dibujar un rectángulo en pantalla, pero necesita proporcionar 8 elementos diferentes para lograrlo. Obviamente es muy fácil darnos cuenta de que se tratan ni más ni menos que de los 4 puntos correspondientes a los vértices del rectángulo, pero se entiende el punto: esto es complejo a la vista, por lo que fácilmente podemos confundirnos e introducir errores al pasar los valores incorrectamente.

Sin embargo, si la misma interfaz la modificamos un poco — tal como se aprecia en el Código 7 — notaremos que, en efecto, esta otra es mucho menos compleja ya que contiene menos elementos.

Código 7: Interfaz con menos parámetros de entrada.

Variedad de los elementos involucrados

Aún peor que un gran número de elementos involucrados en una interfaz sería que, además, estos representaran conceptos muy variados.

Como ejemplo de esto pondré código real — sacado de un framework que desarrollé para el desarrollo de aplicaciones JavaFX (Ainoha Framework [7]).

Código 8: Interfaz con muchos parámetros y muy variados (tomado del código fuente [8] del proyecto Ainoha Framework [7]).

Podemos notar que la interfaz definida con el método showFxmlView(...)Código 8 — requiere 12 parámetros de entrada y que estos representan 12 conceptos diferentes. También nos damos cuenta de que, además de los conceptos que representan cada uno de los parámetros, los tipos de datos de muchos de estos — 7 para ser exactos —, a su vez, representan otros conceptos que también tendremos que entender para poder trabajar con la interfaz: Class, Stage, Object, Modality, StageStyle, String, KeyCombination.

Me refiero a que estos tipos de datos representan «conceptos que tenemos que entender» porque, por lo menos, tenemos que saber cómo obtener una instancia de ellos para poder utilizarlos; y esto implica un esfuerzo extra a la hora de utilizar la interfaz — en este caso el método.

Pero hay otro elemento que agrega complejidad y seguramente los que estamos muy familiarizados con Java lo pasemos por alto, pero a quienes vengan de otros lenguajes y lean la definición de este método les generará problemas de comprensión: el type parameter o parámetro de tipo <T>— uso de genéricos.

Estos parámetros de tipo son un elemento del lenguaje que, al estar presentes en la interfaz, necesitan ser entendidos tanto desde el punto de vista sintáctico como semántico dentro del ámbito del módulo que define dicha interfaz.

Como vemos, cualquier elemento que forme parte de una interfaz contribuirá a aumentar el grado de acoplamiento que esta genera a sus clientes.

«Se puede conseguir una mejor aproximación [al medir la complejidad de una interfaz] contando el número de símbolos discretos o “tokens del lenguaje” involucrados en la interfaz — esto es, nombres, palabras de vocabulario, variables, signos de puntuación, etc. »— Structured Design [1, p 81]

Conclusiones

En este artículo hemos visto varios escenarios en los que aumentamos el acoplamiento entre módulos de un programa, muchas veces sin que siquiera nos pase por la cabeza que lo estamos haciendo.

En lo personal muchos de estos escenarios ya los conocía como causantes de acoplamiento, pero otros no — como la variedad de los elementos involucrados en una interfaz — y fue gracias al descubrimiento de este gran tratado sobre diseño de software llamado Structured Design [1] que los conocí.

Les recomiendo a todos que si tienen la oportunidad y las ganas de profundizar en este concepto — y muchos otros — desde un enfoque muy teórico se lean el libro. Puede resultar un poco difícil de leer, pero vale la pena cada minuto invertido en hacerlo.

Referencias

  1. Edward Yourdon and Larry L. Constantine, Structured Design. Fundamentals of a Discipline of Computer Program and Systems Design (Yourdon Press, segunda edición, 1979).
  2. https://es.wikipedia.org/wiki/George_Armitage_Miller
  3. https://es.wikipedia.org/wiki/El_m%C3%A1gico_n%C3%BAmero_siete,_m%C3%A1s_o_menos_dos
  4. https://en.wikipedia.org/wiki/Law_of_Demeter
  5. Martin Fowler, Refactoring. Improving the Design of Existing Code (Addison-Wesley, 1999), p 69.
  6. https://refactoring.guru/smells/message-chains
  7. https://github.com/ainoha-framework/ainoha-core/
  8. https://github.com/ainoha-framework/ainoha-core/blob/master/src/main/java/com/ainoha/internal/FxmlViewHelper.java#L83

--

--