
El principio de inversión de las dependencias (DIP por sus siglas en inglés) es uno de los cinco principios de desarrollo de software orientado a objeto, propuesto por Robert C. Martin y que se conocen como los principios SOLID. DIP corresponde concretamente con la “D” de SOLID.
DIP parte de dos premisas:
- Los módulos de alto nivel no deben depender de los de bajo nivel, sino que ambos deben depender de abstracciones (idealmente interfaces).
- Las abstracciones no deben de depender de las implementaciones concretas, sino que deben ser las implementaciones las que dependan de las abstracciones. Es más, las clases concretas no deben depender en general de otras clases concretas, sino que dependerán directamente de abstracciones.
Cuando hablamos de módulos de alto nivel, nos estamos refiriendo a aquellos que contienen las clases más cercanas a la lógica de negocio de la aplicación, mientras que los de bajo nivel son aquellos que llevan a cabo los rudimentos que permiten completar las tareas de alto nivel.
El objetivo final es aminorar los riesgos que entraña para cualquier clase sus dependencias: cuando una pieza de software (A) depende de otra (B) esa dependencia supone un riesgo para A, ya que, si por alguna razón la B cambia, A puede verse comprometida.
Debemos tener en cuenta que las clases concretas son en general volátiles frente a las interfaces que, si están bien diseñadas, son estables. Por su lado, cuanto de más bajo nivel son los módulos más volátiles tienden a ser, por el contrario, los módulos de alto nivel suelen ser más estables.
Así las cosas, hacer que los módulos de alto nivel dejen de depender de los de bajo nivel para hacerlo de abstracciones y, en general, hacer que las clases concretas dependan de las abstracciones, hace que las clases dependan de artefactos estables lo que aminora el riesgo que suponen las dependencias, ya que el riesgo deriva de la posibilidad de que la pieza de la que dependemos cambie. Específicamente buscamos que las clases de alto nivel no se vean afectados por los cambios en las clases de bajo nivel.
Pero, si los módulos de bajo nivel implementan los rudimentos que permiten llevar a cabo las tareas de alto nivel, esto es, los módulos de bajo nivel dan servicio a los de alto nivel, todo parece indicar que los módulos de alto nivel deben depender de los de bajo nivel ¿cómo hacer que eso no sea así? Vamos a verlo en un ejemplo.
Digamos que una parte de nuestra aplicación recupera documentos de una base de datos Mongo y los exporta a formato PDF.
Un enfoque sin preocuparnos de aplicar las directrices de DIP sería el siguiente:


Veamos ahora como como podemos rediseñar esta parte de la aplicación aplicando DIP


Como podemos observar, hemos pasado de un diseño en el que la clase de alto nivel Processor depende de las clases de bajo nivel MongoDB y PDFExporter, a un diseño en que la clase de alto nivel depende de sendas abstracciones y las clases de bajo nivel también dependen de dichas abstracciones.
Las dependencias de la clase de alto nivel son de uso y las dependencias de las clases de bajo nivel de implementación (implementan las respectivas interfaces).
Como se puede ver, se invierte la dirección de las dependencias entre módulos. El módulo de alto nivel queda aislado y protegido de los cambios en las capas de bajo nivel, de tal manera que un hipotético cambio de base de datos de Mongo a MySql o la necesidad de incorporar un exportador a HTML no le afectan.
¿Por qué lo llamamos inversión de dependencias?
La razón por la que este proceso se llama inversión de dependencias ya lo dejábamos caer en el ejemplo previo. En un diseño en el que se prescinde de DIP o simplemente no se puede aplicar porque el lenguaje no es orientado a objetos (no disponemos de polimorfismo), las dependencias se dirigen en el mismo sentido que el flujo de control de la aplicación, que es de las áreas centrales del sistema o de alto nivel hacia las áreas periféricas o de bajo nivel que le dan servicio.
Al aplicar DIP, lo que hacemos es invertir (de ahí el nombre) la dirección de las dependencias, que pasan a dirigirse en sentido inverso al flujo de control, es decir, desde las áreas periféricas hacia las centrales.

Sin DIP

Con DIP
Como podemos ver en estos dos diagramas, la dirección de las dependencias pasa a ir en sentido ascendente. Para ello, introducimos las abstracciones y los módulos de los niveles superiores dejan de depender de los inferiores y ambos pasan a depender de las abstracciones (los superiores con dependencia de uso y los inferiores con dependencia de implementación).
Si nos fijamos, para que las dependencias vayan en sentido ascendente, es necesario situar las abstracciones en el nivel adecuado (por ejemplo, Abstracción Mecanismo en el nivel 2).
Como podemos imaginar, situar las abstracciones de esa manera no es casual ni tampoco lo hacemos para que los diagramas resulten a nuestra conveniencia. La clave está en entender que esas abstracciones son contratos que establecen los niveles superiores para que los inferiores se adhieran a ellos, es decir, esas abstracciones forman parte del diseño del nivel superior y mediante ellas el nivel superior le dice al inferior qué servicios necesita. La iniciativa en el diseño del servicio pasa del nivel inferior al superior
Si los niveles pudiesen dialogar entre sí, sería como si el nivel superior le dijese al inferior: “En lugar de hacer los servicios a tu manera y que yo me adapte, yo te digo qué servicios necesito y cómo debes hacerlos y eres tú quien debe adaptarse. Si algo cambia u otro actor quiere proveer el mismo servicio debe ser respetando el contrato, de manera que yo no me entere ni me vea afectado por esos cambios”.
Pongámonos en el caso del primer ejemplo que veíamos. En el momento de diseñar la capa Processor se va a determinar qué servicios de persistencia y de exportación precisamos y definiremos sendas interfaces que actúen como contratos. Y eso lo haremos abstrayéndonos de los rudimentos de persistencia y exportación; la clase Processor no tiene por qué saber si la base de datos subyacente es Mongo, MySql o ficheros planos, se limita a establecer que servicios necesita y como los quiere (en este caso un simple método getDocument).
La clase de bajo nivel será la encargada de llevar a cabo los rudimentos para obtener un documento extrayéndolo de una base de datos Mongo respetando el contrato, esto es, implementando la interfaz; si las especificaciones cambian y se hace necesario extraer los documentos de una base MySql, haremos una nueva clase que también implemente la interfaz pero que interaccione con MySql. La clase Processor no tiene por qué enterarse de dichos cambios ni modificar su funcionamiento para adaptarse a la nueva realidad.
Teniendo todo esto en cuenta, debemos darnos cuenta de que es necesario entender plenamente el qué y el porqué de DIP en lugar de aplicarlo como una receta sin entender realmente el fondo.
Con frecuencia se realizan las clases de nivel inferior de manera previa, adaptando las de nivel superior a ellas y finalmente poniendo una interfaz por encima para dar apariencia de que el sistema es DIP compliance; sin embargo, este enfoque no solo no aporta los beneficios esperados, sino que, con frecuencia, resulta contraproducente ya que introduce una capa de abstracción innecesaria (cuyo único objetivo es “cubrir el expediente”) con el plus de complejidad que ello conlleva.
Y es que, como casi todo en la vida, cuando programamos es muy oportuno ser capaces de crear automatismos mentales y protocolizar nuestra forma de crear software, puesto que eso nos permite no estar sistemáticamente repensando todos los pasos que damos. Pero no es menos cierto que es imprescindible tener un conocimiento profundo de aquello que hacemos y evitar aplicar fórmulas rutinariamente sin entender por qué, ya que lo habitual es que eso nos conduzca a un trabajo de baja calidad.
¿Te interesan más temas como este? Aquí te dejamos una lista de entradas que pueden interesarte:
Si eres un buen programador, entonces eres una buena persona