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 A
tiene 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 Z
consiste en la funcionalidad A
, B
y C
. Creo una fachada de clase Z
, que, a través de interfaces, usa las clases A
, B
y C
. Comienzo a la codificación de la aplicación y en algún momento me doy cuenta de que la tarea Z
consiste en realidad en la funcionalidad A
, B
y D
. Ahora necesito desechar la C
interfaz, el C
prototipo de la clase y escribir una D
interfaz 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.
Respuestas:
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.
fuente
File
, así que en su lugar tomará unIFile
, trabajo hecho". Entonces no pueden sustituir fácilmente un flujo de red, porque exigieron demasiado la interfaz, y hay operaciones enIFile
el método que ni siquiera se usan, que no se aplican a los sockets, por lo que un socket no puede implementarseIFile
. Una de las cosas para las que DI no es una bala de plata es inventar las abstracciones (interfaces) correctas :-)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 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
case
declaraciones o anidadasifs
. 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.fuente
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:
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.
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.
fuente
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 unshort[]
, 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 abyte[]
, etc.) Es una lástima que los marcos no tengan sus tipos de cadena implementados interfaces útiles relacionadas con la cadena.Me parece que a la pregunta original le falta parte del punto del DIP.
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.
fuente
La respuesta corta es "casi nunca", pero hay, de hecho, algunos lugares donde el DIP no tiene sentido:
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.
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.
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.
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.
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.
fuente
Algunos signos de que puede estar aplicando DIP a un nivel demasiado micro, donde no proporciona valor:
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 .
fuente
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 :
fuente