oop - patron - ¿Debe la inyección de dependencia venir a expensas de la encapsulación?
patrones de diseño (21)
Si entiendo correctamente, el mecanismo típico para Dependency Injection es inyectar a través del constructor de una clase o a través de una propiedad pública (miembro) de la clase.
Esto expone la dependencia que se inyecta y viola el principio de encapsulación de OOP.
¿Estoy en lo cierto al identificar esta compensación? ¿Cómo lidias con este problema?
También vea mi respuesta a mi propia pregunta a continuación.
Esto expone la dependencia que se inyecta y viola el principio de encapsulación de OOP.
Bueno, francamente, todo viola la encapsulación. :) Es una especie de principio tierno que debe tratarse bien.
Entonces, ¿qué viola la encapsulación?
La herencia lo hace .
"Debido a que la herencia expone una subclase a los detalles de la implementación de su principal, a menudo se dice que ''la herencia rompe la encapsulación''". (Gang of Four 1995: 19)
La programación orientada a aspectos sí lo hace . Por ejemplo, te registras en Callback CallMethodCall () y eso te da una gran oportunidad para inyectar código a la evaluación del método normal, añadiendo extraños efectos secundarios, etc.
La declaración de amigo en C ++ sí lo hace .
La extensión de clase en Ruby sí lo hace . Simplemente redefina un método de cadena en algún lugar después de que una clase de cadena esté completamente definida.
Bueno, muchas cosas lo hacen .
La encapsulación es un principio bueno e importante. Pero no el único.
switch (principle)
{
case encapsulation:
if (there_is_a_reason)
break!
}
Como señaló Jeff Sternal en un comentario a la pregunta, la respuesta depende completamente de cómo defina la encapsulación .
Parece que hay dos campos principales de lo que significa la encapsulación:
- Todo lo relacionado con el objeto es un método sobre un objeto. Entonces, un objeto
File
puede tener métodos paraSave
,Print
,Display
,ModifyText
, etc. - Un objeto es su pequeño mundo y no depende del comportamiento externo.
Estas dos definiciones están en contradicción directa entre sí. Si un objeto File
puede imprimirse por sí mismo, dependerá en gran medida del comportamiento de la impresora. Por otro lado, si simplemente conoce algo que puede imprimir (una IFilePrinter
o alguna de esas interfaces), entonces el objeto File
no tiene que saber nada sobre la impresión, por lo que trabajar con ella traerá menos dependencias al objeto.
Entonces, la inyección de dependencia romperá la encapsulación si usa la primera definición. Pero, francamente, no sé si me gusta la primera definición, claramente no escala (si lo hiciera, MS Word sería una gran clase).
Por otro lado, la inyección de dependencia es casi obligatoria si está utilizando la segunda definición de encapsulación.
Creo que es una cuestión de alcance. Cuando define la encapsulación (sin dejar saber cómo), debe definir qué es la funcionalidad encapsulada.
Clase tal como es : lo que está encapsulando es la única responsabilidad de la clase. Lo que sabe hacer Por ejemplo, clasificación. Si se inyecta un comparador para ordenar, digamos, clientes, eso no forma parte de lo encapsulado: quicksort.
Configured functionality : if you want to provide a ready-to-use functionality then you are not providing QuickSort class, but an instance of QuickSort class configured with a Comparator. In that case the code responsible for creating and configuring that must be hidden from the user code. And that''s the encapsulation.
When you are programming classes, it is, implementing single responsibilities into classes, you are using option 1.
When you are programming applications, it is, making something that undertakes some useful concrete work then you are repeteadily using option 2.
This is the implementation of the configured instance:
<bean id="clientSorter" class="QuickSort">
<property name="comparator">
<bean class="ClientComparator"/>
</property>
</bean>
This is how some other client code use it:
<bean id="clientService" class"...">
<property name="sorter" ref="clientSorter"/>
</bean>
It is encapsulated because if you change implementation (you change clientSorter
bean definition) it doesn''t break client use. Maybe, as you use xml files with all written together you are seeing all the details. But believe me, the client code ( ClientService
) don''t know nothing about its sorter.
Depende de si la dependencia es realmente un detalle de implementación o algo que el cliente quisiera / necesita saber de una forma u otra. Una cosa que es relevante es a qué nivel de abstracción se dirige la clase. Aquí hay unos ejemplos:
Si tiene un método que utiliza el almacenamiento en caché debajo del capó para acelerar las llamadas, entonces el objeto de caché debe ser Singleton o algo así y no debe ser inyectado. El hecho de que el caché se esté utilizando en absoluto es un detalle de implementación que los clientes de su clase no deberían tener en cuenta.
Si su clase necesita generar flujos de datos, probablemente tenga sentido inyectar la secuencia de salida para que la clase pueda generar fácilmente los resultados en una matriz, un archivo o en cualquier otro lugar en el que alguien más desee enviar los datos.
Para un área gris, digamos que tienes una clase que hace algo de simulación de monte carlo. Necesita una fuente de aleatoriedad. Por un lado, el hecho de que necesita esto es un detalle de implementación en el sentido de que al cliente realmente no le importa exactamente de dónde proviene la aleatoriedad. Por otro lado, dado que los generadores de números aleatorios del mundo real hacen concesiones entre el grado de aleatoriedad, velocidad, etc. que el cliente desea controlar, y el cliente puede querer controlar la siembra para obtener un comportamiento repetible, la inyección puede tener sentido. En este caso, sugeriría ofrecer una forma de crear la clase sin especificar un generador de números aleatorios, y usar un Singleton local de subprocesos como el predeterminado. Si / cuando surge la necesidad de un control más preciso, proporcione otro constructor que permita inyectar una fuente de aleatoriedad.
Esto es similar a la respuesta de votos elevados, pero quiero pensar en voz alta: quizás otros también vean las cosas de esta manera.
Classical OO utiliza constructores para definir el contrato público de "inicialización" para los consumidores de la clase (ocultando TODOS los detalles de la implementación, también conocida como encapsulación). Este contrato puede garantizar que después de la instanciación tenga un objeto listo para usar (es decir, que el usuario no recuerde (e olvide) pasos de inicialización adicionales).
(constructor) DI rompe innegablemente la encapsulación al desangrar los detalles de implementación a través de esta interfaz pública de constructor. Mientras sigamos considerando al constructor público responsable de definir el contrato de inicialización para los usuarios, hemos creado una violación horrible de la encapsulación.
Ejemplo Teórico:
Class Foo tiene 4 métodos y necesita un número entero para la inicialización, por lo que su constructor se parece a Foo (int size) y es inmediatamente claro para los usuarios de la clase Foo que deben proporcionar un tamaño en instanciación para que Foo funcione.
Digamos que esta implementación particular de Foo también puede necesitar un IWidget para hacer su trabajo. La inyección del constructor de esta dependencia nos obligaría a crear un constructor como Foo (int size, widget IWidget)
Lo que me molesta de esto es que ahora tenemos un constructor que mezcla datos de inicialización con dependencias: una entrada es de interés para el usuario de la clase ( tamaño ), la otra es una dependencia interna que solo sirve para confundir al usuario y es una implementación detalle ( artilugio ).
El parámetro de tamaño NO es una dependencia, es simple un valor de inicialización por instancia. IoC es excelente para las dependencias externas (como el widget) pero no para la inicialización del estado interno.
Peor aún, ¿y si el Widget solo es necesario para 2 de los 4 métodos de esta clase? ¡Puedo incurrir en sobrecarga de instanciación para Widget aunque no se use!
¿Cómo comprometer / conciliar esto?
Un enfoque es cambiar exclusivamente a interfaces para definir el contrato de operación; y abolir el uso de constructores por parte de los usuarios. Para ser coherente, todos los objetos tendrían que accederse a través de interfaces solamente, e instanciarse solo a través de algún tipo de resolver (como un contenedor IOC / DI). Solo el contenedor puede crear instancias.
Eso se ocupa de la dependencia de los widgets, pero ¿cómo inicializamos el "tamaño" sin recurrir a un método de inicialización separado en la interfaz de Foo? Al utilizar esta solución, perdimos la capacidad de garantizar que una instancia de Foo se haya inicializado por completo en el momento en que obtenga la instancia. Bummer, porque me gusta mucho la idea y la simplicidad de la inyección de constructor.
¿Cómo puedo lograr la inicialización garantizada en este mundo DI, cuando la inicialización es MÁS que SOLO las dependencias externas?
Estoy de acuerdo que llevado al extremo, DI puede violar la encapsulación. Por lo general, DI expone dependencias que nunca fueron realmente encapsuladas. Aquí hay un ejemplo simplificado tomado de Singleks de Miško Hevery, son mentirosos patológicos :
Comienza con una prueba de tarjeta de crédito y escribe una prueba de unidad simple.
@Test
public void creditCard_Charge()
{
CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
c.charge(100);
}
El próximo mes recibirá una factura por $ 100. ¿Por qué te cobraron? La prueba unitaria afectó a una base de datos de producción. Internamente, CreditCard llama a Database.getInstance()
. Refactorizar CreditCard para que tome una DatabaseInterface
en su constructor expone el hecho de que hay dependencia. Pero yo diría que la dependencia nunca se encapsuló, ya que la clase CreditCard causa efectos secundarios visibles desde el exterior. Si desea probar CreditCard sin refactorizar, ciertamente puede observar la dependencia.
@Before
public void setUp()
{
Database.setInstance(new MockDatabase());
}
@After
public void tearDown()
{
Database.resetInstance();
}
No creo que valga la pena preocuparse si exponer la base de datos como una dependencia reduce la encapsulación, porque es un buen diseño. No todas las decisiones de DI serán tan directas. Sin embargo, ninguna de las otras respuestas muestra un contra modelo.
Habiendo luchado un poco más con el tema, ahora estoy en la opinión de que la Inyección de Dependencia (en este momento) viola la encapsulación hasta cierto punto. Sin embargo, no me malinterpreten, creo que el uso de la inyección de dependencia bien vale el equilibrio en la mayoría de los casos.
El argumento de por qué DI viola la encapsulación se vuelve claro cuando el componente en el que está trabajando debe entregarse a una parte "externa" (piense en escribir una biblioteca para un cliente).
Cuando mi componente requiere que los subcomponentes se inyecten a través del constructor (o propiedades públicas) no hay garantía para
"evitando que los usuarios configuren los datos internos del componente en un estado no válido o incoherente".
Al mismo tiempo, no se puede decir que
"Los usuarios del componente (otras piezas de software) solo necesitan saber qué hace el componente y no pueden depender de los detalles de cómo lo hace" .
Ambas citas son de wikipedia .
Para dar un ejemplo específico: necesito entregar una DLL del lado del cliente que simplifique y oculte la comunicación a un servicio WCF (esencialmente una fachada remota). Debido a que depende de 3 clases de proxy WCF diferentes, si tomo el enfoque DI me veo obligado a exponerlos a través del constructor. Con eso expongo las partes internas de mi capa de comunicación que trato de ocultar.
En general, yo soy todo por DI. En este ejemplo particular (extremo), me parece peligroso.
Hay otra forma de ver este problema que puede resultarle interesante.
Cuando usamos la inyección de IoC / dependency, no estamos usando los conceptos de OOP. Es cierto que estamos usando un lenguaje OO como ''host'', pero las ideas detrás de IoC provienen de la ingeniería de software orientada a componentes, no de OO.
El software de componentes tiene que ver con la administración de dependencias; un ejemplo de uso común es el mecanismo de ensamblaje de .NET. Cada ensamblaje publica la lista de ensamblajes a los que hace referencia, y esto hace que sea mucho más fácil juntar (y validar) las piezas necesarias para una aplicación en ejecución.
Al aplicar técnicas similares en nuestros programas OO a través de IoC, nuestro objetivo es facilitar la configuración y el mantenimiento de los programas. Publicar dependencias (como parámetros de constructor o lo que sea) es una parte clave de esto. La encapsulación no se aplica realmente, ya que en el mundo orientado al componente / servicio, no existe un "tipo de implementación" para que los detalles se filtren.
Desafortunadamente, nuestros lenguajes actualmente no segregan los conceptos de grano fino y orientados a objetos de los más groseros orientados a componentes, por lo que esta es una distinción que solo debes tener en cuenta :)
La encapsulación pura es un ideal que nunca se puede lograr. Si todas las dependencias estuvieran ocultas, entonces no tendrías ninguna necesidad de DI. Piénselo de esta manera, si realmente tiene valores privados que pueden ser internalizados dentro del objeto, digamos por ejemplo el valor entero de la velocidad de un objeto de automóvil, entonces no tiene dependencia externa y no necesita invertir o inyectar esa dependencia. Este tipo de valores de estado internos que son operados puramente por funciones privadas es lo que desea encapsular siempre.
Pero si está construyendo un automóvil que quiere un cierto tipo de objeto de motor, entonces tiene una dependencia externa. Puedes instanciar ese motor, por ejemplo, el nuevo GMOverHeadCamEngine (), internamente dentro del constructor del objeto del coche, preservando el encapsulado pero creando un acoplamiento mucho más insidioso a una clase concreta GMOverHeadCamEngine, o puedes inyectarlo, permitiendo que tu objeto Car funcione agnóstico (y mucho más robusto) en, por ejemplo, una interfaz IEngine sin la dependencia concreta. Ya sea que use un contenedor IOC o un DI simple para lograr esto, no es el punto: el punto es que tiene un automóvil que puede usar muchos tipos de motores sin estar acoplado a ninguno de ellos, lo que hace que su código base sea más flexible y menos propenso a los efectos secundarios.
DI no es una violación de la encapsulación, es una forma de minimizar el acoplamiento cuando la encapsulación necesariamente se rompe como una cuestión de curso en prácticamente todos los proyectos de OOP. Inyectar una dependencia en una interfaz minimiza externamente los efectos colaterales y permite que sus clases permanezcan indiferentes a la implementación.
La encapsulación solo se rompe si una clase tiene la responsabilidad de crear el objeto (que requiere conocimiento de los detalles de implementación) y luego usa la clase (que no requiere el conocimiento de estos detalles). Explicaré por qué, pero primero una rápida anaología del automóvil:
Cuando manejaba mi antiguo Kombi de 1971, podía presionar el acelerador y se iba (un poco) más rápido. No necesitaba saber por qué, pero los tipos que construyeron Kombi en la fábrica sabían exactamente por qué.
Pero volvamos a la codificación. La encapsulación es "ocultar un detalle de implementación de algo utilizando esa implementación". La encapsulación es algo bueno porque los detalles de implementación pueden cambiar sin que el usuario de la clase lo sepa.
Al usar la inyección de dependencia, la inyección del constructor se usa para construir objetos del tipo de servicio (a diferencia de los objetos de entidad / valor que modelan el estado). Cualquier variable miembro en el objeto de tipo de servicio representa detalles de implementación que no deberían filtrarse. por ejemplo, número de puerto de socket, credenciales de base de datos, otra clase para llamar para realizar encriptación, un caché, etc.
El constructor es relevante cuando la clase se está creando inicialmente. Esto ocurre durante la fase de construcción mientras su contenedor DI (o fábrica) conecta todos los objetos de servicio. El contenedor DI solo conoce los detalles de la implementación. Sabe todo acerca de los detalles de implementación como los chicos de la fábrica Kombi saben sobre bujías.
En tiempo de ejecución, el objeto de servicio que se creó se llama apon para hacer un trabajo real. En este momento, la persona que llama del objeto no sabe nada de los detalles de implementación.
Ese soy yo conduciendo mi Kombi a la playa.
Ahora, volviendo a la encapsulación. Si los detalles de implementación cambian, la clase que usa esa implementación en tiempo de ejecución no necesita cambiar. La encapsulación no está rota.
También puedo conducir mi auto nuevo a la playa. La encapsulación no está rota.
Si los detalles de implementación cambian, el contenedor DI (o fábrica) necesita cambiar. En primer lugar, nunca trataste de ocultar los detalles de implementación de la fábrica.
Luché con esta idea también. Al principio, el ''requisito'' de usar el contenedor DI (como Spring) para crear una instancia de un objeto parecía saltar a través de los aros. Pero en realidad, realmente no es un círculo, es solo otra forma ''publicada'' de crear objetos que necesito. Claro, la encapsulación está ''rota'' porque alguien ''fuera de la clase'' sabe lo que necesita, pero realmente no es el resto del sistema el que sabe eso, es el contenedor DI. Nada mágico sucede de manera diferente porque DI ''sabe'' que un objeto necesita otro.
De hecho, es aún mejor: centrándose en fábricas y repositorios ¡Ni siquiera tengo que saber que DI está involucrado en absoluto! Eso para mí pone la tapa a la encapsulación. ¡Uf!
No viola la encapsulación. Está proporcionando un colaborador, pero la clase decide cómo se usa. Mientras siga a Tell, no pregunte si las cosas están bien. Encuentro preferible la inyección de constructores, pero los setters pueden estar bien siempre y cuando sean inteligentes. Es decir, contienen lógica para mantener las invariantes que representa la clase.
PD. Al proporcionar Inyección de dependencia , no necesariamente rompe la encapsulación . Ejemplo:
obj.inject_dependency( factory.get_instance_of_unknown_class(x) );
El código del cliente aún no conoce los detalles de implementación.
Sí, DI viola la encapsulación (también conocida como "ocultación de información").
Pero el verdadero problema surge cuando los desarrolladores lo usan como una excusa para violar los principios de KISS (Keep It Short and Simple) y YAGNI (You are not no lo va a necesitar).
Personalmente, prefiero soluciones simples y efectivas. Uso principalmente el operador "nuevo" para crear instancias de dependencias con estado cuando y donde se necesiten. Es simple, bien encapsulado, fácil de entender y fácil de probar. ¿Entonces por qué no?
Tal vez esta es una forma ingenua de pensar al respecto, pero ¿cuál es la diferencia entre un constructor que toma un parámetro entero y un constructor que toma un servicio como parámetro? ¿Esto significa que definir un número entero fuera del nuevo objeto y alimentarlo en el objeto rompe la encapsulación? Si el servicio solo se usa dentro del nuevo objeto, no veo cómo eso rompería la encapsulación.
Además, al usar algún tipo de función de autoenlace (Autofac para C #, por ejemplo), hace que el código sea extremadamente limpio. Al construir métodos de extensión para el creador de Autofac, pude cortar MUCHO código de configuración DI que habría tenido que mantener a medida que crecía la lista de dependencias.
Un buen contenedor / sistema de inyección de dependencia permitirá la inyección del constructor. Los objetos dependientes estarán encapsulados y no necesitan ser expuestos públicamente en absoluto. Además, al usar un sistema DP, ninguno de los códigos "conoce" siquiera los detalles de cómo se construye el objeto, posiblemente incluso incluyendo el objeto que se está construyendo. Hay más encapsulación en este caso, ya que casi todo su código no solo está protegido del conocimiento de los objetos encapsulados, sino que ni siquiera participa en la construcción de los objetos.
Ahora, supongo que está comparando con el caso donde el objeto creado crea sus propios objetos encapsulados, muy probablemente en su constructor. Mi comprensión de DP es que queremos quitar esta responsabilidad del objeto y dárselo a otra persona. Con ese fin, el "alguien más", que es el contenedor de DP en este caso, tiene un conocimiento íntimo que "viola" la encapsulación; el beneficio es que extrae ese conocimiento del objeto, él mismo. Alguien tiene que tenerlo. El resto de tu aplicación no.
Lo pensaría de esta manera: el contenedor / sistema de inyección de dependencias viola la encapsulación, pero tu código no lo hace. De hecho, su código está más "encapsulado" que nunca.
Yo creo en la simplicidad. La aplicación de IOC / Inyección de Dependencia en las clases de Dominio no mejora, excepto que hace que el código sea mucho más difícil de mantener al tener un archivo xml externo que describe la relación. Muchas tecnologías como EJB 1.0 / 2.0 y struts 1.1 están retrocediendo reduciendo las cosas que se ponen en XML e intenta ponerlas en código como annoation, etc. Por lo tanto, la aplicación de IOC para todas las clases que desarrolles hará que el código carezca de sentido.
El COI tiene beneficios cuando el objeto dependiente no está listo para la creación en tiempo de compilación. Esto puede ocurrir en la mayoría de los componentes de arquitectura de nivel abstracto de infrasture, tratando de establecer un marco de base común que puede necesitar trabajar para diferentes escenarios. En esos lugares, el uso del COI tiene más sentido. Aún así, esto no hace que el código sea más simple / mantenible.
Como todas las otras tecnologías, esto también tiene PROs y CONs. Mi preocupación es que implementamos las últimas tecnologías en todos los lugares, independientemente de su mejor uso de contexto.
DI violates Encapsulation for NON-Shared objects - period. Shared objects have a lifespan outside of the object being created, and thus must be AGGREGATED into the object being created. Objects that are private to the object being created should be COMPOSED into the created object - when the created object is destroyed, it takes the composed object with it. Let''s take the human body as an example. What''s composed and what''s aggregated. If we were to use DI, the human body constructor would have 100''s of objects. Many of the organs, for example, are (potentially) replaceable. But, they are still composed into the body. Blood cells are created in the body (and destroyed) everyday, without the need for external influences (other than protein). Thus, blood cells are created internally by the body - new BloodCell().
Los defensores de DI argumentan que un objeto NUNCA debe usar el nuevo operador. Ese enfoque "purista" no solo viola la encapsulación sino también el Principio de Sustitución de Liskov para quien crea el objeto.
Es una buena pregunta, pero en algún momento, la encapsulación en su forma más pura necesita ser violada si el objeto alguna vez tendrá su dependencia cumplida. Algunos proveedores de la dependencia deben saber que el objeto en cuestión requiere un Foo
, y el proveedor debe tener una forma de proporcionar el Foo
al objeto.
Clásicamente, este último caso se maneja como dices, a través de argumentos de constructor o métodos setter. Sin embargo, esto no es necesariamente cierto: sé que las últimas versiones del marco Spring DI en Java, por ejemplo, le permiten anotar campos privados (por ejemplo, con @Autowired
) y la dependencia se establecerá a través de la reflexión sin necesidad de exponer el dependencia a través de cualquiera de las clases métodos públicos / constructores. Esta podría ser la clase de solución que estabas buscando.
Dicho esto, tampoco creo que la inyección del constructor sea un gran problema. Siempre he pensado que los objetos deberían ser completamente válidos después de la construcción, de modo que todo lo que necesiten para desempeñar su función (es decir, estar en un estado válido) se deba suministrar a través del constructor de todos modos. Si tiene un objeto que requiere que un colaborador trabaje, me parece bien que el constructor publique públicamente este requisito y se asegure de que se cumpla cuando se crea una nueva instancia de la clase.
Idealmente, al tratar con objetos, usted interactúa con ellos a través de una interfaz de todos modos, y cuanto más lo haga (y tenga dependencias cableadas a través de DI), menos tendrá que lidiar con los constructores usted mismo. En la situación ideal, su código no trata o incluso crea instancias concretas de clases; por lo tanto, solo recibe un IFoo
través de DI, sin preocuparse por lo que el constructor de FooImpl
indica que necesita para hacer su trabajo, y de hecho sin siquiera estar al tanto de la FooImpl
de FooImpl
. Desde este punto de vista, la encapsulación es perfecta.
Esta es una opinión, por supuesto, pero en mi opinión DI no necesariamente viola la encapsulación y, de hecho, puede ayudar al centralizar todo el conocimiento necesario de los elementos internos en un solo lugar. No solo es algo bueno en sí mismo, sino que incluso mejor, este lugar está fuera de su propia base de código, por lo que ninguno de los códigos que escribe necesita saber sobre las dependencias de las clases.
I think it''s self evident that at the very least DI significantly weakens encapsulation. In additional to that here are some other downsides of DI to consider.
It makes code harder to reuse. A module which a client can use without having to explicitly provide dependencies to, is obviously easier to use than one where the client has to somehow discover what that component''s dependencies are and then somehow make them available. For example a component originally created to be used in an ASP application may expect to have its dependencies provided by a DI container that provides object instances with lifetimes related to client http requests. This may not be simple to reproduce in another client that does not come with the same built in DI container as the original ASP application.
It can make code more fragile. Dependencies provided by interface specification can be implemented in unexpected ways which gives rise to a whole class of runtime bugs that are not possible with a statically resolved concrete dependency.
It can make code less flexible in the sense that you may end up with fewer choices about how you want it to work. Not every class needs to have all its dependencies in existence for the entire lifetime of the owning instance, yet with many DI implementations you have no other option.
With that in mind I think the most important question then becomes, " does a particular dependency need to be externally specified at all? ". In practise I have rarely found it necessary to make a dependency externally supplied just to support testing.
Where a dependency genuinely needs to be externally supplied, that normally suggests that the relation between the objects is a collaboration rather than an internal dependency, in which case the appropriate goal is then encapsulation of each class, rather than encapsulation of one class inside the other.
In my experience the main problem regarding the use of DI is that whether you start with an application framework with built in DI, or you add DI support to your codebase, for some reason people assume that since you have DI support that must be the correct way to instantiate everything . They just never even bother to ask the question "does this dependency need to be externally specified?". And worse, they also start trying to force everyone else to use the DI support for everything too.
The result of this is that inexorably your codebase starts to devolve into a state where creating any instance of anything in your codebase requires reams of obtuse DI container configuration, and debugging anything is twice as hard because you have the extra workload of trying to identify how and where anything was instantiated.
So my answer to the question is this. Use DI where you can identify an actual problem that it solves for you, which you can''t solve more simply any other way.
It''s probably worth mentioning that Encapsulation
is somewhat perspective dependent.
public class A {
private B b;
public A() {
this.b = new B();
}
}
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
From the perspective of someone working on the A
class, in the second example A
knows a lot less about the nature of this.b
Whereas without DI
new A()
vs
new A(new B())
The person looking at this code knows more about the nature of A
in the second example.
With DI, at least all that leaked knowledge is in one place.