¿Cuándo NO aplicar el principio de inversión de dependencia?


43

Actualmente estoy tratando de resolver SOLID. Entonces, el Principio de Inversión de Dependencia significa que dos clases deben comunicarse a través de interfaces, no directamente. Ejemplo: si class Atiene un método, que espera un puntero a un objeto de tipo class B, entonces este método debería esperar un objeto de tipo abstract base class of B. Esto también ayuda para Abrir / Cerrar.

Siempre que lo haya entendido correctamente, mi pregunta sería si es una buena práctica aplicar esto a todas las interacciones de clase o debería tratar de pensar en términos de capas .

La razón por la que soy escéptico es porque estamos pagando un precio por seguir este principio. Digamos que necesito implementar la función Z. Después del análisis, concluyo que característica Zconsiste en la funcionalidad A, By C. Creo una fachada de clase Z, que, a través de interfaces, usa las clases A, By C. Comienzo a la codificación de la aplicación y en algún momento me doy cuenta de que la tarea Zconsiste en realidad en la funcionalidad A, By D. Ahora necesito desechar la Cinterfaz, el Cprototipo de la clase y escribir una Dinterfaz y una clase separadas . Sin interfaces, solo la clase debería haber sido reemplazada.

En otras palabras, para cambiar algo, necesito cambiar 1. la persona que llama 2. la interfaz 3. la declaración 4. la implementación. En una implementación de Python directamente acoplada, necesitaría cambiar solo la implementación.


13
La inversión de dependencia es simplemente una técnica, por lo que solo debe aplicarse cuando sea necesario ... no hay límite en el grado en que se puede aplicar, por lo que si lo aplica en todas partes, terminará con la basura: como en cualquier otra situación -específica técnica.
Frank Hileman

En pocas palabras, la aplicación de algunos principios de diseño de software depende de poder refactorizar sin piedad cuando los requisitos están cambiando. De ellos, se cree que la parte de la interfaz captura mejor los invariantes contractuales del diseño, mientras que se espera que el código fuente (implementación) tolere cambios más frecuentes.
rwong

@rwong Una interfaz captura invariantes contractuales solo si utiliza un lenguaje que admita invariantes contractuales. En lenguajes comunes (Java, C #), una interfaz es simplemente un conjunto de firmas API. Agregar interfaces superfluas solo degrada un diseño.
Frank Hileman

Diría que lo entendiste mal. DIP se trata de evitar dependencias en tiempo de compilación de un componente de "alto nivel" a uno de "bajo nivel", para permitir la reutilización del componente de alto nivel en otros contextos, donde utilizaría una implementación diferente para el bajo componente de nivel; esto se realiza creando un tipo abstracto en el nivel superior, que se implementa mediante los componentes de nivel bajo; entonces los componentes de alto y bajo nivel dependen de esta abstracción. Al final, los componentes de alto y bajo nivel se comunican a través de una interfaz, pero esta no es la esencia de DIP.
Rogério

Respuestas:


87

En muchas caricaturas u otros medios, las fuerzas del bien y del mal a menudo son ilustradas por un ángel y un demonio sentado sobre los hombros del personaje. En nuestra historia aquí, en lugar de lo bueno y lo malo, tenemos SÓLIDO en un hombro, y YAGNI (¡no lo necesitarás!) Sentado en el otro.

Los principios SOLID llevados al máximo son los más adecuados para sistemas empresariales enormes, complejos y ultraconfigurables. Para sistemas más pequeños o más específicos, no es apropiado hacer que todo sea ridículamente flexible, ya que el tiempo que pasa abstrayendo las cosas no resultará beneficioso.

Pasar interfaces en lugar de clases concretas a veces significa, por ejemplo, que puede intercambiar fácilmente la lectura de un archivo por una secuencia de red. Sin embargo, para una gran cantidad de proyectos de software, ese tipo de flexibilidad simplemente nunca será necesaria, y también podría pasar clases concretas de archivos y llamarlo un día y ahorrar sus células cerebrales.

Parte del arte del desarrollo de software es tener una buena idea de lo que es probable que cambie con el tiempo y lo que no. Para las cosas que es probable que cambien, use las interfaces y otros conceptos SÓLIDOS. Para las cosas que no funcionan, use YAGNI y simplemente pase tipos concretos, olvide las clases de fábrica, olvide todas las conexiones y configuraciones de tiempo de ejecución, etc., y olvide muchas de las abstracciones SÓLIDAS. En mi experiencia, el enfoque YAGNI ha demostrado ser correcto con mucha más frecuencia de lo que no lo es.


19
Mi primera introducción a SOLID fue hace unos 15 años en un nuevo sistema que estábamos construyendo. Todos bebimos el hombre de ayuda kool. Si alguien mencionara algo que sonara como YAGNI, seríamos como "Pfffft ... plebeian". Tuve el honor (¿horror?) De ver evolucionar ese sistema en el transcurso de la próxima década. Se convirtió en un desastre difícil de manejar que nadie podía entender, ni siquiera nosotros los fundadores. Los arquitectos aman SOLID. Las personas que realmente se ganan la vida aman a YAGNI. Ninguno de los dos es perfecto, pero YAGNI está más cerca de ser perfecto, y debería ser tu opción predeterminada si no sabes lo que estás haciendo. :-)
Calphool

11
@NWard Sí, lo hicimos en un proyecto. Se volvió loco con eso. Ahora nuestras pruebas son imposibles de leer o mantener, en parte debido a la burla excesiva. Además de eso, debido a la inyección de dependencia, es difícil navegar por el código cuando intentas resolver algo. SOLIDO no es una bala de plata. YAGNI no es una bala de plata. Las pruebas automatizadas no son una bala de plata. Nada puede salvarlo de hacer el arduo trabajo de pensar en lo que está haciendo y tomar decisiones sobre si ayudará o dificultará su trabajo o el de otra persona.
jpmc26

22
Mucho sentimiento anti-SÓLIDO aquí. SOLID y YAGNI no son dos extremos de un espectro. Son como las coordenadas X e Y en un gráfico. Un buen sistema tiene muy poco código superfluo (YAGNI) Y sigue los principios SÓLIDOS.
Stephen

31
Meh, (a) no estoy de acuerdo con que SOLID = enterprisey y (b) el objetivo de SOLID es que tendemos a ser pronosticadores extremadamente pobres de lo que se necesitará. Tengo que estar de acuerdo con @Stephen aquí. YAGNI dice que no debemos tratar de anticipar los requisitos futuros que no se detallan claramente hoy. SOLID dice que debemos esperar que el diseño evolucione con el tiempo y aplicar ciertas técnicas simples para facilitarlo. Ellos son no excluyentes entre sí; ambas son técnicas para adaptarse a los requisitos cambiantes. Los problemas reales ocurren cuando intenta diseñar para requisitos poco claros o muy distantes.
Aaronaught

8
"puede cambiar fácilmente la lectura de un archivo por un flujo de red": este es un buen ejemplo en el que una descripción demasiado simplificada de DI lleva a la gente por mal camino. La gente a veces piensa (en efecto), "este método usará un File, así que en su lugar tomará un IFile, trabajo hecho". Entonces no pueden sustituir fácilmente un flujo de red, porque exigieron demasiado la interfaz, y hay operaciones en IFileel método que ni siquiera se usan, que no se aplican a los sockets, por lo que un socket no puede implementarse IFile. Una de las cosas para las que DI no es una bala de plata es inventar las abstracciones (interfaces) correctas :-)
Steve Jessop

11

En palabras simples:

Aplicar el DIP es fácil y divertido . No obtener el diseño correcto en el primer intento no es razón suficiente para renunciar por completo al DIP.

  • Por lo general, los IDE lo ayudan a realizar ese tipo de refactorización, algunos incluso le permiten extraer la interfaz de una clase ya implementada
  • Es casi imposible hacer bien el diseño la primera vez
  • El flujo de trabajo normal implica cambiar y repensar las interfaces en las primeras etapas de desarrollo.
  • A medida que el desarrollo evoluciona, madura y tendrá menos razones para modificar las interfaces.
  • En una etapa avanzada, las interfaces (el diseño) serán maduras y apenas cambiarán.
  • A partir de ese momento, comienza a cosechar los beneficios, ya que su aplicación está abierta para ampliarse.

Por otro lado, la programación con interfaces y OOD puede devolver la alegría a la artesanía a veces obsoleta de la programación.

Algunas personas dicen que agrega complejidad, pero creo que lo opuesto es cierto. Incluso para pequeños proyectos. Facilita las pruebas / burlas. Hace que su código tenga menos si hay casedeclaraciones o anidadas ifs. Reduce la complejidad ciclomática y te hace pensar de maneras nuevas. Hace que la programación sea más parecida al diseño y la fabricación del mundo real.


55
No sé qué idiomas o IDE está utilizando el OP, pero en VS 2013 es ridículamente simple trabajar contra interfaces, extraer interfaces e implementarlas, y crítico si se usa TDD. No hay gastos adicionales de desarrollo para desarrollar utilizando estos principios.
stephenbayer

¿Por qué esta respuesta habla de DI si la pregunta era sobre DIP? DIP es un concepto de la década de 1990, mientras que DI es de 2004. Son muy diferentes.
Rogério

1
(Mi comentario anterior estaba destinado a otra respuesta; ignórelo). "La programación a interfaces" es mucho más general que DIP, pero no se trata de hacer que cada clase implemente una interfaz separada. Y solo hace que la "prueba / burla" sea más fácil si las herramientas de prueba / burla sufren serias limitaciones.
Rogério

@ Rogério Por lo general, cuando se usa DI, no todas las clases implementan una interfaz separada. Una interfaz implementada por varias clases es común.
Tulains Córdova

@ Rogério Corregí mi respuesta, cada vez que mencionaba DI me refería a DIP.
Tulains Córdova

9

Use la inversión de dependencia donde tenga sentido.

Un contraejemplo extremo es la clase "string" incluida en muchos idiomas. Representa un concepto primitivo, esencialmente una serie de caracteres. Suponiendo que pueda cambiar esta clase principal, no tiene sentido usar DI aquí porque nunca necesitará cambiar el estado interno con otra cosa.

Si tiene un grupo de objetos utilizados internamente en un módulo que no están expuestos a otros módulos o reutilizados en cualquier lugar, probablemente no valga la pena utilizar DI.

Hay dos lugares donde DI debería usarse automáticamente en mi opinión:

  1. En módulos diseñados para extensión. Si el propósito completo de un módulo es extenderlo y cambiar el comportamiento, tiene mucho sentido hornear DI desde el principio.

  2. En los módulos que está refactorizando con el fin de reutilizar el código. Tal vez haya codificado una clase para hacer algo, luego se dé cuenta de que con un refactor puede aprovechar ese código en otro lugar y es necesario hacerlo . Es un gran candidato para DI y otros cambios de extensibilidad.

Las claves aquí son usarlo donde sea necesario porque introducirá una complejidad adicional y asegúrese de medir esa necesidad a través de requisitos técnicos (punto uno) o revisión de código cuantitativa (punto dos).

DI es una gran herramienta, pero al igual que cualquier herramienta *, se puede usar en exceso o mal.

* Excepción a la regla anterior: una sierra recíproca es la herramienta perfecta para cualquier trabajo. Si no soluciona su problema, lo eliminará. Permanentemente.


3
¿Qué pasa si "tu problema" es un agujero en la pared? Una sierra no lo quitaría; Lo empeoraría. ;)
Mason Wheeler

3
@MasonWheeler con una sierra poderosa y divertida de usar, "agujero en la pared" podría convertirse en "puerta", que es un activo útil :-)

1
¿No puedes usar una sierra para hacer un parche para el agujero?
JeffO

Si bien existen algunas ventajas de tener un tipo no extensible por el usuario String, hay muchos casos en los que las representaciones alternativas serían útiles si el tipo tuviera un buen conjunto de operaciones virtuales (por ejemplo, copie una subcadena a una parte específica de un short[], informe si un la subcadena contiene o puede contener solo ASCII, intente copiar una subcadena que se cree que contiene solo ASCII a una parte específica de a byte[], etc.) Es una lástima que los marcos no tengan sus tipos de cadena implementados interfaces útiles relacionadas con la cadena.
supercat

1
¿Por qué esta respuesta habla de DI si la pregunta era sobre DIP? DIP es un concepto de la década de 1990, mientras que DI es de 2004. Son muy diferentes.
Rogério

5

Me parece que a la pregunta original le falta parte del punto del DIP.

La razón por la que soy escéptico es porque estamos pagando un precio por seguir este principio. Digamos, necesito implementar la característica Z. Después del análisis, concluyo que la característica Z consiste en la funcionalidad A, B y C. Creo una clase Z fascade, que, a través de interfaces, usa las clases A, B y C. Empiezo a codificar implementación y en algún momento me doy cuenta de que la tarea Z en realidad consiste en la funcionalidad A, B y D. Ahora necesito desechar la interfaz C, el prototipo de clase C y escribir una interfaz y clase D separadas. Sin interfaces, solo la clase wave debería ser reemplazada.

Para aprovechar realmente el DIP, primero debe crear la clase Z y hacer que llame a la funcionalidad de las clases A, B y C (que aún no se han desarrollado). Esto le proporciona la API para las clases A, B y C. Luego, vaya y cree las clases A, B y C y complete los detalles. Efectivamente, debería crear las abstracciones que necesita mientras crea la clase Z, basándose completamente en lo que necesita la clase Z. Incluso puede escribir pruebas alrededor de la clase Z antes de que se escriban las clases A, B o C.

Recuerde que el DIP dice que "los módulos de alto nivel no deberían depender de módulos de bajo nivel. Ambos deberían depender de abstracciones".

Una vez que haya resuelto qué necesita la clase Z y la forma en que quiere obtener lo que necesita, puede completar los detalles. Claro, a veces será necesario realizar cambios en la clase Z, pero el 99% de las veces este no será el caso.

Nunca habrá una clase D porque descubriste que Z necesita A, B y C antes de que se escribieran. Un cambio en los requisitos es una historia completamente diferente.


5

La respuesta corta es "casi nunca", pero hay, de hecho, algunos lugares donde el DIP no tiene sentido:

  1. Fábricas o constructores, cuyo trabajo es crear objetos. Estos son esencialmente los "nodos hoja" en un sistema que abarca completamente IoC. En algún momento, algo tiene que crear realmente sus objetos, y no puede depender de nada más para hacer eso. En muchos idiomas, un contenedor de IoC puede hacerlo por usted, pero a veces debe hacerlo a la antigua.

  2. Implementaciones de estructuras de datos y algoritmos. En general, en estos casos, las características sobresalientes para las que está optimizando (como el tiempo de ejecución y la complejidad asintótica) dependen de los tipos de datos específicos que se utilizan. Si está implementando una tabla hash, realmente necesita saber que está trabajando con una matriz para el almacenamiento, no con una lista vinculada, y solo la tabla en sí misma sabe cómo asignar adecuadamente las matrices. Tampoco desea pasar en una matriz mutable y que la persona que llama rompa su tabla hash jugando con su contenido.

  3. Clases de modelo de dominio . Estos implementan su lógica de negocios y (la mayoría de las veces) solo tiene sentido tener una implementación, porque (la mayoría de las veces) solo está desarrollando el software para un negocio. Aunque algunas clases de modelo de dominio pueden construirse utilizando otras clases de modelo de dominio, esto generalmente será caso por caso. Dado que los objetos del modelo de dominio no incluyen ninguna funcionalidad que pueda ser burlada útilmente, no hay beneficios de comprobabilidad o mantenibilidad para el DIP.

  4. Cualquier objeto que se proporciona como una API externa y necesita crear otros objetos, cuyos detalles de implementación no desea exponer públicamente. Esto cae dentro de la categoría general de "el diseño de la biblioteca es diferente del diseño de la aplicación". Una biblioteca o marco puede hacer un uso liberal de DI internamente, pero eventualmente tendrá que hacer un trabajo real, de lo contrario no es una biblioteca muy útil. Digamos que está desarrollando una biblioteca de redes; realmente no desea que el consumidor pueda proporcionar su propia implementación de un socket. Puede utilizar internamente una abstracción de un socket, pero la API que exponga a las personas que llaman creará sus propios sockets.

  5. Pruebas unitarias y dobles de prueba. Se supone que las falsificaciones y los talones hacen una cosa y lo hacen simplemente. Si tiene una falsificación que es lo suficientemente compleja como para preocuparse por si hacer o no una inyección de dependencia, entonces probablemente sea demasiado complejo (tal vez porque está implementando una interfaz que también es demasiado compleja).

Puede haber más; Estos son los que veo con cierta frecuencia.


¿Qué tal "cada vez que trabajas en un lenguaje dinámico"?
Kevin

¿No? Hago mucho trabajo en JavaScript y todavía se aplica igualmente bien allí. Sin embargo, la "O" y la "I" en SOLID pueden volverse un poco borrosas.
Aaronaught

Huh ... Me parece que los tipos de primera clase de Python combinados con la escritura de pato lo hacen menos necesario.
Kevin

El DIP no tiene absolutamente nada que ver con el sistema de tipos. ¿Y de qué manera los "tipos de primera clase" son exclusivos de Python? Cuando desee probar algo de forma aislada, se supone que debe sustituir los dobles de prueba por sus dependencias. Esos dobles de prueba pueden ser implementaciones alternativas de una interfaz (en lenguajes de tipo estático) o pueden ser objetos anónimos o tipos alternativos que tienen los mismos métodos / funciones (tipo de pato). En ambos casos, aún necesita una forma de sustituir una instancia.
Aaronaught

1
@Kevin python no fue el primer idioma en poseer una escritura dinámica o simulacros sueltos. También es completamente irrelevante. La pregunta no es qué tipo de objeto es sino cómo / dónde se crea ese objeto. Cuando un objeto crea sus propias dependencias, se ve obligado a realizar pruebas unitarias de lo que deberían ser detalles de implementación, al hacer cosas horribles, como eliminar los constructores de clases que la API pública no menciona. Y olvidarse de las pruebas, el comportamiento de mezcla y la construcción de objetos simplemente conduce a un acoplamiento estrecho. La escritura de pato no resuelve ninguno de esos problemas.
Aaronaught

2

Algunos signos de que puede estar aplicando DIP a un nivel demasiado micro, donde no proporciona valor:

  • tiene un par C / CImpl o IC / C, con una sola implementación de esa interfaz
  • las firmas en su interfaz e implementación coinciden uno a uno (violando el principio DRY)
  • cambias frecuentemente C y CImpl al mismo tiempo.
  • C es interno a su proyecto y no se comparte fuera de su proyecto como una biblioteca.
  • te sientes frustrado por F3 en Eclipse / F12 en Visual Studio que te lleva a la interfaz en lugar de la clase real

Si esto es lo que está viendo, es mejor que Z llame directamente a C y omita la interfaz.

Además, no pienso en la decoración de métodos mediante un marco de proxy dinámico / inyección de dependencia (Spring, Java EE) de la misma manera que el DIP SÓLIDO verdadero: esto es más como un detalle de implementación de cómo funciona la decoración de métodos en esa pila tecnológica. La comunidad Java EE considera que es una mejora que no necesita pares Foo / FooImpl como solía ( referencia ). Por el contrario, Python admite la decoración de funciones como una característica de lenguaje de primera clase.

Vea también esta publicación de blog .


0

Si siempre invierte sus dependencias, todas sus dependencias están al revés. Lo que significa que si comenzó con un código desordenado con un nudo de dependencias, eso es lo que todavía tiene (realmente), simplemente invertido. Aquí es donde surge el problema de que cada cambio en una implementación también necesita cambiar su interfaz.

El punto de inversión de dependencia es que inviertes selectivamente las dependencias que están enredando las cosas. Los que deberían ir de A a B a C todavía lo hacen, los que iban de C a A ahora van de A a C.

El resultado debería ser un gráfico de dependencia libre de ciclos: un DAG. Hay varias herramientas que verificarán esta propiedad y dibujarán el gráfico.

Para una explicación más completa, vea este artículo :

La esencia de aplicar correctamente el Principio de Inversión de Dependencia es esta:

Divida el código / servicio / ... del que depende en una interfaz e implementación. La interfaz reestructura la dependencia en la jerga del código que lo usa, la implementación lo implementa en términos de sus técnicas subyacentes.

La implementación permanece donde está. Pero la interfaz tiene una función diferente ahora (y usa una jerga / lenguaje diferente), que describe algo que puede hacer el código que usa. Muévelo a ese paquete. Al no colocar la interfaz y la implementación en el mismo paquete, la dependencia (dirección de la) se invierte de usuario → implementación a implementación → usuario.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.