oop - segregacion - ¿Cuál es el principio de inversión de dependencia y por qué es importante?
principios solid ejemplos java (12)
¿Cuál es el principio de inversión de dependencia y por qué es importante?
Básicamente dice:
La clase debe depender de abstracciones (por ejemplo, interfaz, clases abstractas), no detalles específicos (implementaciones).
Buenas respuestas y buenos ejemplos ya han sido dados por otros aquí.
La razón por la cual DIP es importante es porque asegura el principio de OO "diseño acoplado libremente".
Los objetos en su software NO deben entrar en una jerarquía donde algunos objetos son los de nivel superior, que dependen de objetos de bajo nivel. Los cambios en los objetos de bajo nivel se propagarán a sus objetos de nivel superior, lo que hace que el software sea muy frágil para el cambio.
Desea que sus objetos de "nivel superior" sean muy estables y no frágiles para el cambio, por lo tanto, debe invertir las dependencias.
Creo que tengo un ejemplo mucho mejor (más intuitivo).
- Imagine un sistema (aplicación web) con empleados y administración de contactos (dos pantallas).
- No están exactamente relacionados por lo que los quiere cada uno en su propio módulo / carpeta
Por lo tanto, tendría un punto de entrada "principal" que debería conocer tanto el módulo de gestión del empleado como el módulo de gestión de contactos, y debería proporcionar enlaces en la navegación y aceptar solicitudes de API, etc. En otras palabras, el módulo principal dependería en estos dos aspectos, sabrá sobre sus controladores, rutas y enlaces que deben ser renderizados en navegación (compartida).
Ejemplo de Node.js
// main.js
import express from ''express''
// two modules, each having many exports
import { api as contactsApi, navigation as cNav } from ''./contacts/''
import { api as employeesApi, navigation as eNav } from ''./employees/''
const api = express()
const navigation = {
...cNav,
...eNav
}
api.use(''contacts'', contactsApi)
api.use(''employees'', employeesApi)
// do something with navigation, possibly do some other setup
Además, tenga en cuenta que hay casos (simples) cuando esto está totalmente bien.
Por lo tanto, con el tiempo llegará a un punto en el que no es tan trivial agregar nuevos módulos. Debes recordar registrar api, navegación, quizás permisos , y este main.js se hace cada vez más grande.
Y ahí es donde entra la inversión de dependencia. En lugar de que su módulo principal dependa de todos los demás módulos, introducirá algunos "núcleos" y hará que cada módulo se registre solo.
Entonces, en este caso se trata de tener una noción de algún ApplicationModule, que puede enviarse a muchos servicios (rutas, navegación, permisos) y el módulo principal puede seguir siendo simple (solo importar el módulo y dejarlo instalar)
En otras palabras, se trata de hacer una arquitectura conectable. Este es un trabajo y código adicional que tendrá que escribir / leer y mantener, por lo que no debe hacerlo por adelantado, sino cuando tiene este tipo de olor.
Lo que es especialmente interesante es que puede convertir cualquier cosa en un complemento, incluso en la capa de persistencia, lo que podría valer la pena si necesita admitir muchas implementaciones de persistencia, pero ese no suele ser el caso. Vea la otra respuesta para la imagen con arquitectura hexagonal, es genial para la ilustración: hay un núcleo y todo lo demás es esencialmente un complemento.
Cuando diseñamos aplicaciones de software podemos considerar las clases de bajo nivel las clases que implementan operaciones básicas y primarias (acceso a disco, protocolos de red, ...) y clases de alto nivel las clases que encapsulan lógica compleja (flujos de negocios, ...).
Los últimos dependen de las clases de bajo nivel. Una forma natural de implementar tales estructuras sería escribir clases de bajo nivel y una vez que las tengamos para escribir las complejas clases de alto nivel. Como las clases de alto nivel se definen en términos de otras, esta parece ser la forma lógica de hacerlo. Pero este no es un diseño flexible. ¿Qué sucede si tenemos que reemplazar una clase de bajo nivel?
El Principio de Inversión de Dependencia establece que:
- Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de las abstracciones.
- Las abstracciones no deberían depender de detalles. Los detalles deben depender de las abstracciones.
Este principio busca "invertir" la noción convencional de que los módulos de alto nivel en el software deberían depender de los módulos de nivel inferior. Aquí los módulos de alto nivel poseen la abstracción (por ejemplo, decidir los métodos de la interfaz) que son implementados por módulos de nivel inferior. Por lo tanto, hacer que los módulos de nivel inferior dependan de módulos de nivel superior.
El punto de inversión de la dependencia es hacer software reutilizable.
La idea es que en lugar de dos piezas de código que confían entre sí, confíen en alguna interfaz abstracta. Entonces puedes reutilizar cualquiera de las piezas sin la otra.
La forma más común de lograr esto es a través de un contenedor de inversión de control (IoC) como Spring en Java. En este modelo, las propiedades de los objetos se configuran a través de una configuración XML en lugar de que los objetos salgan y encuentren su dependencia.
Imagina este pseudocódigo ...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass depende directamente de la clase de servicio y de la clase ServiceLocator. Necesita ambos si desea usarlo en otra aplicación. Ahora imagina esto ...
public class MyClass
{
public IService myService;
}
Ahora, MyClass se basa en una sola interfaz, la interfaz IService. Dejamos que el contenedor IoC realmente establezca el valor de esa variable.
Así que ahora, MyClass se puede reutilizar fácilmente en otros proyectos, sin agregar la dependencia de esas otras dos clases.
Aún mejor, no tiene que arrastrar las dependencias de MyService, ni las dependencias de esas dependencias, y ... bueno, ya entendió la idea.
La inversión de la dependencia bien aplicada proporciona flexibilidad y estabilidad a nivel de toda la arquitectura de su aplicación. Permitirá que su aplicación evolucione de manera más segura y estable.
Arquitectura tradicional en capas
Tradicionalmente, una UI de arquitectura en capas dependía de la capa de negocio y esto, a su vez, dependía de la capa de acceso a datos.
http://xurxodev.com/content/images/2016/02/Traditional-Layered.png
Debe comprender la capa, el paquete o la biblioteca. Veamos cómo sería el código.
Tendríamos una biblioteca o paquete para la capa de acceso a datos.
// DataAccessLayer.dll
public class ProductDAO {
}
Y otra biblioteca o lógica de negocio de capa de paquete que depende de la capa de acceso a datos.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
Arquitectura en capas con inversión de dependencia
La inversión de dependencia indica lo siguiente:
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de abstracciones.
Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.
¿Cuáles son los módulos de alto nivel y bajo nivel? Los módulos de pensamiento tales como bibliotecas o paquetes, módulo de alto nivel serían aquellos que tradicionalmente tienen dependencias y bajo nivel del que dependen.
En otras palabras, el nivel alto del módulo sería donde se invoca la acción y el nivel bajo donde se realiza la acción.
Una conclusión razonable para extraer de este principio es que no debe haber dependencia entre concreciones, pero debe haber una dependencia de una abstracción. Pero de acuerdo con el enfoque que tomamos, podemos estar aplicando erróneamente la dependencia de la inversión dependiente, pero una abstracción.
Imagine que adaptamos nuestro código de la siguiente manera:
Tendríamos una biblioteca o paquete para la capa de acceso a datos que define la abstracción.
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
Y otra biblioteca o lógica de negocio de capa de paquete que depende de la capa de acceso a datos.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
Aunque dependemos de una abstracción, la dependencia entre las empresas y el acceso a los datos sigue siendo la misma.
http://xurxodev.com/content/images/2016/02/Traditional-Layered.png
Para obtener la inversión de dependencia, la interfaz de persistencia se debe definir en el módulo o paquete donde se encuentra esta lógica o dominio de alto nivel y no en el módulo de bajo nivel.
Primero defina qué es la capa de dominio y la abstracción de su comunicación se define como persistencia.
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
Después de que la capa de persistencia dependa del dominio, invierta ahora si se define una dependencia.
// Persistence.dll
public class ProductDAO : IProductRepository{
}
http://xurxodev.com/content/images/2016/02/Dependency-Inversion-Layers.png
Profundizando el principio
Es importante asimilar bien el concepto, profundizando el propósito y los beneficios. Si permanecemos mecánicamente y aprendemos el repositorio típico de casos, no podremos identificar dónde podemos aplicar el principio de dependencia.
Pero, ¿por qué invertimos una dependencia? ¿Cuál es el objetivo principal más allá de ejemplos específicos?
Tal comúnmente permite que las cosas más estables, que no dependen de cosas menos estables, cambien con mayor frecuencia.
Es más fácil cambiar el tipo de persistencia, ya sea la base de datos o la tecnología para acceder a la misma base de datos que la lógica del dominio o las acciones diseñadas para comunicarse con persistencia. Debido a esto, la dependencia se invierte porque es más fácil cambiar la persistencia si ocurre este cambio. De esta forma no tendremos que cambiar el dominio. La capa de dominio es la más estable de todas, por lo que no debe depender de nada.
Pero no es solo este ejemplo de repositorio. Hay muchos escenarios donde se aplica este principio y hay arquitecturas basadas en este principio.
Arquitecturas
Hay arquitecturas donde la inversión de la dependencia es clave para su definición. En todos los dominios es el más importante y son las abstracciones las que indicarán el protocolo de comunicación entre el dominio y el resto de los paquetes o bibliotecas.
Arquitectura limpia
En Arquitectura limpia, el dominio está ubicado en el centro y si miras en la dirección de las flechas que indican dependencia, está claro cuáles son las capas más importantes y estables. Las capas externas se consideran herramientas inestables, así que evita depender de ellas.
Arquitectura Hexagonal
Sucede de la misma manera con la arquitectura hexagonal, donde el dominio también se ubica en la parte central y los puertos son abstracciones de comunicación desde el domino hacia afuera. Aquí nuevamente es evidente que el dominio es la dependencia más estable y tradicional que se invierte.
Los libros Desarrollo de software ágil, Principios, patrones y prácticas y Principios, patrones y prácticas ágiles en C # son los mejores recursos para comprender completamente las metas y motivaciones originales detrás del Principio de inversión de dependencia. El artículo "El Principio de Inversión de Dependencia" también es un buen recurso, pero debido a que es una versión condensada de un borrador que finalmente se abrió paso en los libros mencionados anteriormente, deja fuera una discusión importante sobre el concepto de propiedad del paquete y la interfaz que son clave para distinguir este principio de los consejos más generales para "programar una interfaz, no una implementación" que se encuentra dentro del libro Patrones de diseño (Gamma, et al.).
Para proporcionar un resumen, el Principio de Inversión de Dependencia se trata principalmente de invertir la dirección convencional de dependencias de componentes de "nivel superior" a componentes de "nivel inferior" tales que los componentes de "nivel inferior" dependen de las interfaces propiedad de los componentes de "nivel superior" . (Nota: el componente de "nivel superior" aquí se refiere al componente que requiere dependencias / servicios externos, no necesariamente su posición conceptual dentro de una arquitectura en capas.) Al hacerlo, el acoplamiento no se reduce tanto como se desplaza de componentes teóricamente menos valioso para reutilizar a los componentes que son teóricamente más valiosos para la reutilización.
Esto se logra diseñando componentes cuyas dependencias externas se expresan en términos de una interfaz para la cual el consumidor del componente debe proporcionar una implementación. En otras palabras, las interfaces definidas expresan lo que necesita el componente, no cómo se usa el componente (por ejemplo, "INeedSomething", no "IDoSomething").
A lo que no se refiere el Principio de Inversión de Dependencia es la práctica simple de abstraer dependencias mediante el uso de interfaces (por ejemplo, MyService → [ILogger ⇐ Logger]). Si bien esto desacopla un componente del detalle de implementación específico de la dependencia, no invierte la relación entre el consumidor y la dependencia (por ejemplo, [MyService → IMyServiceLogger] ⇐ Logger).
La importancia del Principio de Inversión de Dependencia se ve principalmente en el desarrollo de componentes de software reutilizables que dependen de dependencias externas (registro, validación, etc.) ya que tomar dependencias de tales dependencias requiere que los consumidores también requieran las mismas dependencias. Esto puede ser problemático cuando los consumidores de su biblioteca eligen utilizar una biblioteca diferente para las mismas necesidades de infraestructura (por ejemplo, NLog vs. log4net), o si eligen usar una versión posterior de la biblioteca requerida que no es compatible con la versión anterior requerido por su biblioteca.
here se puede encontrar una discusión más extensa de este principio en lo que se refiere al uso simple de interfaces, Inyección de Dependencia y el patrón de Interfaz Separada.
Para mí, el Principio de Inversión de Dependencia, como se describe en el artículo oficial , es realmente un intento equivocado de aumentar la reutilización de módulos que son inherentemente menos reutilizables, así como una forma de solucionar un problema en el lenguaje C ++.
El problema en C ++ es que los archivos de encabezado suelen contener declaraciones de campos y métodos privados. Por lo tanto, si un módulo C ++ de alto nivel incluye el archivo de encabezado para un módulo de bajo nivel, dependerá de los detalles de implementación reales de ese módulo. Y eso, obviamente, no es algo bueno. Pero esto no es un problema en los lenguajes más modernos comúnmente utilizados en la actualidad.
Los módulos de alto nivel son intrínsecamente menos reutilizables que los módulos de bajo nivel porque los primeros son normalmente más específicos de aplicación / contexto que los últimos. Por ejemplo, un componente que implementa una pantalla de interfaz de usuario es del más alto nivel y también muy (¿completamente?) Específico para la aplicación. Intentar reutilizar dicho componente en una aplicación diferente es contraproducente y solo puede llevar a una sobreingeniería.
Entonces, la creación de una abstracción separada en el mismo nivel de un componente A que depende de un componente B (que no depende de A) solo puede hacerse si el componente A realmente será útil para la reutilización en diferentes aplicaciones o contextos. Si ese no es el caso, entonces aplicar DIP sería un mal diseño.
Una forma mucho más clara de establecer el Principio de Inversión de Dependencia es:
Sus módulos que encapsulan la lógica empresarial compleja no deben depender directamente de otros módulos que encapsulan la lógica comercial. En cambio, deberían depender solo de las interfaces con datos simples.
Es decir, en lugar de implementar su clase de Logic
como suele hacer la gente:
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
deberías hacer algo como:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
y DataFromDependency
deben vivir en el mismo módulo que Logic
, no con Dependency
.
¿Por qué hacer esto?
- Los dos módulos de lógica de negocios ahora están desacoplados. Cuando cambia la
Dependency
, no es necesario que cambie laLogic
. - Comprender lo que hace
Logic
es una tarea mucho más simple: opera solo en lo que parece un ADT. -
Logic
ahora se puede probar más fácilmente. Ahora puede crear instancias directas de losData
con datos falsos y pasarlos. No es necesario realizar simulaciones o andamios de prueba complejos.
Vea este documento: El Principio de Inversión de Dependencia .
Básicamente dice:
- Los módulos de alto nivel no deberían depender de módulos de bajo nivel. Ambos deberían depender de las abstracciones.
- Las abstracciones nunca deben depender de los detalles. Los detalles deben depender de las abstracciones.
En cuanto a por qué es importante, en resumen: los cambios son riesgosos, y al depender de un concepto en lugar de una implementación, se reduce la necesidad de cambio en los sitios de llamadas.
Efectivamente, el DIP reduce el acoplamiento entre diferentes piezas de código. La idea es que, aunque hay muchas maneras de implementar, por ejemplo, una instalación de registro, la forma en que la usaría debería ser relativamente estable en el tiempo. Si puede extraer una interfaz que represente el concepto de registro, esta interfaz debería ser mucho más estable en el tiempo que su implementación, y los sitios de llamadas deberían verse mucho menos afectados por los cambios que podría realizar mientras mantiene o amplía ese mecanismo de registro.
Al hacer que la implementación dependa de una interfaz, usted tiene la posibilidad de elegir en el tiempo de ejecución qué implementación se adapta mejor a su entorno particular. Dependiendo de los casos, esto también puede ser interesante.
La inversión de control (IoC) es un patrón de diseño donde un objeto obtiene su dependencia por un marco externo, en lugar de pedirle un marco para su dependencia.
Ejemplo de pseudocódigo utilizando la búsqueda tradicional:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
Código similar con IoC:
class Service {
Database database;
init(database) {
this.database = database;
}
}
Los beneficios de IoC son:
- No tiene dependencia de un marco central, por lo que puede cambiarse si lo desea.
- Dado que los objetos se crean por inyección, preferiblemente utilizando interfaces, es fácil crear pruebas unitarias que reemplacen las dependencias con versiones simuladas.
- Desacoplando el código.
La inversión de los Contenedores de Control y el patrón de Inyección de Dependencia de Martin Fowler es una buena lectura también. Encontré Head First Design Patterns un libro impresionante para mi primera incursión en el aprendizaje de DI y otros patrones.