En el diseño orientado a objetos, el principio de inversión de dependencia es una forma específica de desacoplar módulos de software. Al seguir este principio, las relaciones de dependencia convencionales establecidas desde los módulos de alto nivel de establecimiento de políticas a los módulos de dependencia de bajo nivel se invierten, lo que hace que los módulos de alto nivel sean independientes de los detalles de implementación del módulo de bajo nivel. El principio establece:
Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones (p.ej., interfaces). Las abstracciones no deberían depender de los detalles. Los detalles (implementaciones concretas) deben depender de abstracciones.
Al dictar que tanto los objetos de alto nivel como los de bajo nivel deben depender de la misma abstracción, este principio de diseño invierte la forma en que algunas personas pueden pensar sobre la programación orientada a objetos.
La idea detrás de los puntos 1 y 2 de este principio es que al diseñar la interacción entre un módulo de alto nivel y uno de bajo nivel, la interacción debe considerarse como una interacción abstracta entre ellos. Esto no solo tiene implicaciones en el diseño del módulo de alto nivel, sino también en el de bajo nivel: el de bajo nivel debe diseñarse teniendo en cuenta la interacción y puede ser necesario cambiar su interfaz de uso.
En muchos casos, pensar en la interacción en sí misma como un concepto abstracto permite reducir el acoplamiento de los componentes sin introducir patrones de codificación adicionales, permitiendo solo un esquema de interacción más ligero y menos dependiente de la implementación.
Cuando el/los esquema/s de interacción abstracta descubierto/s entre dos módulos es/son genéricos y la generalización tiene sentido, este principio de diseño también conduce al siguiente patrón de codificación de inversión de dependencia.
Patrón de capas tradicionales En la arquitectura de aplicación convencional, los componentes de nivel inferior (p. Ej. La capa de utilidad) está diseñada para ser consumida por componentes de nivel superior (p. Ej. Capa de política) que permite construir sistemas cada vez más complejos. En esta composición, los componentes de nivel superior dependen directamente de los componentes de nivel inferior para lograr alguna tarea. Esta dependencia de los componentes de nivel inferior limita las oportunidades de reutilización de los componentes de nivel superior. centro El objetivo del patrón de inversión de dependencia es evitar esta distribución altamente acoplada con la mediación de una capa abstracta, y aumentar la reutilización de las capas superiores/políticas.
Patrón de inversión de dependencia Con la adición de una capa abstracta, las capas de nivel superior e inferior reducen las dependencias tradicionales de arriba abajo. Sin embargo, el concepto de "inversión" no significa que las capas de nivel inferior dependan de las capas de nivel superior. Ambas capas deben depender de abstracciones que dibujen el comportamiento que necesitan las capas de nivel superior. centro En una aplicación directa de inversión de dependencia, los resúmenes son propiedad de las capas superiores / políticas. Esta arquitectura agrupa los componentes superiores / de política y las abstracciones que definen los servicios inferiores juntos en el mismo paquete. Las capas de nivel inferior se crean por herencia / implementación de estas clases o interfaces abstractas.
La inversión de las dependencias y la propiedad fomenta la reutilización de las capas superiores/políticas. Las capas superiores podrían usar otras implementaciones de los servicios inferiores. Cuando los componentes de la capa de nivel inferior están cerrados o cuando la aplicación requiere la reutilización de servicios existentes, es común que un Adaptador medie entre los servicios y las abstracciones.
Generalización del patrón de inversión de dependencia En muchos proyectos, el principio y el patrón de inversión de dependencia se consideran un concepto único que debe generalizarse, es decir, aplicarse a todas las interfaces entre módulos de software. Hay al menos dos razones para eso:
Es más simple ver un buen principio de pensamiento como un patrón de codificación. Una vez que se ha codificado una clase abstracta o una interfaz, el programador puede decir: "He hecho el trabajo de abstracción". Debido a que muchas herramientas de pruebas unitarias dependen de la herencia para lograr la burla, el uso de interfaces genéricas entre clases (no solo entre módulos cuando tiene sentido usar la generalidad) se convirtió en la regla.
Si la herramienta de burla utilizada se basa solo en la herencia, puede ser necesario aplicar ampliamente el patrón de inversión de dependencia. Esto tiene grandes inconvenientes:
La simple implementación de una interfaz sobre una clase no es suficiente para reducir el acoplamiento; Solo pensar en la abstracción potencial de las interacciones puede conducir a un diseño menos acoplado. La implementación de interfaces genéricas en todas partes en un proyecto hace que sea más difícil de entender y mantener. En cada paso, el lector se preguntará cuáles son las otras implementaciones de esta interfaz y la respuesta es generalmente: solo simulacros. La generalización de la interfaz requiere más código de plomería, en particular las fábricas que generalmente dependen de un marco de inyección de dependencia. La generalización de la interfaz también restringe el uso del lenguaje de programación.
Restricciones de generalización La presencia de interfaces para lograr el patrón de inversión de dependencia (DIP) tiene otras implicaciones de diseño en un programa orientado a objetos :
Todas las variables miembro en una clase deben ser interfaces o resúmenes. Todos los paquetes de clase concretos deben conectarse solo a través de la interfaz o paquetes de clase abstractos. Ninguna clase debe derivar de una clase concreta. Ningún método debe anular un método implementado. Toda instanciación variable requiere la implementación de un patrón de creación, como el método de fábrica o el patrón de fábrica, o el uso de un marco de inyección de dependencia .
Restricciones de burla de la interfaz El uso de herramientas de burla basadas en herencia también introduce restricciones:
Los miembros estáticos visibles desde el exterior deben confiar sistemáticamente en la inyección de dependencia, lo que los hace mucho más difíciles de implementar. Todos los métodos comprobables deberían convertirse en una implementación de interfaz o una anulación de una definición abstracta.
Direcciones futuras Los principios son formas de pensar, los patrones son formas comunes de resolver problemas. Los patrones de codificación pueden verse como características faltantes del lenguaje de programación.
Los lenguajes de programación continuarán evolucionando para permitir el cumplimiento de contratos de uso más fuertes y más precisos, en al menos dos direcciones: hacer cumplir las condiciones de uso (condiciones pre, post e invariantes) e interfaces basadas en estado. Esto probablemente fomentará y potencialmente simplificará una aplicación más fuerte del patrón de inversión de dependencia en muchas situaciones. Más y más herramientas de burla ahora usan la inyección de dependencia para resolver el problema de reemplazar miembros estáticos y no virtuales. El lenguaje de programación probablemente evolucionará para generar un código de bytes compatible con burlas. Una dirección será restringir el uso de miembros no virtuales, la otra será generar, al menos en situaciones de prueba, un código de bytes que permita la burla basada en la no herencia.
Implementaciones Dos implementaciones comunes de DIP utilizan una arquitectura lógica similar, con diferentes implicaciones.
Una implementación directa empaqueta las clases de políticas con clases de resúmenes de servicio en una biblioteca. En esta implementación, los componentes de alto nivel y los componentes de bajo nivel se distribuyen en paquetes / bibliotecas separadas, donde las interfaces que definen el comportamiento / servicios requeridos por el componente de alto nivel son propiedad y existen dentro de la biblioteca del componente de alto nivel. La implementación de la interfaz del componente de alto nivel por parte del componente de bajo nivel requiere que el paquete de componentes de bajo nivel dependa del componente de alto nivel para la compilación, invirtiendo así la relación de dependencia convencional. centro Las Figuras 1 y 2 ilustran el código con la misma funcionalidad, sin embargo, en la Figura 2, se ha utilizado una interfaz para invertir la dependencia. La dirección de dependencia se puede elegir para maximizar la reutilización del código de política y eliminar las dependencias cíclicas.
En esta versión de DIP, la dependencia del componente de la capa inferior de las interfaces/resúmenes en las capas de nivel superior dificulta la reutilización de los componentes de la capa inferior. En cambio, esta implementación ″ invierte ″ la dependencia tradicional de arriba hacia abajo a lo opuesto, de abajo hacia arriba.
Una solución más flexible extrae los componentes abstractos en un conjunto independiente de paquetes / bibliotecas: centro La separación de cada capa en su propio paquete fomenta la reutilización de cualquier capa, proporcionando robustez y movilidad.
Ejemplos
Módulo genealógico Un sistema genealógico puede representar las relaciones entre las personas como un gráfico de las relaciones directas entre ellas (padre-hijo, padre-hija, madre-hijo, madre-hija, esposo-esposa, esposa-esposo, etc. ) Esto es muy eficiente y extensible, ya que es fácil agregar un exesposo o un tutor legal.
Pero algunos módulos de nivel superior pueden requerir una forma más sencilla de navegar por el sistema: cualquier persona puede tener hijos, padres, hermanos (incluidos o no hermanastros o hermanastros), abuelos, primos, etc.
Dependiendo del uso del módulo genealógico, presentar relaciones comunes como propiedades directas distintas (ocultar el gráfico) hará que el acoplamiento entre un módulo de nivel superior y el módulo genealógico sea mucho más ligero y le permita a uno cambiar completamente la representación interna de las relaciones directas. sin ningún efecto en los módulos que los usan. También permite incorporar definiciones exactas de hermanos o tíos en el módulo genealógico, aplicando así el principio de responsabilidad única .
Finalmente, si el primer enfoque de gráfico generalizado extensible parece el más extensible, el uso del módulo genealógico puede mostrar que una implementación de relación más especializada y más simple es suficiente para las aplicaciones y ayuda a crear un sistema más eficiente.
En este ejemplo, abstraer la interacción entre los módulos conduce a una interfaz simplificada del módulo de nivel inferior y puede conducir a una implementación más simple del mismo.
Cliente de servidor de archivos remoto Imagine que tiene que implementar un cliente en un servidor de archivos remoto (FTP, almacenamiento en la nube...) Puede considerarlo como un conjunto de interfaces abstractas:
Conexión/desconexión (puede ser necesaria una capa de persistencia de conexión) Interfaz de carpeta/creación de etiquetas/renombrar/eliminar/listar Interfaz de creación/reemplazo/cambio de nombre/eliminación/lectura de archivos Búsqueda de archivos Reemplazo concurrente o resolución de borrado Gestión del historial de archivos...
Si tanto los archivos locales como los archivos remotos ofrecen las mismas interfaces abstractas, cualquier módulo de alto nivel que use archivos locales e implemente completamente el patrón de inversión de dependencia podrá acceder indiscriminadamente a los archivos locales y remotos.