La semana pasada hablábamos del acoplamiento, una de las causas de que cambios en una parte del código afecten a otros sitios que parecían no estar relacionados, incluso hasta llegar a fallar. Y también de qué es la cohesión (¿Sufres de un alto acoplamiento software? ¿Haces un cambio en una parte del código y se rompe otra cosa?).
Ahora que ya sabes que un alto acoplamiento software es malo, te preguntarás qué puedes hacer para mantener el acoplamiento bajo control o cómo se puede reducir.
En este post quiero hablarte de principios y técnicas que ayudan a controlar el acoplamiento.
Lo más probable es que hayas escuchado hablar de ellas, o que sin darte cuenta las hayas usado.
Pero no puede pasar un día más sin que entiendas estas buenas prácticas y cómo intentan reducir el acoplamiento.
Principio de inversión de dependencias (DIP – “The Dependency Inversion Principle”)
En general, a la hora de diseñar software y en programación orientada a objetos, los principios SOLID del tío Bob son una guía a seguir.
En concreto, el principio de inversión de dependencias puede ayudarnos a diseñar módulos menos acoplados entre sí.
Robert C Martin habló de él por primera vez en este artículo en 1996 (que por cierto, es una lectura muy recomendable).
Resumido en una sola frase, lo que dice es que en el código, en tus diseños software, debes:
“Depender en abstracciones, no en clases concretas”
También podemos encontrar este principio de la siguiente manera:
– Los módulos de alto nivel no deberían depender en módulos a más bajo nivel. Ambos deberían depender de abstracciones.
– Las abstracciones no deberían depender de los detalles. Son los detalles deben depender de las abstracciones.
Este principio lo que pretende es resolver el problema de que módulos de alto nivel dependan y estén acoplados a módulos de menor nivel y sus detalles.
¿Y por qué podría ser esto un problema? Hagamos un símil con algo del día a día para entenderlo mejor.
Todos en casa tenemos dispositivos que se tienen que cargar: cámaras, móviles, consolas…
¿Y a la hora de cargarlos tienen algo en común? Lo cierto es que no, no tienen una interfaz común para cargarlos: los cargadores de cada dispositivo son diferentes.
Por lo tanto, en tu casa no puedes tener un único cargador que sirva para todos los dispositivos. Tienes que tener distintos cargadores específicos para cada aparato.
Trasladándolo un poco a términos de software (aunque suene un poco friki), el módulo de “cargar dispositivos electrónicos de tu casa” es dependiente de cada dispositivo y está fuertemente acoplado a ellos.
Si cambias un dispositivo y te compras otro nuevo, tendrás que comprar otro cargador distinto y modificar cómo se cargan los dispositivos de la casa.
En este caso, los dispositivos son los que dictan cómo se tienen que cargar, y no al revés.
Aquí este orden de dependencias (que sean los módulos de menor nivel los que digan cómo tienen que hacerse las cosas) resulta molesto, y mucho más engorroso de mantener.
Lo suyo sería que el módulo de cargar de tu casa, el de más alto nivel, fuera el que dicte cómo se tienen que cargar los dispositivos y no al revés. Si esto ocurriera así, todo sería más sencillo.
Ahora volviendo al mundo del software. Imagina que tenemos una clase Calculadora, que hace las operaciones típicas: suma, resta, multiplicación, división etc.
Y para poder hacer estas operaciones, esta clase tiene un switch relativamente grande: si el operador de la calculadora es +, entonces sumo; si es -, resto y así sucesivamente.
El problema es el de antes: las operaciones, los detalles, están definiendo lo que nuestro módulo de alto nivel tiene que hacer. Tienen el control, están dictando el comportamiento que tiene que adoptar la Calculadora.
Y cuando cambie alguna operación, o queramos incorporar nuevas operaciones, tendremos que cambiar el código de Calculadora.
Aquí lo que podemos hacer para mejorar este diseño es invertir este control, invertir las dependencias, definiendo una interfaz que las distintas operaciones (sumar, restar, multiplicar..) deberán seguir.
En este caso, podemos dividir cada una de las operaciones en clases separadas, que implementan la interfaz Operación de la que hemos hablado.
La clave aquí es que nuestro módulo calculadora (de alto nivel) es el que está controlando la interfaz a la que los módulos de más bajo nivel tienen que adherirse.
Además estamos desacoplando el módulo Calculadora de las operaciones, ya que si incorporamos una nueva operación o cambiamos una ya existente, Calculadora no lo nota, porque depende de una interfaz y no de una implementación concreta.
Y esto, aumenta la mantenibilidad de la clase.
Una forma de verlo, es pensar que los módulos de bajo nivel dan un servicio a los módulos de alto nivel. Los módulos de alto nivel dan una interfaz de qué tienen que hacer esos módulos y ellos, basándose en lo que el módulo de alto nivel dice, le ofrecen su servicio.
Cosas a tener en cuenta sobre la inversión de las dependencias…
No obstante, como siempre pasa, el hecho de que conozcas y sepas que puedes invertir una dependencia, no significa que sea bueno hacerlo siempre.
Si por ejemplo en el caso anterior, la Calculadora hace solo una operación y no está previsto que haga más, no tiene sentido aplicar este principio, ya que no nos aporta ningún beneficio.
A la hora de enfrentarnos a problemas de este tipo y buscar buenas soluciones de diseño, existen lo que se llaman patrones de diseño. Estos son un catálogo de problemas que surgen en el día a día al desarrollar software, acompañados de sus soluciones.
Inversion of Control y Dependency Injection
Muy relacionado con el principio de inversión de las dependencias está el término de Inversion of Control.
Digamos que Inversion of Control es la forma específica en la que invertimos el control de una dependencia.
Los frameworks, como por ejemplo Spring, se basan en esto. Invierten el control de la creación de las dependencias.
¿Esto que quiere decir? Normalmente, cuando codificamos creamos objetos haciendo algo así:
MiClase miObjeto = new MiClase();
Frecuentemente, creamos los objetos dentro de la clase que necesita esos objetos.
En cambio, si invertimos el control de creación de las dependencias, el mismo ejemplo de antes se convertiría en:
MiClase miObjeto;
En este caso, la creación del objeto se haría de forma externa a la clase que lo usa. Nosotros no creamos las dependencias, no construimos los objetos, sino que de ello se encarga un agente externo, que por ejemplo, podría inyectarlas en el código en tiempo de compilación.
Y esto último es la inyección de dependencias.
——-
Si quieres profundizar un poco sobre este tema, te recomiendo este post de Martin Fowler, sobre Inversion of Control, y Dependency Injection.
- Diario: cómo Javier Garzás evita quedarse obsoleto estudiando a un X10 con IA-Esteroides - 7 noviembre, 2024
- Si creas Historias de Usuario con IA ¿A quién pertenecen? ¿A ti o la IA? El mono Naruto te lo explica - 31 octubre, 2024
- HistorIAs de usuario y como a Maximiliano lo ENGAÑABAN con la IA y como una viejuna historia del 1500 le salvó - 24 octubre, 2024