Inyección de dependencia: ¿Inyección de campo versus inyección de constructor?

62

Sé que este es un debate candente y las opiniones tienden a cambiar con el tiempo en cuanto a la mejor práctica de enfoque.

Solía usar exclusivamente la inyección de campo para mis clases, hasta que empecé a leer sobre diferentes blogs (ejs: petrikainulainen y schauderhaft y Fowler ) sobre los beneficios de la inyección de constructor. Desde entonces he cambiado mis metodologías para usar la inyección de constructor para las dependencias requeridas y la inyección de setter para dependencias opcionales.

Sin embargo, recientemente me he metido en un debate con el autor de JMockit, un marco burlón, en el que ve la inyección del constructor y del instalador como una mala práctica e indica que la comunidad JEE está de acuerdo con él.

En el mundo de hoy, ¿hay una forma preferida de hacer la inyección? ¿Se prefiere la inyección de campo?

Después de cambiar a inyección de constructor desde la inyección de campo en los últimos años, me resulta mucho más fácil de usar, pero me pregunto si debería volver a examinar mi punto de vista. El autor de JMockit (Rogério Liesenfeld) , está claramente versado en DI, por lo que me siento obligado a revisar mi enfoque dado que se siente muy en contra de la inyección del constructor / instalador.

Eric B.
fuente
2
Si no puede usar la inyección de constructor o la inyección de setter, ¿cómo se supone que debe entregar sus dependencias a la clase? Tal vez sea mejor que te vincules al debate, a menos que tu conversación con el tipo Mockit sea privada.
Robert Harvey
2
@DavidArno: en una clase inmutable, el estado se establece una vez ... en el constructor.
Robert Harvey
1
@Davkd lo que estás describiendo es el modelo de dominio anémico. Ciertamente tiene defensores, pero ya no está realmente orientado a objetos; donde los datos y las funciones que actúan sobre esos datos viven juntos.
Richard Tingle
3
@David No hay nada malo con la programación funcional. Pero fundamentalmente, Java está diseñado para ser orientado a objetos y constantemente lucharás contra él y terminarás con el modelo de dominio anémico si no tienes mucho cuidado. No hay nada de malo en el código orientado a funciones, pero se debe usar una herramienta adecuada para él, que no es Java (a pesar de que Lambda ha agregado elementos de programación basada en funciones)
Richard Tingle

Respuestas:

48

La inyección de campo es un poco demasiado "acción espeluznante a distancia" para mi gusto.

Considere el ejemplo que proporcionó en su publicación de Grupos de Google:

public class VeracodeServiceImplTest {


    @Tested(fullyInitialized=true)
    VeracodeServiceImpl veracodeService;
    @Tested(fullyInitialized=true, availableDuringSetup=true)
    VeracodeRepositoryImpl veracodeRepository;
    @Injectable private ResultsAPIWrapper resultsApiWrapper;
    @Injectable private AdminAPIWrapper adminApiWrapper;
    @Injectable private UploadAPIWrapper uploadApiWrapper;
    @Injectable private MitigationAPIWrapper mitigationApiWrapper;

    static { VeracodeRepositoryImpl.class.getName(); }
...
}

Entonces, básicamente, lo que está diciendo es que "tengo esta clase con estado privado, a la que he adjuntado @injectableanotaciones, lo que significa que el estado puede ser poblado automáticamente por algún agente externo, a pesar de que mi estado ha sido declarado privado". "

Entiendo las motivaciones para esto. Es un intento de evitar gran parte de la ceremonia que es inherente a organizar una clase correctamente. Básicamente, lo que uno dice es que "estoy cansado de escribir toda esta repetitiva, así que voy a anotar todo mi estado y dejar que el contenedor DI se encargue de prepararlo para mí".

Es un punto de vista perfectamente válido. Pero también es una solución alternativa para las características del lenguaje que posiblemente no deberían evitarse. Además, ¿por qué parar allí? Tradicionalmente, DI confía en que cada clase tenga una interfaz complementaria. ¿Por qué no eliminar todas esas interfaces con anotaciones también?

Considere la alternativa (esto será C #, porque lo conozco mejor, pero probablemente haya un equivalente exacto en Java):

public class VeracodeService
{        
    private readonly IResultsAPIWrapper _resultsApiWrapper;
    private readonly IAdminAPIWrapper _adminApiWrapper;
    private readonly IUploadAPIWrapper _uploadApiWrapper;
    private readonly IMitigationAPIWrapper _mitigationApiWrapper;

    // Constructor
    public VeracodeService(IResultsAPIWrapper resultsApiWrapper, IAdminAPIWrapper adminApiWrapper, IUploadAPIWrapper uploadApiWrapper,         IMitigationAPIWrapper mitigationApiWrapper)
    {
         _resultsAPIWrapper = resultsAPIWrapper;
         _adminAPIWrapper = adminAPIWrapper;
         _uploadAPIWrapper = uploadAPIWrapper;
         _mitigationAPIWrapper = mitigationAPIWrapper;
    }
}

Ya sé algunas cosas sobre esta clase. Es una clase inmutable; solo se puede establecer en el constructor (referencias, en este caso particular). Y debido a que todo se deriva de una interfaz, puedo intercambiar implementaciones en el constructor, que es donde entran tus simulacros.

Ahora todo lo que mi contenedor DI tiene que hacer es reflexionar sobre el constructor para determinar qué objetos necesita para renovarse. Pero esa reflexión se está haciendo en un miembro público, de una manera de primera clase; es decir, los metadatos ya forman parte de la clase, habiéndose declarado en el constructor, un método cuyo propósito expreso es proporcionar a la clase las dependencias que necesita.

De acuerdo, esto es un montón de repeticiones, pero así es como se diseñó el lenguaje. Las anotaciones parecen un truco sucio para algo que debería haberse incorporado al lenguaje mismo.

Robert Harvey
fuente
A menos que lea mal su publicación, en realidad no está viendo el ejemplo correctamente. Mi implementación de VeracodeServicees casi idéntica a la que escribiste (aunque en Java vs C #). El VeracodeServiceImplTestes en realidad una clase de prueba de unidad. Los @Injectablecampos son esencialmente objetos simulados que se insertan en el contexto. El @Testedcampo es el objeto / clase con el DI Constructor definido. Estoy de acuerdo con su punto de vista que prefiere la inyección de Constructor sobre la inyección de campo. Sin embargo, como mencioné, el autor de JMockit siente lo contrario y estoy tratando de entender por qué
Eric B.
Si miras la primera publicación en esa discusión, verás que tengo exactamente la misma definición en mi clase de Repo.
Eric B.
Si solo está hablando de clases de prueba, puede que no importe. Porque, clases de prueba.
Robert Harvey
No, no estoy hablando de clases de prueba. Estoy hablando de clases de producción. Simplemente sucede que el ejemplo en el grupo de discusión es una prueba unitaria, ya que el marco es un marco de prueba / burla. (Toda la discusión gira en torno a si el marco burlón debería admitir la inyección del constructor)
Eric B.
3
+1: Sí, no hay magia en la versión C #. Es una clase estándar, tiene dependencias, todas se inyectan en un punto, antes de que se use la clase. La clase se puede usar con un contenedor DI o no. Nada en la clase lo dice IOC o marco de "Inyección automática de dependencias".
Binario Worrier
27

El argumento de menos inicialización de prueba es válido, pero hay otras preocupaciones que deben tenerse en cuenta. En primer lugar, debes responder una pregunta:

¿Quiero que mi clase sea instanciable solo con reflexión?

El uso de inyecciones de campo significa reducir la compatibilidad de una clase a entornos de inyección de dependencia que crean instancias de objetos utilizando la reflexión y admiten estas anotaciones de inyección particulares. Algunas plataformas basadas en el lenguaje Java ni siquiera admiten la reflexión ( GWT ), por lo que la clase inyectada en campo no será compatible con ellas.

El segundo problema es el rendimiento . Una llamada de constructor (directa o por reflexión) siempre es más rápida que un montón de asignaciones de campos de reflexión. Los marcos de inyección de dependencias deben usar el análisis de reflexión para construir un árbol de dependencia y crear constructores de reflexión. Este proceso provoca un impacto adicional en el rendimiento.

El rendimiento afecta la mantenibilidad . Si cada conjunto de pruebas debe ejecutarse en algún tipo de contenedor de inyección de dependencia, una ejecución de prueba de unos pocos miles de pruebas unitarias puede durar decenas de minutos. Dependiendo del tamaño de una base de código, esto puede ser un problema.

Todo esto plantea muchas preguntas nuevas:

  1. ¿Cuál es la probabilidad de usar partes del código en otra plataforma?
  2. ¿Cuántas pruebas unitarias se escribirán?
  3. ¿Qué tan rápido necesito preparar cada nueva versión?
  4. ...

En general, cuanto más grande y más importante es un proyecto, más importantes son estos factores. Además, en términos de calidad, generalmente nos gustaría mantener el código alto en compatibilidad, comprobabilidad y mantenibilidad. Desde el punto de vista filosófico, la inyección de campo rompe la encapsulación , que es uno de los cuatro fundamentos de la programación orientada a objetos , que es el paradigma principal de Java.

Muchos, muchos argumentos en contra de la inyección de campo.

Maciej Chałapuk
fuente
3
+1 Golpeaste un nervio. No me gusta necesitar reflexión o un marco DI / IoC para instanciar una clase. Es como conducir a Roma para llegar a París desde Ámsterdam. Eso sí, me encanta DI. Simplemente no estoy seguro acerca de los marcos.
Marjan Venema
1
Grandes argumentos Expresa verbalmente muchos de los pensamientos que tuve, pero estoy feliz de ver a otros sentir lo mismo. Tan sorprendente como solía encontrar la inyección de campo, creo que me encantó mucho simplemente porque era "más fácil". Encuentro la inyección del constructor mucho más clara ahora.
Eric B.
19

La inyección de campo obtiene un voto definitivo de "No" de mi parte.

Al igual que Robert Harvey, es demasiado automático para mi gusto. Prefiero el código explícito sobre el implícito, y tolero la indirección solo cuando / cuando proporciona beneficios claros, ya que hace que el código sea más difícil de entender y razonar.

Al igual que Maciej Chałapuk, no me gusta necesitar reflexión o un marco DI / IoC para crear una instancia de una clase. Es como conducir a Roma para llegar a París desde Ámsterdam.

Eso sí, me encanta DI y todas las ventajas que trae. Simplemente no estoy seguro de necesitar los marcos para crear una instancia de una clase. Especialmente no el efecto que los contenedores IoC u otros marcos DI pueden tener en el código de prueba.

Me gusta que mis pruebas sean muy sencillas. No me gusta tener que pasar por la indirecta de configurar un contenedor IoC u otro marco de trabajo DI para luego configurar mi clase bajo prueba. Simplemente se siente incómodo.

Y hace que mis pruebas dependan del estado global del marco. Es decir, ya no puedo ejecutar dos pruebas en paralelo que requieren que se configure una instancia de clase X con diferentes dependencias.

Al menos con la inyección del constructor y del instalador, tiene la opción de no usar los marcos y / o la reflexión en sus pruebas.

Marjan Venema
fuente
Si usa, por ejemplo, Mockito mockito.org , no necesita un marco DI / IoC incluso cuando usa la inyección de campo, y puede ejecutar la prueba en paralelo y así sucesivamente.
Hans-Peter Störr
1
@hstoerr Niza. Sin embargo, eso debe significar que Mockito está usando la reflexión (o como se llame el equivalente Java de eso) ...
Marjan Venema
5

Bueno, esto parece una discusión polémica. Lo primero que debe abordarse es que la inyección de campo es diferente de la inyección de Setter . Ya trabajo con algunas personas que piensan que la inyección Field and Setter es lo mismo.

Entonces, para ser claros:

Inyección de campo:

@Autowired
private SomeService someService;

Setter inyección:

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

Pero, para mí, comparten las mismas preocupaciones. Y, mi favorito, la inyección del constructor:

@AutoWired
public MyService(SomeService someService) {
    this.someService = someService;
}

Tengo las siguientes razones para creer que la inyección del constructor es mejor que la inyección de setter / field (citaré algunos enlaces de Spring para demostrar mi punto):

  1. Prevenir dependencias circulares : Spring puede detectar dependencias circulares entre los Beans, como explicó @Tomasz. Ver también el Equipo de Primavera diciendo eso.
  2. Las dependencias de la clase son obvias : las dependencias de la clase son obvias en el constructor pero se ocultan con la inyección de campo / setter. Siento mis clases como una "caja negra" con inyección de campo / setter. Y hay (según mi experiencia) una tendencia a que los desarrolladores no se preocupen por la clase con muchas dependencias, eso es obvio cuando intenta burlarse de la clase usando la inyección del constructor (vea el siguiente elemento). Es más probable que se resuelva un problema que molesta a los desarrolladores. Además, una clase con demasiadas dependencias probablemente viola el Principio de responsabilidad única (SRP).
  3. Fácil y confiable de burlarse : en comparación con la inyección de campo , es más fácil burlarse en una prueba unitaria, porque no necesita ninguna técnica de simulación o reflexión de contexto para llegar al Bean privado declarado dentro de la clase y no se dejará engañar por él . Simplemente crea una instancia de la clase y pasa los frijoles burlados. Simple de entender y fácil.
  4. Las herramientas de calidad de código pueden ayudarlo : herramientas como Sonar pueden advertirle de que la clase se vuelve demasiado compleja, porque una clase con muchos parámetros es probablemente una clase demasiado compleja y necesita un refactorizador. Estas herramientas no pueden identificar el mismo problema cuando la clase está usando inyección Field / Setter.
  5. Código mejor e independiente : con menos @AutoWireddifusión entre su código, su código depende menos del marco. Sé que la posibilidad de cambiar simplemente el marco DI en un proyecto no justifica eso (¿quién lo hace, verdad?). Pero esto muestra, para mí, un código mejor (aprendí esto de una manera difícil con EJB y búsquedas). Y con Spring incluso puede eliminar la @AutoWiredanotación utilizando la inyección del constructor.
  6. Más anotaciones no siempre es la respuesta correcta : por algunas razones , realmente trato de usar anotaciones solo cuando son realmente útiles.
  7. Puede hacer que las inyecciones sean inmutables . La inmutabilidad es una característica muy bienvenida .

Si está buscando más información, le recomiendo este artículo antiguo (pero aún relevante) de Spring Blog que nos dice por qué usan tanta inyección de setter y la recomendación de usar la inyección de constructor:

la inyección de setter se usa con mucha más frecuencia de lo que cabría esperar, es el hecho de que los marcos como Spring en general, son mucho más adecuados para ser configurados por inyección de setter que por inyección de constructor

Y esta nota sobre por qué creen que el constructor es más adecuado para el código de la aplicación:

Creo que la inyección de constructor es mucho más utilizable para el código de aplicación que para el código marco. En el código de la aplicación, inherentemente tiene una menor necesidad de valores opcionales que necesita configurar

Pensamientos finales

Si no está realmente seguro de la ventaja del constructor, tal vez la idea de mezclar setter (no campo) con inyección de constructor es una opción, como lo explicó el equipo de Spring :

Dado que puede mezclar tanto DI basada en constructor como en setter, es una buena regla general usar argumentos de constructor para dependencias obligatorias y establecedores para dependencias opcionales

Pero recuerde que la inyección de campo es, probablemente, la que debe evitarse entre los tres.

Dherik
fuente
-5

¿Es probable que su dependencia cambie durante la vida de la clase? Si es así, use la inyección de campo / propiedad. De lo contrario, use la inyección del constructor.

Darren Young
fuente
2
Esto realmente no explica nada. ¿Por qué inyectar campos privados cuando puede hacerlo de la manera adecuada?
Robert Harvey
¿Dónde dice algo sobre campos privados? Estoy hablando de la interfaz pública de una clase /
Darren Young
No hay argumento sobre la inyección de método vs inyección de constructor en la pregunta. El autor de JMockit considera que ambos son malos. Mira el título de la pregunta.
Robert Harvey
¿La pregunta no dice inyección de campo versus inyección de constructor?
Darren Young
1
La inyección de campo es un animal completamente diferente. Estás introduciendo valores en miembros privados .
Robert Harvey