Acoplamiento y cohesión

Todo desarrollador de software debe tener en cuenta que se obtienen tantos más beneficios cuanto más alta es la cohesión en una unidad de software y más bajo es el acoplamiento entre las unidades. Esta máxima se debe aplicar tanto en el diseño, la arquitectura y la codificación.

A menudo escuchamos problemas relacionados con dos partes de software que están fuertemente acopladas, y escuchamos términos como desacoplar.

Tanto el acoplamiento como la cohesión son términos un tanto abstractos y difíciles de medir. Hace algunos años, cuando sólo existía la programación imperativa, las cosas eran un tanto más concretas. En la actualidad, con varios paradigmas en marcha (programación orientada a objetos, a aspectos, extreme programming, etc.. ) y múltiples lenguajes y plataformas, los términos de acoplamiento y cohesión se hacen más difíciles de concretar, pero siguien teniendo aplicación, aunque su significado sea más abstracto.


Acoplamiento

Empezaremos por el acoplamiento. El término "acoplamiento" hace alusión al grado de dependencia que tienen dos unidades de software. Tiempo atrás se utilizaba la palabra "módulo" o "subrutina" en lugar de unidad de sofware. Hoy en día, en opinión del que escribe, la palabra "módulo" es completamente inadecuada y obsoleta. Mejor utilizaremos "unidad de software", que es un concepto más amplio.

Bien... ¿Qué es una unidad de software? Pues simplemente cualquier pieza de software que realice algún cometido. Por ejemplo: una función, un método, una clase, una librería, una aplicación, un componente, etc...

Si hablamos de funciones, el acoplamiento nos da una idea de lo dependientes que son dos funciones entre sí. Es decir, en qué grado una función puede hacer su trabajo sin la otra. Si hablamos de librerías, el acoplamiento nos dará una idea de en qué medida el contenido de una librería puede hacer su trabajo sin la otra.

Cuando dos unidades de software son absolutamente independientes (cada una puede hacer su trabajo sin contar para nada con la otra), encontramos el grado más bajo de acoplamiento, y decimos que ambas unidades están totalmente desacopladas.

Nuestro objetivo al programar o diseñar debe ser el de tener un acoplamiento lo más bajo posible entre dos unidades de software cualesquiera. Por supuesto, es imposible lograr un desacoplamiento total entre las unidades. Sin embargo, manteniendo lo más bajo posible el acoplamiento lograremos que las distintas "piezas" de nuestro software funcionen sin depender demasiado unas de otras. Eso redunda en una mejora considerable en la detección y corrección de errores, en una mayor facilidad de mantenimiento y sobre todo, en la reutilización de esas "piezas" de software.

Pues bien... hay varios tipos de acoplamiento... les pegaremos un breve repaso, empezando por los más deseables y terminando por los que deberíamos evitar. Según quién nos hable del acoplamiento, veremos que se utilizan unos términos u otros. Es lo de menos. Lo importante es mantenerlo bajo, independientemente de que le pongamos un nombre u otro a cada tipo de acoplamiento.

I) Unidades completamente desacopladas

Ya lo hemos comentado, dos unidades están completamente desacopladas cuando hacen su trabajo de manera totalmente independiente. Esto nos permitiría coger una de ellas y utilizarla tal cual en un programa sin necesidad de llevarnos la otra.

Por ejemplo, estos dos métodos (en C#), están totalmente desacoplados. Ninguno de ellos necesita al otro para hacer su trabajo.

static int metodo1(int a, int b)
{
   return a * b;
}

static int metodo2(int a, int b)
{
   return a + b;
}

II) Acoplamiento normal

El acoplamiento más común que existe es aquel en el que una unidad de software necesita del trabajo que hace la otra. Probablemente se llama normal, porque si descomponemos la solución de un problema en varias partes, ésta es la forma más natural y frecuente ("normal"), de recomponer la solución.

Por ejemplo, estos dos métodos tienen un acoplamiento normal.

static int metodo1(int a, int b)
{
   int c = metodo2(a, b); ;
   return 2 * c;
}

static int metodo2(int a, int b)
{
   return a + b;
}

metodo1 invoca a metodo2, y no puede realizar su función sin él. Decimos que metodo1 tiene un acoplamiento normal con metodo2. Al reves no es cierto. método2 no está acoplado con respecto a metodo1, ya que metodo2 puede realizar su trabajo independientemente de metodo1.

En el acoplamiento normal, la comunicación entre las unidades debe de producirse utilizando puntos de entrada y de salida correctos y de su interfaz. Es decir, en el caso de los métodos. toda comunicación entre un método y otro acoplado normalmente debe producirse exclusivamente por los parámetros (como entrada) y por el retorno (como salida).

Por ejemplo, si hablamos de métodos o funciones, los datos deben pasarse de una a otra a traves de parámetros, y devolverse los resultados como retorno de la función o método y no de ninguna otra forma.

Si extrapolamos esta definición a clases o librerías, por ejemplo, una clase está acoplada a otra normalmente si los objetos de una utilizan a los de la otra, y se comunican invocando sus métodos y pasándoles datos como parámetros exclusivamente y recibiéndolos a traves de canales normales, como retornos, propiedades, etc.

III) Acoplamiento de datos.

Una unidad de software está acoplada a otra por los datos cuando ambas necesitan del mismo conjunto local de datos para funcionar.

Por ejemplo, observa estos dos métodos de la misma clase:

class Ejemplo
{
   int compartido=0;

   void metodo1(int a, int b)
   {
      compartido = a * b;
   }

   void metodo2(int a, int b)
   {
       compartido = a + b;
   }
}

metodo1 y metodo2 están acoplados por los datos, ya que ambos comparten el mismo dato local para trabajar. El acoplamiento de datos es común entre los métodos de una clase, producido por la necesidad de acceder a las variables de instancia. No obstante, en muchos casos, es evitable. Si podermos hacer lo mismo sin acoplar los métodos por los datos, mejor.

IV) Acoplamiento de control.

Decimos que un método está acoplado a otro por control cuando de alguna manera un método controla la ejecución del otro. En general, suele ocurrir cuando un método pasa algún parámetro a otro, y en función de él se comporta de una u otra manera. Suele haber discusión acerca de si este acoplamiento es intrínsecamente malo o simplemente poco conveniente. No obstante, y sin entrar en divagaciones, si que está claro que si se puede evitar, mejor que mejor.

En este ejemplo, metodo1 controla la ejecución de metodo2, mediante un parámetro. Con el parámetro c, metodo1 consigue hacer que método2 multiplique o sume a y b. Ojo... no se trata de que metodo1 varíe o configure la forma en la que trabaja metodo2 en algún aspecto, sino que es metodo1 el que decide cómo debe comportarse metodo2 en su práctica totalidad.

static int metodo1(int a, int b)
{
   bool c=a > 5;
   return metodo2(a, b, c);
}

static int metodo2(int a, int b, c)
{
   int resultado;

   if (c)
   {
      resultado= a * b;
   }
   else
   {
      resultado= a + b;
   }
   return resultado;
}

Esto es fácilmente evitable cayendo en un simple acoplamiento normal, de esta manera:

static int metodo1(int a, int b)
{
   if (a > 5)
   {
      resultado = multiplicar(a, b);
   }
   else
   {
      resultado = sumar(a, b);
   }
}

static int multiplicar(int a, int b)
{
   return (a * b);
}

static int sumar(int a, int b)
{
   return (a + b);
}

V) Acoplamientos no deseados

Los comentados hasta ahora son acoplamientos que se producen habitualmente. Simplemente, debemos tener en cuenta a la hora de diseñar o programar que mejor cuanto menos acoplamiento.

En este último apartado comentaremos un par de acoplamientos que hay que evitar a toda costa.

V.1.- Acoplamiento global.

Decimos que dos unidades están globalmente acopladas cuando se pasan datos entre sí a través de una estructura global (Ojo, no confundir con el acoplamiento de datos, descrito arriba, en el cual, ambas unidades trabajan sobre el mismo conjunto de datos). En este caso, hablamos de utilizar una estructura global para pasarse datos, sin que esta estructura tenga otra finalidad.

Por ejemplo, aquí a, b y resultado se utilizan para que metodo1 y multiplicar se comuniquen... para eso no hacen falta esas variables... el mecanismo adecuado es el paso de parámetros, y un acoplamiento normal.

class EjemploAcoplamientoGlobal
{
   int a,b;
   int resultado;

   static void metodo1()
   {
      a = 5;
      b = 9;
      multiplicar();
      Console.Out.WriteLine(resultado);
   }

   static int multiplicar()
   {
       resultado = a * b;
   }
}

V.2.- Acoplamiento por contenido.

Este acoplamiento es el más dificil de definir. Afortunadamente, los lenguajes de programación más modernos nos evitan en la mayor parte de los casos caer en él insconscientemente.

Podemos decir que una unidad está acoplada a otra por contenido cuando para programar la primera es necesario conocer cualquier detalle del interior de la segunda. Es decir, en general, cuando programamos una unidad que está acoplada a otra, de la segunda sólo necesitamos conocer sus puntos de entrada y los resultados que nos va a devolver. No debe ser necesario conocer ningún detalle de la implementación de la segunda. Si vemos que lo necesitamos, estaremos incurriendo en un acoplamiento por contenido.

Desacoplar

Por último, comentar que cuando revisamos una unidad de sofware acoplada a otra y logramos reducir su nivel de acoplamiento, decimos que las estamos desacoplando, es decir, reduciendo su dependencia.


Cohesión

La cohesión tiene que ver con la forma en la que agrupamos unidades de software en una unidad mayor. Por ejemplo, la forma en la que agrupamos funciones en una librería, o la forma en la que agrupamos métodos en una clase, o la forma en la que agrupamos clases en una librería, etc...

Se suele decir que cuanto más cohesionados estén los elementos agrupados, mejor. El criterio por el cual se agrupan es la cohesión.

Veremos los distintos tipos de cohesión, de la que se considera mayor cohesión a la que se considera menor.

Nuevamente, los nombres no son demasiado importantes, basta saber que a la hora de decidir por qué criterio agrupar, unos suelen dar mejores resultados que otros desde el punto de vista de la modularidad.

La cohesión no tiene tanto impacto sobre la modularidad como el acoplamiento. Es decir, un gran acoplamiento puede ser muy malo a la hora de corregir errores, integrar partes, hacer mantenimientos... Sin embargo, una cohesión baja puede ser incómoda, pero no suele plantear grandes problemas. Aunque esto, no es motivo para descuidarla.

1) Cohesión funcional.

Se produce cuando agrupamos unidades de software teniendo en cuenta que todas ellas contribuyen a realizar un mismo fin. Es decir, cuando todas las unidades agrupadas, trabajando juntas consiguen un objetivo. En general, es el criterio de agrupación más deseable. Además, entre este tipo de unidades suele haber un acoplamiento relativamente alto, así que mejor que estén juntas.

2) Cohesión secuencial.

Cuando agrupamos unidades que cumplen que los resultados que produce una son los que utiliza otra para continuar trabajando. Es decir, los datos de salida de una sirven de entrada para otras. Es una forma de agrupar muy relacionada con el problema que se está tratando de resolver.

3) Cohesión de datos.

Cuando todas las unidades agrupadas trabajan sobre el mismo conjunto de datos.

4) Cohesión lógica.

Cuando todas las unidades agrupadas realizan trabajo en una misma categoría lógica, pero no necesariamente tienen relación unas con otras. Por ejemplo, librerías de funciones matemáticas... se agrupan simplemente porque realizan cálculos matemáticos, pero no necesariamente tienen relación unos con otros.

5) Cohesión temporal.

Este criterio empieza a ser algo peor. Significa que agrupamos una serie de unidades simplemente porque tienen que ejecutarse más o menos en el mismo periodo de tiempo, pero sin que tengan una relación mayor entre ellas... es decir, sin que contribuyan al mismo fin (funcional), sin que se pasen datos en secuencia (secuencial) y sin que ni tan siquiera trabajen sobre los mismos datos (de datos) ni caen dentro de una misma categoría (lógica). Simplemente, tienen que ejecutarse cerca unas de otras.

6) Cohesión casual.

Pues cualquier criterio que no caiga dentro de los anteriores se considera ya puramente casual. Mejor evitarla, si se puede... más vale tener un criterio, aunque no estemos seguros de que es bueno, que no tener ningún criterio.


Conclusión

En resumen, mantener el acoplamiento lo más bajo posible y la cohesión lo más alta posible suele ser el objetivo de todo arquitecto, diseñador o programador. Tener unos buenos criterios para agrupar unidades de software (alta cohesión), y mantener esas unidades lo más independientes posible (bajo acoplamiento) garantiza la modularidad, facilitando la reutilización del software y gran parte de las tareas del desarrollo del software.