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

43

Actualmente estoy tratando de averiguar SOLID. Por lo tanto, el principio de inversión de dependencia significa que cualquiera de las dos clases debe comunicarse a través de interfaces, no directamente. Ejemplo: Si class A tiene un método, que espera un puntero a un objeto de tipo class B , entonces este método debería esperar realmente un objeto de tipo abstract base class of B . Esto también es útil para Abrir / Cerrar.

Siempre que haya entendido correctamente, mi pregunta sería ¿es una buena práctica aplicar esto a todas interacciones de clase o debería intentar 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 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 fachada Z , que, a través de interfaces, usa las clases A , B y C . Comienzo a codificar la 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 la interfaz y clase D por separado. Sin interfaces, solo habría que reemplazar la clase.

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 acoplada directamente a python, necesitaría cambiar solo la implementación.

    
pregunta Vorac 25.02.2015 - 19:26

7 respuestas

86

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

Los principios de

SOLID llevados al máximo son los más adecuados para sistemas empresariales enormes, complejos y ultra configurables. 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 dedique a abstraer cosas no será un beneficio.

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 no será nunca necesaria, y es mejor que apruebes clases concretas de archivos, que sea un día y que ahorres a tus neuronas. .

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 SOLIDOS. Para las cosas que no lo son, use YAGNI y solo pase tipos concretos, olvide las clases de fábrica, olvide todo el tiempo de ejecución y la configuración, etc., y olvide muchas de las abstracciones de SOLID. En mi experiencia, el enfoque YAGNI ha demostrado ser correcto con mucha más frecuencia de lo que no lo es.

    
respondido por el whatsisname 25.02.2015 - 20:27
11

En palabras sencillas:

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 te ayudan a hacer ese tipo de refactorización, algunos incluso te permiten extraer la interfaz de una clase ya implementada
  • Es casi imposible hacer el diseño correctamente 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) estarán maduras y apenas cambiarán
  • A partir de ese momento, comenzarás a cosechar los beneficios, ya que tu aplicación está abierta para escalar.

Por otra parte, la programación con interfaces y OOD puede devolverle la alegría a la artesanía a veces obsoleta de la programación.

Algunas personas dicen que agrega complejidad, pero creo que el sitio opuesto es cierto. Incluso para pequeños proyectos. Facilita las pruebas / burla. Hace que su código tenga menos sentencias case o ifs anidado. Reduce la complejidad ciclomática y te hace pensar en formas nuevas. Hace que la programación sea más parecida al diseño y fabricación del mundo real.

    
respondido por el Tulains Córdova 25.02.2015 - 19:31
9

Use inversión de dependencia donde tenga sentido.

Un contraejemplo extremo es la clase de "cadena" incluida en muchos idiomas. Representa un concepto primitivo, esencialmente una matriz 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 ni reutilizados en cualquier lugar, probablemente no valga la pena el esfuerzo de usar DI.

En mi opinión, hay dos lugares donde se debería usar DI automáticamente:

  1. En los módulos diseñados para extensión. Si todo el propósito 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 propósito de reutilizar el código. Tal vez haya codificado una clase para hacer algo, y luego darse cuenta de que con un refactor puede usar ese código en otra parte y es necesario hacerlo . Ese es un gran candidato para DI y otros cambios de extensibilidad.

Las claves aquí son utilícelas donde sea necesario porque introducirá una complejidad adicional, y se asegurará de que mida esa necesidad a través de requisitos técnicos (punto uno) o Revisión cuantitativa del código (punto dos).

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

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

    
respondido por el user22815 25.02.2015 - 19:58
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. Digo, 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 de fascade Z, que, a través de interfaces, usa las clases A, B y C. Comienzo a codificar la 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 tendría que 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 están desarrolladas). Esto le da la API para las clases A, B y C. Luego, vaya y cree las clases A, B y C y complete los detalles. Efectivamente, debe crear las abstracciones que necesita mientras crea la clase Z, basándose completamente en lo que la clase Z necesita. Incluso puede escribir pruebas alrededor de la clase Z antes de que las clases A, B o C estén escritas.

Recuerde que el DIP dice que "Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de las abstracciones".

Una vez que haya averiguado qué necesita la clase Z y la forma en que desea 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 no será así.

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

    
respondido por el Stephen 26.02.2015 - 02:02
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 de hoja" en un sistema que abarca completamente la IoC. En algún momento, algo tiene que crear realmente sus objetos y no puede depender de otra cosa para hacer eso. En muchos idiomas, un contenedor de IoC puede hacerlo por usted, pero a veces necesita hacerlo de la manera 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 enlazada, y solo la tabla misma sabe cómo asignar las matrices correctamente. Tampoco desea pasar una matriz mutable y hacer que la persona que llama rompa su tabla hash jugando con su contenido.

  3. clases de modelo de dominio . Estas implementan la lógica de su negocio 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 una empresa. Aunque algunas clases de modelo de dominio pueden construirse utilizando otras clases de modelo de dominio, esto generalmente se hará caso por caso. Dado que los objetos del modelo de dominio no incluyen ninguna funcionalidad que pueda simularse de manera útil, no hay beneficios probables o de mantenimiento para el DIP.

  4. Cualquier objeto que se proporcione como una API externa y necesite crear otros objetos, cuyos detalles de implementación no desee exponer públicamente. Esto cae bajo 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ás desarrollando una biblioteca de redes; realmente no quiere que el consumidor pueda proporcionar su propia implementación de un socket. Puede usar internamente una abstracción de un socket, pero la API que exponga a los llamantes creará sus propios sockets.

  5. Pruebas unitarias, y pruebas de dobles. Falsos y talones se supone que deben hacer una cosa y hacerlo simplemente. Si tiene una falsificación que es lo suficientemente compleja como para preocuparse por hacer o no la inyección de dependencia, entonces probablemente sea demasiado compleja (tal vez porque está implementando una interfaz que también es demasiado compleja).

Puede haber más; estos son los que veo con frecuencia un poco .

    
respondido por el Aaronaught 26.02.2015 - 06:20
2

Algunas señales indican que puede estar aplicando DIP a un nivel demasiado micro, donde no está proporcionando valor:

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

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

Además, no pienso en la decoración de métodos mediante un marco de inyección de dependencia / proxy dinámico (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 tecnología apilar. La comunidad Java EE considera que es una mejora que no necesita pares Foo / FooImpl como solía hacerlo ( reference ). Por el contrario, Python admite la decoración de funciones como una característica de lenguaje de primera clase.

Consulte también esta publicación del blog .

    
respondido por el wrschneider 04.11.2015 - 22:48
0

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

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

El resultado debe ser un gráfico de dependencia libre de ciclos, un DAG. Hay varios tools que comprobará esta propiedad y dibujará el gráfico.

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

  

La esencia de aplicar el principio de inversión de dependencia correctamente es la siguiente:

     

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 la usa, la implementación la 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 utiliza una jerga / idioma diferente), que describe algo que el código de uso puede hacer. Moverlo a ese paquete.   Al no colocar la interfaz y la implementación en el mismo paquete, la (dirección de) la dependencia se invierte de usuario → implementación a implementación → usuario.

    
respondido por el soru 25.02.2015 - 23:32

Lea otras preguntas en las etiquetas