En el diseño orientado a objetos , el principio de inversión de dependencias es una forma específica de acoplar libremente módulos de software . Al seguir este principio, las relaciones de dependencia convencionales establecidas desde los módulos de establecimiento de políticas de alto nivel hasta 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: [1]
- Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de abstracciones (por ejemplo, interfaces).
- Las abstracciones no deberían depender de los detalles. Los detalles (implementaciones concretas) deberían depender de las 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. [2]
La idea detrás de los puntos A y B 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 los esquemas de interacción abstracta descubiertos entre dos módulos 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 (por ejemplo, la capa de utilidad) están diseñados para ser consumidos por componentes de nivel superior (por ejemplo, la capa de política) que permiten la construcción de 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. [1]
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 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 a abajo. Sin embargo, el concepto de "inversión" no significa que las capas de nivel inferior dependan directamente de las capas de nivel superior. Ambas capas deben depender de abstracciones (interfaces) que exponen el comportamiento que necesitan las capas de nivel superior.
En una aplicación directa de inversión de dependencia, los resúmenes pertenecen a las capas superiores / políticas. Esta arquitectura agrupa los componentes de políticas / superiores y las abstracciones que definen los servicios inferiores en el mismo paquete. Las capas de nivel inferior se crean mediante la herencia / implementación de estas clases o interfaces abstractas . [1]
La inversión de las dependencias y la propiedad fomenta la reutilización de las capas superiores de políticas. Las capas superiores podrían utilizar 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 dependencias 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 ello:
- Es más sencillo 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 prueba unitaria se basan en 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 mera implementación de una interfaz sobre una clase no es suficiente para reducir el acoplamiento; solo pensar en la posible abstracción de interacciones puede conducir a un diseño menos acoplado.
- La implementación de interfaces genéricas en todas partes de 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 burlas.
- La generalización de la interfaz requiere más código de plomería, en particular fábricas que generalmente se basan en 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 de una clase deben ser interfaces o resúmenes.
- Todos los paquetes de clases concretas deben conectarse solo a través de la interfaz o paquetes de clases abstractas.
- Ninguna clase debe derivar de una clase concreta.
- Ningún método debe anular un método implementado. [1]
- Toda instanciación de variables 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 interfaz
El uso de herramientas de simulación basadas en herencia también presenta restricciones:
- Los miembros estáticos visibles externamente deben depender sistemáticamente de la inyección de dependencia, lo que los hace mucho más difíciles de implementar.
- Todos los métodos comprobables deben convertirse en una implementación de interfaz o una sustitución de una definición abstracta.
Direcciones futuras
Los principios son formas de pensar. Los patrones son formas comunes de resolver problemas. Es posible que a los patrones de codificación les falten características del lenguaje de programación.
- Los lenguajes de programación continuarán evolucionando para permitirles hacer cumplir contratos de uso más fuertes y precisos en al menos dos direcciones: hacer cumplir las condiciones de uso (condiciones previas, posteriores e invariantes) e interfaces basadas en el estado. Esto probablemente alentará y potencialmente simplificará una aplicación más sólida del patrón de inversión de dependencia en muchas situaciones.
- Cada vez más herramientas de burla utilizan ahora la inyección de dependencias para resolver el problema de reemplazar miembros estáticos y no virtuales. Los lenguajes de programación probablemente evolucionarán para generar códigos de bytes compatibles con la simulación. Una dirección será restringir el uso de miembros no virtuales. El otro será generar, al menos en situaciones de prueba, un código de bytes que permita simulaciones no basadas en herencia.
Implementaciones
Dos implementaciones comunes de DIP utilizan una arquitectura lógica similar pero con diferentes implicaciones.
Una implementación directa empaqueta las clases de políticas con clases de resúmenes de servicios en una biblioteca. En esta implementación, los componentes de alto nivel y los componentes de bajo nivel se distribuyen en paquetes / bibliotecas separados, donde las interfaces que definen el comportamiento / servicios requeridos por el componente de alto nivel son propiedad de y existen dentro de la biblioteca del componente de alto nivel. La implementación de la interfaz del componente de alto nivel por el 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.
Las figuras 1 y 2 ilustran 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 la 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:
La separación de cada capa en su propio paquete fomenta la reutilización de cualquier capa, proporcionando robustez y movilidad. [1]
Ejemplos de
Módulo genealógico
Un sistema genealógico puede representar las relaciones entre personas como un gráfico de las relaciones directas entre ellas (padre-hijo, padre-hija, madre-hijo, madre-hija, marido-mujer, mujer-marido, etc.). Esto es muy eficiente y extensible, ya que es fácil agregar un exmarido o un tutor legal. Los mecanismos deben implementar su interfaz, no la política. El diagrama es incorrecto.
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 medios hermanos y hermanas o no), abuelos, primos, etc.
Dependiendo del uso del módulo genealógico, la presentación de relaciones comunes como propiedades directas distintas (ocultando 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 permitirá cambiar completamente la representación interna de las relaciones directas. sin ningún efecto sobre los módulos que los utilizan. También permite incorporar definiciones exactas de hermanos o tíos en el módulo genealógico, reforzando 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 simple es suficiente para la aplicación o 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
Imagina que tienes que implementar un cliente a un servidor de archivos remoto (FTP, almacenamiento en la nube ...). Puede pensar en ello como un conjunto de interfaces abstractas:
- Conexión / Desconexión (puede ser necesaria una capa de persistencia de la conexión)
- Interfaz de creación / cambio de nombre / eliminación / lista de carpetas / etiquetas
- Interfaz de creación / reemplazo / cambio de nombre / eliminación / lectura de archivos
- Búsqueda de archivos
- Resolución de sustitución o eliminación simultánea
- Gestión del historial de archivos ...
Si tanto los archivos locales como los remotos ofrecen las mismas interfaces abstractas, cualquier módulo de alto nivel que utilice archivos locales e implemente por completo el patrón de inversión de dependencias podrá acceder a archivos locales y remotos de forma indiscriminada.
La funcionalidad del disco local generalmente usará carpetas, mientras que el almacenamiento remoto podría usar carpetas o etiquetas. Tienes que decidir cómo unificarlos si es posible. [ cita requerida ]
En el archivo remoto, es posible que tengamos que usar solo crear o reemplazar: la actualización de archivos remotos no necesariamente tiene sentido porque la actualización aleatoria es demasiado lenta comparando la actualización aleatoria de archivos locales y puede ser muy complicada de implementar). En el archivo remoto, es posible que necesitemos lectura y escritura parcial (al menos dentro del módulo de archivo remoto para permitir que la descarga o carga se reanude después de una interrupción de la comunicación), pero la lectura aleatoria no está adaptada (excepto si se usa una caché local).
La búsqueda de archivos se puede conectar: la búsqueda de archivos puede depender del sistema operativo o, en particular, para la búsqueda de etiquetas o texto completo, implementarse con distintos sistemas (sistema operativo integrado o disponible por separado).
La detección simultánea de la resolución de sustitución o eliminación puede afectar a las otras interfaces abstractas.
Al diseñar el cliente del servidor de archivos remoto para cada interfaz conceptual, debe preguntarse el nivel de servicio que requieren sus módulos de alto nivel (no son necesarios todos) y no solo cómo implementar las funcionalidades del servidor de archivos remoto, sino también cómo hacer el archivo. servicios en su aplicación compatibles entre servicios de archivos ya implementados (archivos locales, clientes en la nube existentes) y su nuevo cliente de servidor de archivos remoto.
Una vez que haya diseñado las interfaces abstractas necesarias, su cliente de servidor de archivos remoto debe implementar estas interfaces. Y debido a que probablemente restringió algunas funcionalidades locales existentes en el archivo local (por ejemplo, actualización de archivo), es posible que tenga que escribir adaptadores para módulos de acceso a archivos remotos usados locales u otros que ofrecen las mismas interfaces abstractas. También debe escribir su propio enumerador de acceso a archivos que le permita recuperar todos los sistemas compatibles con archivos disponibles y configurados en su computadora.
Una vez que lo haga, su aplicación podrá guardar sus documentos de forma local o remota de forma transparente. O más simplemente, el módulo de alto nivel que usa las nuevas interfaces de acceso a archivos se puede usar indistintamente en escenarios de acceso a archivos locales o remotos, haciéndolo reutilizable.
Tenga en cuenta que muchos sistemas operativos han comenzado a implementar este tipo de funcionalidades y su trabajo puede limitarse a adaptar su nuevo cliente a estos modelos ya abstraídos.
En este ejemplo, pensar en el módulo como un conjunto de interfaces abstractas y adaptar otros módulos a este conjunto de interfaces le permite proporcionar una interfaz común para muchos sistemas de almacenamiento de archivos.
Controlador de vista de modelo
Los paquetes UI y ApplicationLayer contienen principalmente clases concretas. Los controladores contienen resúmenes / tipos de interfaz. La interfaz de usuario tiene una instancia de ICustomerHandler. Todos los paquetes están separados físicamente. En ApplicationLayer hay una implementación concreta que utilizará la clase Page. Las instancias de esta interfaz son creadas dinámicamente por una fábrica (posiblemente en el mismo paquete de Controllers). Los tipos concretos, Page y CustomerHandler, no dependen unos de otros; ambos dependen de ICustomerHandler.
El efecto directo es que la interfaz de usuario no necesita hacer referencia al ApplicationLayer ni a ningún paquete concreto que implemente ICustomerHandler. La clase concreta se cargará mediante reflexión . En cualquier momento, la implementación concreta podría ser reemplazada por otra implementación concreta sin cambiar la clase UI. Otra posibilidad interesante es que la clase Page implementa una interfaz IPageViewer que podría pasarse como argumento a los métodos ICustomerHandler. Entonces, la implementación concreta podría comunicarse con la interfaz de usuario sin una dependencia concreta. Nuevamente, ambos están vinculados por interfaces.
Patrones relacionados
La aplicación del principio de inversión de dependencia también puede verse como un ejemplo del patrón de adaptador . Es decir, la clase de alto nivel define su propia interfaz de adaptador, que es la abstracción de la que dependen las otras clases de alto nivel. La implementación adaptada también depende necesariamente de la misma abstracción de interfaz de adaptador, mientras que puede implementarse utilizando código desde su propio módulo de bajo nivel. El módulo de alto nivel no depende del módulo de bajo nivel, ya que solo usa la funcionalidad de bajo nivel indirectamente a través de la interfaz del adaptador invocando métodos polimórficos a la interfaz que son implementados por la implementación adaptada y su módulo de bajo nivel. [ cita requerida ]
Se emplean varios patrones, como el complemento, el localizador de servicios o la inyección de dependencias, para facilitar el aprovisionamiento en tiempo de ejecución de la implementación del componente de bajo nivel elegido en el componente de alto nivel. [ cita requerida ]
Historia
El principio de inversión de dependencia fue postulado por Robert C. Martin y descrito en varias publicaciones, incluido el artículo Object Oriented Design Quality Metrics: an analysis of dependencies , [3] un artículo que aparece en el Informe C ++ en mayo de 1996 titulado The Dependency Inversion Principle , [ 4] y los libros Desarrollo de software ágil, principios, patrones y prácticas , [1] y Principios, patrones y prácticas ágiles en C # .
Ver también
- Patrón de adaptador
- Inyección de dependencia
- Diseño por contrato
- Interfaz
- Inversión de control
- Plug-in (informática)
- Patrón de localizador de servicios
- SÓLIDO : la "D" en "SÓLIDO" representa el principio de inversión de dependencia
- La paradoja del inventor
Referencias
- ↑ a b c d e f Martin, Robert C. (2003). Desarrollo de software ágil, principios, patrones y prácticas . Prentice Hall. págs. 127-131. ISBN 978-0135974445.
- ^ Freeman, Eric; Freeman, Elisabeth; Kathy, Sierra; Bert, Bates (2004). Hendrickson, Mike; Loukides, Mike (eds.). Head First Design Patterns (rústica) . 1 . O'REILLY. ISBN 978-0-596-00712-6. Consultado el 21 de junio de 2012 .
- ^ Martin, Robert C. (octubre de 1994). "Métricas de calidad del diseño orientado a objetos: un análisis de dependencias" (PDF) . Consultado el 15 de octubre de 2016 .
- ^ Martin, Robert C. (mayo de 1996). "El principio de inversión de dependencia" (PDF) . Informe C ++. Archivado desde el original (PDF) el 14 de julio de 2011.
enlaces externos
- Métricas de calidad del diseño orientado a objetos: un análisis de dependencias Robert C. Martin, Informe C ++, septiembre / octubre de 1995
- El principio de inversión de dependencia, Robert C. Martin, Informe C ++, mayo de 1996
- Examinando el principio de inversión de dependencia, Derek Greer
- DIP in the Wild, Brett L. Schuchert, mayo de 2013
- Contenedor de IoC para Unity3D - parte 2
- A Little Architecture, Robert C. Martin (tío Bob), enero de 2016: un blog sobre la importancia del principio de inversión de dependencias en la arquitectura de software