¿La inyección de dependencia debe venir a expensas de la encapsulación?

128

Si entiendo correctamente, el mecanismo típico para la Inyección de dependencias es inyectar a través de un constructor de clase o de una propiedad pública (miembro) de la clase.

Esto expone la dependencia que se inyecta y viola el principio de encapsulación OOP.

¿Estoy en lo correcto al identificar esta compensación? ¿Cómo lidias con este problema?

Consulte también mi respuesta a mi propia pregunta a continuación.

urig
fuente
77
en mi humilde opinión esta es una pregunta muy inteligente
DFA
55
Responder esta pregunta primero requiere un argumento sobre lo que significa la encapsulación . ;)
Jeff Sternal
2
La encapsulación es mantenida por las interfaces. Exponen las características esenciales del objeto y ocultan detalles como las dependencias. Esto nos permite 'abrir' las clases hasta cierto punto para proporcionar una configuración más flexible.
Lawrence Wagerfield

Respuestas:

62

Hay otra forma de ver este problema que puede resultarle interesante.

Cuando usamos IoC / inyección de dependencia, no estamos usando conceptos 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 OO.

El software de componentes se trata de administrar dependencias; un ejemplo de uso común es el mecanismo de ensamblaje de .NET. Cada conjunto publica la lista de conjuntos a los que hace referencia, y esto hace que sea mucho más fácil reunir (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 hacer que los programas sean más fáciles de configurar y mantener. Publicar dependencias (como parámetros de constructor o lo que sea) es una parte clave de esto. La encapsulación realmente no se aplica, ya que en el mundo orientado a componentes / servicios, no hay un 'tipo de implementación' para que se filtren los detalles.

Desafortunadamente, nuestros lenguajes actualmente no segregan los conceptos orientados a objetos de grano fino de los orientados a componentes de grano más grueso, por lo que esta es una distinción que solo debe tener en mente :)

Nicholas Blumhardt
fuente
18
La encapsulación no es solo una terminología elegante. Es algo real con beneficios reales, y no importa si considera que su programa está "orientado a componentes" u "orientado a objetos". Se supone que la encapsulación protege el estado de su objeto / componente / servicio / lo que sea que se modifique de manera inesperada, y IoC elimina parte de esta protección, por lo que definitivamente hay una compensación.
Ron Inbar
1
Los argumentos suministrados a través de un constructor todavía caen dentro del ámbito de las formas esperadas en las que el objeto puede "cambiarse": están expuestos explícitamente y se hacen cumplir invariantes a su alrededor. Ocultar información es el mejor término para el tipo de privacidad al que te refieres, @RonInbar, y no siempre es beneficioso (hace que la pasta sea más difícil de desenredar ;-)).
Nicholas Blumhardt
2
El objetivo de OOP es que el enredo de pasta se separa en clases separadas y que solo tiene que meterse con él si es el comportamiento de esa clase en particular lo que desea modificar (así es como OOP mitiga la complejidad). Una clase (o módulo) encapsula sus componentes internos al tiempo que expone una interfaz pública conveniente (así es como OOP facilita la reutilización). Una clase que expone sus dependencias a través de interfaces crea complejidad para sus clientes y, como resultado, es menos reutilizable. También es inherentemente más frágil.
Neutrino
1
No importa de qué manera lo mire, me parece que la DI socava seriamente algunos de los beneficios más valiosos de la POO, y aún no he encontrado una situación en la que realmente lo haya utilizado de una manera que resolvió un problema real. problema existente
Neutrino
1
Aquí hay otra forma de enmarcar el problema: imagine si los ensamblados .NET eligieron "encapsulación" y no declararon en qué otros ensamblados confiaron. Sería una situación loca, leer documentos y esperar que algo funcione después de cargarlo. Declarar dependencias a ese nivel permite que las herramientas automatizadas se ocupen de la composición a gran escala de la aplicación. Tienes que entrecerrar los ojos para ver la analogía, pero fuerzas similares se aplican a nivel de componente. Hay compensaciones, y YMMV como siempre :-)
Nicholas Blumhardt
29

Es una buena pregunta, pero en algún momento, la encapsulación en su forma más pura debe ser violada si alguna vez se cumple la dependencia del objeto. Algunos proveedores de la dependencia deben saber que el objeto en cuestión requiere una Fooy que el proveedor debe tener una forma de proporcionarla Fooal objeto.

Clásicamente este último caso se maneja como usted dice, a través de argumentos de constructor o métodos de establecimiento. 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 la dependencia a través de cualquiera de las clases métodos / constructores públicos. Este podría ser el tipo de solución que estaba buscando.

Dicho esto, tampoco creo que la inyección de 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 cualquier cosa que necesiten para desempeñar su función (es decir, estar en un estado válido) debería ser suministrada a través del constructor de todos modos. Si tiene un objeto que requiere un colaborador para trabajar, me parece bien que el constructor publique públicamente este requisito y se asegure de que se cumpla cuando se cree una nueva instancia de la clase.

Idealmente cuando se trata con objetos, interactúa con ellos a través de una interfaz de todos modos, y cuanto más lo haga (y tenga dependencias conectadas a través de DI), menos tendrá que lidiar con los constructores usted mismo. En la situación ideal, su código no trata ni crea instancias concretas de clases; así que solo se le da un IFooDI directo, sin preocuparse de lo que el constructor de FooImplindica que necesita para hacer su trabajo, y de hecho sin siquiera darse cuenta de FooImplla existencia. 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 ayudarlo al centralizar todo el conocimiento necesario de los componentes 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 escriba necesita saber sobre las dependencias de las clases.

Andrzej Doyle
fuente
2
Buenos puntos. Aconsejo no usar @Autowired en campos privados; eso hace que la clase sea difícil de evaluar; ¿Cómo se inyectan simulacros o trozos?
lumpynose
44
Estoy en desacuerdo. DI viola la encapsulación, y esto se puede evitar. Por ejemplo, mediante el uso de un ServiceLocator, que obviamente no necesita saber nada sobre la clase del cliente; solo necesita saber acerca de las implementaciones de la dependencia de Foo. Pero lo mejor, en la mayoría de los casos, es simplemente usar el "nuevo" operador.
Rogério el
55
@Rogerio: podría decirse que cualquier marco DI actúa exactamente como el ServiceLocator que usted describe; el cliente no sabe nada específico sobre las implementaciones de Foo, y la herramienta DI no sabe nada específico sobre el cliente. Y usar "nuevo" es mucho peor por violar la encapsulación, ya que necesita saber no solo la clase de implementación exacta, sino también las clases e instancias exactas de todas las dependencias que necesita.
Andrzej Doyle
44
El uso de "nuevo" para crear una instancia de una clase auxiliar, que a menudo ni siquiera es pública, promueve la encapsulación. La alternativa DI sería hacer pública la clase de ayuda y agregar un constructor o setter público en la clase de cliente; ambos cambios romperían la encapsulación provista por la clase auxiliar original.
Rogério
1
"Es una buena pregunta, pero en algún momento, la encapsulación en su forma más pura debe ser violada si alguna vez se cumple su dependencia". La premisa fundamental de su respuesta es simplemente falsa. Como @ Rogério declara renovar una dependencia internamente, y cualquier otro método en el que un objeto satisface internamente sus propias dependencias no viola la encapsulación.
Neutrino
17

Esto expone la dependencia que se inyecta y viola el principio de encapsulación OOP.

Bueno, francamente hablando, todo viola la encapsulación. :) Es una especie de principio tierno que debe tratarse bien.

Entonces, ¿qué viola la encapsulación?

La herencia .

"Debido a que la herencia expone una subclase a los detalles de la implementación de su padre, a menudo se dice que 'la herencia rompe la encapsulación'". (Banda de cuatro 1995: 19)

La programación orientada a aspectos . Por ejemplo, registra la devolución de llamada onMethodCall () y eso le brinda una gran oportunidad para inyectar código en la evaluación del método normal, agregar efectos secundarios extraños, etc.

La declaración de amigo en C ++ .

La extensión de clase en Ruby . Simplemente redefina un método de cadena en algún lugar después de que una clase de cadena se haya definido completamente.

Bueno, muchas cosas .

La encapsulación es un principio bueno e importante. Pero no el único.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}
juego vertical
fuente
3
"Esos son mis principios, y si no te gustan ... bueno, tengo otros". (Groucho Marx)
Ron Inbar
2
Creo que este es el punto. Es inyección dependiente vs encapsulación. Por lo tanto, solo use una inyección dependiente donde proporcione beneficios significativos. Es el DI en todas partes lo que le da un mal nombre
Richard Tingle
No estoy seguro de lo que esta respuesta está tratando de decir ... ¿Está bien violar la encapsulación al hacer DI, que está "siempre bien" porque de todos modos se violaría, o simplemente que DI podría ser una razón para violar la encapsulación? En otra nota, en estos días ya no es necesario confiar en constructores o propiedades públicas para inyectar dependencias; en cambio, podemos inyectar en campos privados anotados , que es más simple (menos código) y preserva la encapsulación. Por lo tanto, podemos aprovechar ambos principios al mismo tiempo.
Rogério
La herencia en principio no viola la encapsulación, aunque puede hacerlo si la clase principal está mal escrita. Los otros puntos que plantea son un paradigma de programación bastante marginal y varias características del lenguaje que tienen poco que ver con la arquitectura o el diseño.
Neutrino
13

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 Don't Don't Need You Need It).

Personalmente, prefiero soluciones simples y efectivas. Principalmente uso el "nuevo" operador 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?

Rogério
fuente
Pensar en el futuro no es terrible, pero estoy de acuerdo ¡Mantenlo simple, estúpido, especialmente si no lo vas a necesitar! He visto a los desarrolladores desperdiciar ciclos porque han terminado de diseñar algo para ser demasiado a prueba de futuro, y se basan en presentimientos en eso, ni siquiera en los requisitos comerciales que se conocen / sospechan.
Jacob McKay
5

Un buen contenedor / sistema de inyección de confianza permitirá la inyección del constructor. Los objetos dependientes se encapsularán y no necesitan exponerse públicamente. Además, mediante el uso de un sistema DP, ninguno de sus códigos incluso "conoce" los detalles de cómo se construye el objeto, posiblemente incluso 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 en el que el objeto creado crea sus propios objetos encapsulados, muy probablemente en su constructor. Entiendo que DP es que queremos quitarle esta responsabilidad al objeto y dársela a otra persona. Con ese fin, el "alguien más", que es el contenedor DP en este caso, tiene un conocimiento íntimo que "viola" la encapsulación; El beneficio es que extrae ese conocimiento del objeto mismo. Alguien tiene que tenerlo. El resto de su aplicación no.

Lo pensaría de esta manera: el contenedor / sistema de inyección de dependencia viola la encapsulación, pero su código no. De hecho, su código está más "encapsulado" que nunca.

Cuenta
fuente
3
Si tiene una situación en la que el objeto del cliente PUEDE instanciar sus dependencias directamente, ¿por qué no hacerlo? Definitivamente es lo más simple y no necesariamente reduce la capacidad de prueba. Además de la simplicidad y una mejor encapsulación, esto también hace que sea más fácil tener objetos con estado en lugar de singletons sin estado.
Rogério
1
Agregando a lo que @ Rogério dijo que también es potencialmente significativamente más eficiente. No todas las clases jamás creadas en la historia del mundo necesitan tener cada una de sus dependencias instanciadas durante toda la vida útil del objeto propietario. Un objeto que usa DI pierde el control más básico de sus propias dependencias, es decir, su vida útil.
Neutrino
5

Como Jeff Sternal señaló en un comentario a la pregunta, la respuesta depende completamente de cómo se defina la encapsulación .

Parece que hay dos campos principales de lo que significa la encapsulación:

  1. Todo lo relacionado con el objeto es un método en un objeto. Por lo tanto, un Fileobjeto puede tener métodos a Save, Print, Display, ModifyText, etc.
  2. Un objeto es su propio pequeño mundo y no depende del comportamiento externo.

Estas dos definiciones están en contradicción directa entre sí. Si un Fileobjeto puede imprimirse, dependerá en gran medida del comportamiento de la impresora. Por otro lado, si simplemente sabe algo que puede imprimir para él (una IFilePrintero alguna de esas interfaces), entonces el Fileobjeto no tiene que saber nada sobre impresión, por lo que trabajar con él traerá menos dependencias en el objeto.

Entonces, la inyección de dependencia interrumpirá 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.

Kyoryu
fuente
Definitivamente estoy de acuerdo con usted sobre la primera definición. También viola SoC, que posiblemente sea uno de los pecados capitales de la programación y probablemente una de las razones por las que no se escala.
Marcus Stade
4

No viola la encapsulación. Estás proporcionando un colaborador, pero la clase decide cómo se usa. Mientras sigas a Tell, no preguntes, las cosas están bien. Creo que la inyección de constructor es preferible, pero los colocadores pueden estar bien, siempre y cuando sean inteligentes. Es decir, contienen lógica para mantener los invariantes que representa la clase.

Jason Watkins
fuente
1
Porque no ... Si tiene un registrador y tiene una clase que necesita el registrador, pasar el registrador a esa clase no viola la encapsulación. Y eso es toda la inyección de dependencia.
jrockway
3
Creo que malinterpretas la encapsulación. Por ejemplo, tome una clase de fecha ingenua. Internamente puede tener variables de instancia de día, mes y año. Si se expusieran como setters simples sin lógica, esto rompería la encapsulación, ya que podría hacer algo como establecer mes a 2 y día a 31. Por otro lado, si los setters son inteligentes y verifican los invariantes, entonces las cosas están bien. . También tenga en cuenta que en la última versión, podría cambiar el almacenamiento a días desde el 1/1/1970, y nada de lo que usó la interfaz debe ser consciente de esto siempre que reescriba adecuadamente los métodos de día / mes / año.
Jason Watkins
2
DI definitivamente viola la encapsulación / ocultación de información. Si convierte una dependencia interna privada en algo expuesto en la interfaz pública de la clase, entonces, por definición, ha roto la encapsulación de esa dependencia.
Rogério el
2
Tengo un ejemplo concreto donde creo que la encapsulación se ve comprometida por DI. Tengo un FooProvider que obtiene "datos foo" de la base de datos y un FooManager que los almacena en caché y calcula cosas sobre el proveedor. Hice que los consumidores de mi código acudieran por error a FooProvider para obtener datos, donde preferiría haberlo encapsulado para que solo conozcan el FooManager. Esto es básicamente el desencadenante de mi pregunta original.
urig
1
@Rogerio: Yo diría que el constructor no es parte de la interfaz pública, ya que solo se usa en la raíz de la composición. Por lo tanto, la raíz de la composición solo "ve" la dependencia. La única responsabilidad de la raíz de la composición es conectar estas dependencias. Por lo tanto, el uso de la inyección del constructor no rompe ninguna encapsulación.
Jay Sullivan
4

Esto es similar a la respuesta votada, pero quiero pensar en voz alta, tal vez otros también vean las cosas de esta manera.

  • OO clásico utiliza constructores para definir el contrato público de "inicialización" para los consumidores de la clase (ocultando TODOS los detalles de implementación; también conocido como encapsulación). Este contrato puede garantizar que, después de la creación de instancias, tenga un objeto listo para usar (es decir, ningún paso de inicialización adicional para ser recordado (er, olvidado) por el usuario).

  • (constructor) DI sin lugar a dudas rompe la encapsulación al sangrar los detalles de implementación a través de esta interfaz de constructor público. 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 los usuarios de la clase Foo tienen claro de inmediato que deben proporcionar un tamaño en la 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 haría crear un constructor como Foo (int size, widget de IWidget)

Lo que me molesta de esto es que ahora tenemos un constructor que combina 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 ( widget ).

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, ¿qué pasa si el Widget solo es necesario para 2 de los 4 métodos en esta clase; ¡Puedo estar incurriendo en una sobrecarga de instanciación para Widget aunque no se pueda usar!

¿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 ser accedidos solo a través de interfaces, y instanciados solo a través de alguna forma de resolutor (como un contenedor IOC / DI). Solo el contenedor puede instanciar cosas.

Eso se encarga de la dependencia del widget, pero ¿cómo inicializamos el "tamaño" sin recurrir a un método de inicialización separado en la interfaz de Foo? Con esta solución, perdimos la capacidad de garantizar que una instancia de Foo esté completamente inicializada en el momento en que la obtenga. Qué fastidio, porque realmente me gusta la idea y la simplicidad de la inyección del constructor.

¿Cómo puedo lograr una inicialización garantizada en este mundo DI, cuando la inicialización es MÁS que SOLO dependencias externas?

shawnT
fuente
Actualización: Acabo de notar que Unity 2.0 admite proporcionar valores para los parámetros del constructor (como los inicializadores de estado), mientras sigue utilizando el mecanismo normal para IoC de las dependencias durante resolve (). ¿Quizás otros contenedores también admiten esto? ¡Esto resuelve la dificultad técnica de mezclar el estado init y DI en un constructor, pero todavía viola la encapsulación!
shawnT
Te escucho. Hice esta pregunta porque también siento que dos cosas buenas (DI y encapsulación) vienen a expensas de la otra. Por cierto, en su ejemplo donde solo 2 de los 4 métodos necesitan el IWidget, eso indicaría que los otros 2 pertenecen a un componente diferente en mi humilde opinión.
Urig
3

La encapsulación pura es un ideal que nunca se puede lograr. Si todas las dependencias estuvieran ocultas, entonces no tendría la necesidad de DI en absoluto. Piénselo de esta manera, si realmente tiene valores privados que se pueden internalizar dentro del objeto, 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 interno que son operados exclusivamente por funciones privadas son lo que desea encapsular siempre.

Pero si está construyendo un automóvil que quiere un cierto tipo de objeto del motor, entonces tiene una dependencia externa. Puede crear una instancia de ese motor, por ejemplo, nuevo GMOverHeadCamEngine (), internamente dentro del constructor del objeto del automóvil, preservando la encapsulación pero creando un acoplamiento mucho más insidioso a una clase concreta GMOverHeadCamEngine, o puede inyectarlo, permitiendo que el objeto del automóvil funcione agnósticamente (y mucho más robusto) en, por ejemplo, una interfaz IEngine sin la dependencia concreta. Ya sea que use un contenedor de COI o una 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 base de código 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 se rompe necesariamente como algo natural en casi todos los proyectos de OOP. Inyectar una dependencia en una interfaz externamente minimiza los efectos secundarios de acoplamiento y permite que sus clases se mantengan agnósticas sobre la implementación.

Dave Sims
fuente
3

Depende de si la dependencia es realmente un detalle de implementación o algo que el cliente desearía / necesitaría saber de una forma u otra. Una cosa que es relevante es a qué nivel de abstracción apunta 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 inyectarse. 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 que preocuparse.

Si su clase necesita generar flujos de datos, probablemente tenga sentido inyectar el flujo de salida para que la clase pueda generar fácilmente los resultados en una matriz, un archivo o donde sea que otra persona quiera enviar los datos.

Para un área gris, digamos que tiene una clase que hace una 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, ya 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 compensaciones entre el grado de aleatoriedad, velocidad, etc. que el cliente puede querer 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 de subproceso local como predeterminado. Si surge la necesidad de un control más preciso, proporcione otro constructor que permita inyectar una fuente de aleatoriedad.

dsimcha
fuente
2

Creo en la simplicidad. La aplicación de la inyección de dependencia / IOC en las clases de dominio no mejora, excepto hacer que el código sea mucho más difícil de mantener al tener archivos xml externos que describen la relación. Muchas tecnologías como EJB 1.0 / 2.0 y struts 1.1 están retrocediendo al reducir las cosas que se ponen en XML e intentar ponerlas en código como anotación, etc. Por lo tanto, aplicar IOC para todas las clases que desarrolle hará que el código no tenga sentido.

IOC tiene beneficios cuando el objeto dependiente no está listo para la creación en tiempo de compilación. Esto puede suceder en la mayoría de los componentes de arquitectura de nivel abstracto de infraestructura, tratando de establecer un marco base común que deba funcionar para diferentes escenarios. En esos lugares, el uso de 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 PRO y CON. Mi preocupación es que implementamos las últimas tecnologías en todos los lugares, independientemente de su mejor uso de contexto.

Senthil Kumar Vaithilingam
fuente
2

La encapsulación solo se rompe si una clase tiene la responsabilidad de crear el objeto (que requiere el 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 anaología rápida del automóvil:

Cuando conducía mi viejo Kombi de 1971, podía presionar el acelerador y fue (ligeramente) más rápido. No necesitaba saber por qué, pero los tipos que construyeron el 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 que usa 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.

Cuando se usa la inyección de dependencia, la inyección del constructor se usa para construir objetos de 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 deben filtrarse. por ejemplo, número de puerto de socket, credenciales de la base de datos, otra clase a la que llamar para realizar el cifrado, un caché, etc.

El constructor es relevante cuando la clase se está creando inicialmente. Esto sucede 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 implementación. Sabe todo acerca de los detalles de implementación como lo saben los muchachos de la fábrica de Kombi sobre las 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, el llamador del objeto no sabe nada de los detalles de implementación.

Ese soy yo conduciendo mi Kombi a la playa.

Ahora, de vuelta a la encapsulación. Si los detalles de implementación cambian, entonces 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 intentó ocultar los detalles de implementación de la fábrica.

WW.
fuente
¿Cómo probaría la unidad su fábrica? Y eso significa que el cliente necesita saber sobre la fábrica para obtener un automóvil que funcione, lo que significa que necesita una fábrica para cada otro objeto en su sistema.
Rodrigo Ruiz
2

Después de haber luchado un poco más con el problema, ahora considero que la inyección de dependencia (en este momento) viola la encapsulación en algún grado. Sin embargo, no me malinterpretes: creo que usar la inyección de dependencia vale la pena en la mayoría de los casos.

El caso 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 se inyecten subcomponentes a través del constructor (o propiedades públicas) no hay garantía para

"evitar que los usuarios establezcan los datos internos del componente en un estado no válido o inconsistente".

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 adopto el enfoque DI me veo obligado a exponerlas a través del constructor. Con eso expongo los elementos internos de mi capa de comunicación que estoy tratando de ocultar.

Generalmente estoy a favor de DI. En este ejemplo particular (extremo), me parece peligroso.

urig
fuente
2

DI viola la Encapsulación para objetos NO Compartidos - punto. Los objetos compartidos tienen una vida útil fuera del objeto que se está creando y, por lo tanto, deben AGREGARSE al objeto que se está creando. Los objetos que son privados para el objeto que se está creando deben COMPONERSE en el objeto creado: cuando el objeto creado se destruye, se lleva el objeto compuesto. Tomemos el cuerpo humano como ejemplo. Lo que está compuesto y lo que está agregado. Si tuviéramos que usar DI, el constructor del cuerpo humano tendría cientos de objetos. Muchos de los órganos, por ejemplo, son (potencialmente) reemplazables. Pero, todavía están compuestos en el cuerpo. Las células sanguíneas se crean en el cuerpo (y se destruyen) todos los días, sin la necesidad de influencias externas (que no sean proteínas). Por lo tanto, las células sanguíneas son creadas internamente por el cuerpo: nuevo 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 está creando el objeto.

Greg Brand
fuente
1

Luché con esta noción también. Al principio, el 'requisito' de usar el contenedor DI (como Spring) para crear una instancia de un objeto se sentía como saltar a través de aros. Pero en realidad, no es realmente un aro, 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 lo sabe: es el contenedor DI. Nada mágico sucede de manera diferente porque DI 'sabe' que un objeto necesita otro.

De hecho, se pone aún mejor: al centrarme en las fábricas y repositorios, ¡ni siquiera tengo que saber que la DI está involucrada en absoluto! Eso para mí pone la tapa de nuevo en la encapsulación. ¡Uf!

n8wrl
fuente
1
Mientras DI esté a cargo de toda la cadena de creación de instancias, estoy de acuerdo en que ocurre una especie de encapsulación. Más o menos, porque las dependencias aún son públicas y pueden ser abusadas. Pero cuando en algún lugar "arriba" de la cadena, alguien necesita crear una instancia de un objeto sin que use DI (tal vez sea un "tercero"), entonces se vuelve desordenado. Están expuestos a sus dependencias y pueden verse tentados a abusar de ellos. O tal vez no quieran saber nada de ellos.
urig
1

PD. Al proporcionar la inyección de dependencias que no necesariamente se 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.

juego vertical
fuente
¿Cómo en tu ejemplo se inyecta algo? (Excepto por el nombre de la función de configuración)
foo
1

Tal vez esta sea una forma ingenua de pensarlo, 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? ¿Significa esto 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 cableado automático (Autofac para C #, por ejemplo), hace que el código sea extremadamente limpio. Al construir métodos de extensión para el constructor Autofac, pude cortar MUCHO código de configuración DI que habría tenido que mantener con el tiempo a medida que crecía la lista de dependencias.

blooware
fuente
No se trata de valor versus servicio. Se trata de la inversión de control: si el constructor de la clase configura la clase o si ese servicio se hace cargo de la configuración de la clase. Para lo cual necesita conocer los detalles de implementación de esa clase, por lo que tiene otro lugar para mantener y sincronizar.
foo
1

Creo que es evidente que al menos la DI debilita significativamente la encapsulación. Además de eso, aquí hay algunos otros inconvenientes de la DI a tener en cuenta.

  1. Hace que el código sea más difícil de reutilizar. Un módulo que un cliente puede usar sin tener que proporcionar explícitamente dependencias, obviamente es más fácil de usar que uno donde el cliente tiene que descubrir de alguna manera cuáles son las dependencias de ese componente y luego ponerlas a disposición de alguna manera. Por ejemplo, un componente creado originalmente para usarse en una aplicación ASP puede esperar que sus dependencias sean proporcionadas por un contenedor DI que proporciona instancias de objetos con vidas relacionadas con las solicitudes http del cliente. Es posible que esto no sea sencillo de reproducir en otro cliente que no viene con el mismo contenedor DI incorporado que la aplicación ASP original.

  2. Puede hacer que el código sea más frágil. Las dependencias proporcionadas por la especificación de la interfaz se pueden implementar de formas inesperadas, lo que da lugar a toda una clase de errores de tiempo de ejecución que no son posibles con una dependencia concreta resuelta estáticamente.

  3. Puede hacer que el código sea menos flexible en el sentido de que puede terminar con menos opciones sobre cómo desea que funcione. No todas las clases necesitan tener todas sus dependencias en existencia durante toda la vida útil de la instancia propietaria, sin embargo, con muchas implementaciones de DI no tiene otra opción.

Con eso en mente, creo que la pregunta más importante se convierte en " ¿una dependencia particular necesita ser especificada externamente? ". En la práctica, rara vez he encontrado necesario hacer una dependencia externamente suministrada solo para soportar las pruebas.

Cuando una dependencia realmente necesita ser suministrada externamente, eso normalmente sugiere que la relación entre los objetos es una colaboración en lugar de una dependencia interna, en cuyo caso el objetivo apropiado es la encapsulación de cada clase, en lugar de la encapsulación de una clase dentro de la otra .

En mi experiencia, el principal problema con respecto al uso de DI es que si comienzas con un marco de aplicación con DI incorporado, o si agregas soporte de DI a tu base de código, por alguna razón la gente asume que, dado que tienes soporte de DI, ese debe ser el correcto manera de instanciar todo . Simplemente nunca se molestan en hacer la pregunta "¿esta dependencia debe especificarse externamente?". Y lo que es peor, también comienzan a tratar de obligar a todos los demás a usar el soporte DI todo .

El resultado de esto es que inexorablemente su base de código comienza a convertirse en un estado en el que crear cualquier instancia de cualquier cosa en su base de código requiere resmas de configuración obtusa del contenedor DI, y depurar cualquier cosa es el doble de difícil porque tiene la carga de trabajo adicional de tratar de identificar cómo y donde algo fue instanciado.

Entonces mi respuesta a la pregunta es esta. Use DI donde puede identificar un problema real que resuelve para usted, que no puede resolver más simplemente de otra manera.

Neutrino
fuente
0

Estoy de acuerdo en 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 Singletons de Miško Hevery que son mentirosos patológicos. :

Comienza con una prueba de CreditCard y escribe una prueba unitaria 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 acusaron? La prueba unitaria afectó a una base de datos de producción. Internamente, CreditCard llama Database.getInstance(). Refactorizar CreditCard para que tome un DatabaseInterfaceen su constructor expone el hecho de que hay dependencia. Pero yo diría que, para empezar, 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 sencillas. Sin embargo, ninguna de las otras respuestas muestra un contraejemplo.

Craig P. Motlin
fuente
1
Las pruebas unitarias generalmente son escritas por el autor de la clase, por lo que está bien explicar las dependencias en el caso de prueba, desde un punto de vista técnico. Cuando la clase de tarjeta de crédito posterior cambie para usar una API web como PayPal, el usuario necesitaría cambiar todo si se moría. Las pruebas unitarias generalmente se realizan con un conocimiento íntimo del tema que se está evaluando (¿no es eso todo el punto?), Así que creo que las pruebas son más una excepción que un ejemplo típico.
kizzx2
1
El objetivo de DI es evitar los cambios que describió. Si cambiara de una API web a PayPal, no cambiaría la mayoría de las pruebas porque usarían un MockPaymentService y CreditCard se construiría con un PaymentService. Tendrías solo un puñado de pruebas para observar la interacción real entre CreditCard y un verdadero PaymentService, por lo que los cambios futuros están muy aislados. Los beneficios son aún mayores para gráficos de dependencia más profundos (como una clase que depende de CreditCard).
Craig P. Motlin
@Craig p. Motlin ¿Cómo puede CreditCardcambiar el objeto de un WebAPI a PayPal, sin que tenga que cambiar nada externo a la clase?
Ian Boyd
@Ian mencioné que CreditCard debería ser refactorizado para tomar una DatabaseInterface en su constructor que lo protege de los cambios en las implementaciones de DatabaseInterfaces. Tal vez sea necesario que sea aún más genérico, y tome una interfaz de almacenamiento de WebAPI que podría ser otra implementación. Sin embargo, PayPal está en el nivel incorrecto, porque es una alternativa a CreditCard. Tanto PayPal como CreditCard podrían implementar PaymentInterface para proteger otras partes de la aplicación fuera de este ejemplo.
Craig P. Motlin
@ kizzx2 En otras palabras, no tiene sentido decir que una tarjeta de crédito debe usar PayPal. Son alternativas en la vida real.
Craig P. Motlin
0

Creo que es una cuestión de alcance. Cuando define la encapsulación (sin dejar saber cómo) debe definir cuál es la funcionalidad encapsulada.

  1. Clasifique como está : lo que está encapsulando es la única responsabilidad de la clase. Lo que sabe hacer. Por ejemplo, ordenando. Si inyecta algún comparador para ordenar, digamos, clientes, eso no es parte de lo encapsulado: quicksort.

  2. Funcionalidad configurada : si desea proporcionar una funcionalidad lista para usar, no está proporcionando la clase QuickSort, sino una instancia de la clase QuickSort configurada con un Comparador. En ese caso, el código responsable de crear y configurar eso debe estar oculto del código de usuario. Y esa es la encapsulación.

Cuando está programando clases, es decir, implementando responsabilidades individuales en clases, está utilizando la opción 1.

Cuando está programando aplicaciones, es decir, hacer algo que emprenda un trabajo concreto útil, entonces está utilizando repetidamente la opción 2.

Esta es la implementación de la instancia configurada:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Así es como lo usa algún otro código de cliente:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Está encapsulado porque si cambia la implementación (cambia la clientSorterdefinición del bean) no interrumpe el uso del cliente. Tal vez, a medida que usa archivos xml con todos escritos juntos, está viendo todos los detalles. Pero créame, el código del cliente ( ClientService) no sabe nada sobre su clasificador.

helios
fuente
0

Probablemente valga la pena mencionar que Encapsulationdepende un poco de la perspectiva.

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;
    }
}

Desde la perspectiva de alguien que trabaja en la Aclase, en el segundo ejemplo Asabe mucho menos sobre la naturaleza dethis.b

Mientras que sin DI

new A()

vs

new A(new B())

La persona que mira este código sabe más sobre la naturaleza del Asegundo ejemplo.

Con DI, al menos todo ese conocimiento filtrado se encuentra en un solo lugar.

Tumba
fuente