sábado, 13 de marzo de 2021

Regla Nº20 No depender del llamador, ni de la implementación (evitar acoplamientos)

Frecuéntenme, en nuestra vida personal y laboral, cambiamos nuestro comportamiento y actitud según con quien estemos hablando con la finalidad de poder conseguir y resolver las necesidades o circunstancias que se nos plantean. No es lo mismo (o no diera) que un doctor este dando una clase sobre alguna enfermedad que le este comunicando dicha enfermedad a un paciente, ni tampoco es igual como hablamos con nuestros padres o hijos a como hablamos con nuestros compañeros de trabajo. Por otro lado tampoco recibimos la información de la misma forma de personas que apreciamos y conocemos que de desconocidos, “modulamos” nuestro comportamiento según con quien estemos hablamos.

Ese punto de vista, tan normal en interacciones humanas, supone un problema a la hora de implementar software.

Una clase no debe cambiar su comportamiento según quien la consume, igualmente el consumidor de una clase no debiera saber cómo esta implementada internamente dicha clase, para poder consumirla de forma adecuada.

Estos días he tenido que dar mantenimiento a un sistema, un sistema que es consumido por otros, bastante viejo, ¡pero no tanto como para los problemas que me he encontrado!


Había que agregar un nuevo comportamiento, hasta ahí todo normal, como ya hemos hablado en varias circunstancias (Regla N°1: Va a cambiar), el software se modifica contantemente para adaptarse a negocio cambiante. ¡El problema es que básicamente toda la lógica de negocio estaba en una misma función, con multitud de if anidados  y ramificaciones! (Regla N°18: evita el código espagueti). No solo era difícil de entender, si no extremadamente delicado de modificar, cada vez que se moviera algo, podría afectar de cualquier forma a los consumidores, para colmo el sistema reacciona de forma diferente si lo llama un sistema o si lo llama otro, es decir la clases tiene que reconocer que quien le está llamando, para saber cómo debe comportarse.

 Lo anterior resume un gran problema de acoplamiento y falta de cohesión, lo que está ligado directamente a la dificulta de mantenimiento del software (lo que a su vez es ligado a costos económicos, tiempo, y muy posibles desastres productivos)

Acoplamiento


El acoplamiento entre dos clases es una medida que indica cuanto depende una clase de otra, específicamente cuando depende de su implementación interna. Cuando mas acoplamiento, peor es nuestro código, cuando mas desacoplado este, mas fácil de mantener.

Idealmente una clase solo se debiera relacionar con otras a través de la definición de los métodos, el cómo hagan esos métodos sus funciones con cosas que nos deben preocupar. Lo anterior se ve claramente cuando podemos sustituir la referencia de una clase, por la referencia a una interface, y cambiar la clase que lo implementan sin mucho (o ningún) impacto.

Si podemos cambiar una clase consumida por otra, sin realizar cambios en el consumidor, hemos conseguido un nivel aceptable de desacoplamiento.

Con respecto a no depender de la “implementación interna de una clase”, se dan varios escenarios que demuestran un alto acoplamiento:

  • Cuando la clase consumidora debe almacenar información que no le sirve de nada, salvo para consumir otra clase. Aquí es donde se ve un defecto de diseño, puesto que debiera ser la clase consumida la que debe contener sus propios datos, sin forzar a los consumidores a hacerlo.

  • Las clases se comunican de forma estrafalaria y poco comprensible, por ejemplo se usan cadenas de texto, para comunicar cosas que claramente no lo son, como números, fechas u objetos. El ejemplo de la fecha es muy común, se pasa una fecha en formato texto, pero hay muchas formas de expresar una fecha por texto, no es lo mismo el 1 de Febrero (01/02/2021), que el 2 de Enero  (02/01/2021), pero según el formato de la fecha podría ser una u otra. La única forma correcta de pasar una fecha es mediante un objeto de tipo fecha.

  • Los métodos de una clase deben llamarse en un orden determinado para que funcionen, si no dan resultados inesperados, cada métodos debe hacer algo en concreto, y debe hacerlo independiente en el orden en lo que se les llama, si no puede hacerlo por algún motivo, debe lanzar una excepción (es mejor no hacer algo que hacerlo mal).

    Una situación reciente que me acaba de pasar, tenía que usar un control grafico de terceros, un datagrid para mostrar tabla en pantalla (en una aplicación de escritorio). Una vez mostrada la información en pantalla había que cambiar el valor de una celda en particular, pero por algún motivo extraño, la celda tenía que estar visible, si no ignoraba la instrucción para cambiar el valor (sin producir ningún fallo), así que la solución fue posicional visualmente el componente en la fila requerida y cambiar el valor, y después volver a la fila que estaba el usuario actualmente (que pudiera ser otra), pero esta daba otro problema como un parpadeo innecesario en el programa, eso se solucionaba desactivando el control antes de realizar la operación. Al final el programa consumidor, se lleno de instrucciones que hay que ejecutar antes (y después) de cambiar el valor de una celda, y no se comprende muy bien porque es así, y desde luego si se cambia el componente todo ese código esta de mas.

  • La clase consumida cambia su comportamiento según quien lo está consumiendo, ese es el acoplamiento más rocambolesco de todos. Es decir si la clase A, y B consumen la clase C,  la C cambia su funcionalidad según quien la este llamando.

    Para comenzar, de por si suena muy extraño. Una clase debe tener una misión sea cual sea, debe ser clara y concisa y realizarla de la misma forma independientemente de quien le este llamando.

    Que modifique su comportamiento según el llamador, suena a que la clase no tiene un diseño bueno orientado a objetos, donde se usa una serie de estructuras condicionales gigantescas, en lugar de usar herencia y polimorfismo.

    Cuando se modifica la clase consumida, se puede afectar a la clase consumidora obligándola también a  ser modificada, si hay más de un sistema que consuma dicha clase, habrá que cambiar todos y cada uno de ellos con un consumo en tiempo y recursos. Si además los sistema a modificar son consumidos por otros sistemas, pueden desencadenas una cadenas de modificaciones enorme y costosa.

    Es posible, no obstante, que debamos modificar el acceso a la funcionalidad en cuestión de permisos, es decir dichas componente tendrá acceso a alguna funcionalidad y otra no. Un ejemplo muy típico de esto es cuando las aplicaciones se logean con facebook y nos muestra un mensaje que nos dice a que tendrá acceso la pagina web donde nos logeamos, puede ser al perfil público y a nuestro correo, otra quieren acceder a nuestra lista de contactos y otra incluso a publicar en nuestro nombre. Pudiera parecer que se está creando un acoplamiento en el que facebook devuelve información según la aplicación que se lo pida pero no cierto. Hay una capa de seguridad adicional que se encarga de generar un “token” con ciertos permisos hacia facebook y la aplicación web consumidora usara este token, pero en ningún caso facebook lo hará por que la pagina web que lo consume sea una o la otra, sino porque se solicito unos permisos y se trabaja con la autorización previa de con dichos permisos.

  • Una medida del acoplamiento de una clase es la cantidad de clases adicionales que consume. Puede ser que parezca que no dependa de la implementación interna de cada uno de los componentes que consume pero cuando una clase consume muchas clases, comienza a perder su identidad y a depender de elementos externos para realizar su tarea,  lo que al final repercute en un alto acoplamiento. En una aplicación empresarial se supone que una clase solo debe consumir las clases que estén en un nivel más por debajo que el suyo con lo que sería bastante raro que consumiera muchas clases.


Cohesión


La cohesión posiblemente sea unos de los conceptos que son más difíciles de explicar y comprender acertadamente de la ingeniera de software.

A veces se explica simplemente como que todo lo que pertenezca a mismo ámbito o tenga que ver entre sí debe estar junto. Así agrupamos los métodos en clases, las clases en namespace (o paquetes), los namespace en componentes y los componentes en sistemas.

Pero lo anterior es solo organización, agrupar las cosas según su semejanza y naturaleza, para que sea más sencillo encontrarlas y usarlas pero no define el nivel de cohesión como tal.

  • Una clase se debe componer de datos, representados por atributos (o propiedades) y por funcionalidad que usa esos datos. ¿Qué tantos atributos debe usar cada función o métodos?, en principio debiera usar todos, ya que los atributos es lo que representa el estado del objeto y cada función debiera cambiarlo (por que debiera hacer algo con el objeto), en la medida que use mas métodos y mas atributos de la clase, la misma clase esta mas cohesionada.

  • Si en una clase, una  colección de métodos usa unos atributos y otra colección de métodos usan otros atributos diferentes entonces la clase no tiene cohesión y realmente son dos clases que se han convertido en una sola, debiéndose separar. Si esto lo tenemos varias veces, lo que tenemos es lo que se llama una “clase gorda”, una clase con muchas cosas dentro sin nada que ver entre sí y que posiblemente solo sea una colección de métodos, que no son parte de una objeto con identidad. Posiblemente las  clase tenga más de un motivo para cambiar y dichos motivos no tengan nada que ver entre sí (lo que hace que no se cumpla el principio de “responsabilidad única”).

  • De igual manera si una clase consume en exceso a otra clase, y sobretodo usa sus atributos (a atreves de sus propiedades o métodos) de forma constante pareciendo que no tiene ningún sentido una clase sin la otra, estaríamos en un caso de “clase famélica”, y posiblemente debiera ser una sola.

  • Lo anterior vuelve a pasar con los namespaces (o paquetes). Estos deben componerse de clases que se consumen o se relacionan entre sí (por medio de mecanismos de (herencia, agregación, composición, colección u otros). También debe evitar incluir clases que no se usan de ningún modo. Esto hacen que las clases sea cohesionadas y tengan un objetivo, fácilmente identificable.

  • Los namespaces se agrupan en componentes. Los componentes igualmente tiene un objetivo común y identificable, pero es una agrupación “física”, en forma de elemento reusable como una librería, un jar de java o un servicio.

  • Los sistemas son agrupaciones de uno o más componentes que interactúan entre sí para resolver una serie de problemas de negocio a través de flujos e interacciones con el usuario.


La cohesión nos ayuda a que los problemas que tenemos que resolver sean pequeños, conexos y fácilmente escalables.

Relación entre cohesión y acoplamiento


Cohesión y acoplamiento son dos valores tan relacionados que siempre se van a encontrar juntos.
  • Si aumenta la cohesión disminuye el acoplamiento.

    Si tenemos juntos los elementos que si deben estar juntos, el acoplamiento debiera disminuir porque la organización de los mimos elementos nos lleva a consumir solo los que necesitamos, con cierto control y estructura.

  • Si disminuye la cohesión aumente el acoplamiento.

    Lo contrario de “tener todo en un lugar”, es “tenerlo todo junto”. Si tenemos todas las clases a la mano, es normal que comencemos a usar clases simplemente porque están accesibles, mezclando cosas que poco o nada tiene que ver, y eso aumentaría el acoplamiento

  • Si disminuye el acoplamiento aumenta la cohesión.

    Si diseñamos nuestro código disminuyendo la cantidad de clases que consumimos, no dependemos de su implementación interna y nos comunicamos a través de interfaces, quiere decir que tenemos un nivel de organización y agrupación lo suficientemente algo.

  • Si aumenta el acoplamiento disminuye la cohesión.

    Si nuestra clase comienza a consumir clases de diversa índole y sin ningún control ni estructura, es forzosamente necesaria tenerlas accesibles y tenerlas accesibles le fuerza a que tenerlas lo mas “cerca” posible, disminuyendo considerablemente la cohesión.




Si te ha gustado la entrada, ¡Compártela! ;-)

Reglas de la Ingeniería de software (índice): https://desdelashorasextras.blogspot.com/2019/12/reglas-de-la-ingenieria-de-software_15.html

No hay comentarios:

Publicar un comentario