¿Cuál es la diferencia entre usar inyección de dependencia con un contenedor y usar un localizador de servicios?

107

Entiendo que instanciar directamente dependencias dentro de una clase se considera una mala práctica. Esto tiene sentido, ya que al hacerlo se acopla todo lo que a su vez hace que las pruebas sean muy difíciles.

Casi todos los marcos que he encontrado parecen favorecer la inyección de dependencia con un contenedor en lugar de usar localizadores de servicios. Ambos parecen lograr lo mismo al permitir que el programador especifique qué objeto debe devolverse cuando una clase requiere una dependencia.

¿Cuál es la diferencia entre los dos? ¿Por qué elegiría uno sobre el otro?

tom6025222
fuente
3
Esto se ha preguntado (y respondido) en StackOverflow: stackoverflow.com/questions/8900710/…
Kyralessa
2
En mi opinión, no es raro que los desarrolladores engañen a otros para que piensen que su localizador de servicios es una inyección de dependencia. Lo hacen porque a menudo se piensa que la inyección de dependencia es más avanzada.
Gherman
1
Me sorprende que nadie haya mencionado que con los Localizadores de servicios es más difícil saber cuándo se puede destruir un recurso transitorio. Siempre pensé que esa era la razón principal.
Buh Buh

Respuestas:

126

Cuando el propio objeto es responsable de solicitar sus dependencias, en lugar de aceptarlas a través de un constructor, está ocultando información esencial. Es solo un poco mejor que el caso de uso muy acoplado newpara crear instancias de sus dependencias. Reduce el acoplamiento porque, de hecho, puede cambiar las dependencias que obtiene, pero todavía tiene una dependencia que no puede sacudir: el localizador de servicios. Eso se convierte en lo que todo depende.

Un contenedor que proporciona dependencias a través de argumentos de constructor proporciona la mayor claridad. Vemos por adelantado que un objeto necesita tanto an AccountRepositorycomo a PasswordStrengthEvaluator. Cuando se utiliza un localizador de servicios, esa información es menos evidente de inmediato. Vería de inmediato un caso en el que un objeto tiene, oh, 17 dependencias, y se dirá a sí mismo: "Hmm, eso parece mucho. ¿Qué está pasando allí?" Las llamadas a un localizador de servicios pueden extenderse en torno a los diversos métodos y esconderse detrás de la lógica condicional, y es posible que no se dé cuenta de que ha creado una "clase de Dios", una que hace todo. Tal vez esa clase se podría refactorizar en 3 clases más pequeñas que están más enfocadas y, por lo tanto, son más comprobables.

Ahora considere realizar pruebas. Si un objeto usa un localizador de servicios para obtener sus dependencias, su marco de prueba también necesitará un localizador de servicios. En una prueba, configurará el localizador de servicios para suministrar las dependencias al objeto bajo prueba, tal vez ay FakeAccountRepositorya VeryForgivingPasswordStrengthEvaluator, y luego ejecutará la prueba. Pero eso es más trabajo que especificar dependencias en el constructor del objeto. Y su marco de prueba también se vuelve dependiente del localizador de servicios. Es otra cosa que debe configurar en cada prueba, lo que hace que escribir pruebas sea menos atractivo.

Busque "El localizador de servicios es un antipatrón" para el artículo de Mark Seeman al respecto. Si estás en el mundo .Net, consigue su libro. Es muy bueno.

Carl Raymond
fuente
1
Leyendo la pregunta que pensé sobre el Código Adaptativo a través de C # por Gary McLean Hall, que se alinea bastante bien con esta respuesta. Tiene algunas buenas analogías como que el localizador de servicios es la clave para una caja fuerte; cuando se entrega a una clase, puede crear dependencias a voluntad que podrían no ser fáciles de detectar.
Dennis
10
Una cosa que necesita la OMI que se añade a la constructor supplied dependenciescontra service locatores que el primero se puede verfied tiempo de compilación, mientras que el segundo puede solamente ser verificada en tiempo de ejecución.
andras
2
Además, un localizador de servicios no desalienta a un componente de tener muchas dependencias. Si tiene que agregar 10 parámetros a su constructor, tiene un buen sentido de que algo está mal con su código. Sin embargo, si solo hace 10 llamadas estáticas a un localizador de servicios, puede que no sea obvio que tiene algún tipo de problema. He trabajado en un gran proyecto con un localizador de servicios y este fue el mayor problema. Fue demasiado fácil hacer un corto circuito en un nuevo camino en lugar de sentarse, reflexionar, rediseñar y refactorizar.
Paso
1
Sobre las pruebas: But that's more work than specifying dependencies in the object's constructor.me gustaría objetar. Con un localizador de servicios solo necesita especificar las 3 dependencias que realmente necesita para su prueba. Con un DI basado en el constructor, debe especificar TODOS los 10, incluso si 7 no se utilizan.
Vilx-
44
@ Vilx: si tiene varios parámetros de constructor no utilizados, es probable que su clase esté violando el principio de responsabilidad única.
RB.
79

Imagina que eres un trabajador en una fábrica que fabrica zapatos .

Usted es responsable de armar los zapatos, por lo que necesitará muchas cosas para hacerlo.

  • Cuero
  • Cinta métrica
  • pegamento
  • Uñas
  • Martillo
  • tijeras
  • Cordones de zapatos

Y así.

Estás trabajando en la fábrica y estás listo para comenzar. Tiene una lista de instrucciones sobre cómo proceder, pero todavía no tiene ninguno de los materiales o herramientas.

Un localizador de servicios es como un capataz que puede ayudarlo a obtener lo que necesita.

Usted le pregunta al Localizador de servicios cada vez que necesita algo, y se van a buscarlo. El Localizador de servicios ha sido informado con anticipación sobre lo que es probable que solicite y cómo encontrarlo.

Sin embargo, será mejor que no pidas algo inesperado. Si el Localizador no ha sido informado con anticipación sobre una herramienta o material en particular, no podrá obtenerlo por usted, y simplemente se encogerán de hombros.

localizador de servicios

Un contenedor de inyección de dependencia (DI) es como una gran caja que se llena con todo lo que todos necesitan al comienzo del día.

Cuando se inicia la fábrica, el Big Boss conocido como la raíz de composición toma el contenedor y entrega todo a los gerentes de línea .

Los gerentes de línea ahora tienen lo que necesitan para realizar sus tareas del día. Toman lo que tienen y pasan lo que necesitan a sus subordinados.

Este proceso continúa, con dependencias goteando por la línea de producción. Finalmente, se muestra un contenedor de materiales y herramientas para su capataz.

Su capataz ahora distribuye exactamente lo que necesita para usted y otros trabajadores, sin siquiera pedirlos.

Básicamente, tan pronto como te presentas para trabajar, todo lo que necesitas ya está allí en una caja esperándote. No necesitabas saber nada sobre cómo conseguirlos.

contenedor de inyección de dependencia

Rowan Freeman
fuente
45
Esta es una excelente descripción de lo que son cada una de esas 2 cosas, ¡diagramas súper atractivos también! Pero esto no responde a la pregunta de "¿Por qué elegir uno sobre el otro"?
Matthieu M.
21
"Sin embargo, es mejor que no pidas algo inesperado. Si el Localizador no ha sido informado con anticipación sobre una herramienta o material en particular" Pero eso también puede ser cierto para un contenedor DI.
Kenneth K.
55
@FrankHopkins Si omite registrar una interfaz / clase con su contenedor DI, su código seguirá compilando y fallará rápidamente en el tiempo de ejecución.
Kenneth K.
3
Es igualmente probable que ambos fallen, dependiendo de cómo estén configurados. Pero con un contenedor DI, es más probable que te encuentres con un problema antes, cuando se construye la clase X, y no más tarde, cuando la clase X realmente necesita esa dependencia. Podría decirse que es mejor, porque es más fácil encontrarse con el problema, encontrarlo y resolverlo. Sin embargo, estoy de acuerdo con Kenneth en que la respuesta sugiere que este problema existe solo para los localizadores de servicios, lo cual definitivamente no es el caso.
GolezTrol
3
Gran respuesta, pero creo que se pierde otra cosa; es decir, si se le pregunta al capataz en cualquier etapa de un elefante, y él lo hace sabe cómo conseguir uno, que va a conseguir uno para usted no hay preguntas asked- a pesar de que no lo necesita. Y alguien que esté tratando de depurar un problema en el piso (un desarrollador, si lo desea) se preguntará si está haciendo un elefante aquí en este escritorio, lo que les hace más difícil razonar sobre lo que realmente salió mal. Mientras que con DI, usted, la clase trabajadora, declara por adelantado cada dependencia que necesita incluso antes de comenzar el trabajo, lo que hace que sea más fácil razonar.
Stephen Byrne
10

Un par de puntos extra que he encontrado al recorrer la web:

  • Inyectar dependencias en el constructor facilita la comprensión de lo que necesita una clase. Los IDE modernos indicarán qué argumentos acepta el constructor y sus tipos. Si utiliza un localizador de servicios, debe leer la clase antes de saber qué dependencias son necesarias.
  • La inyección de dependencia parece adherirse al principio "decir no preguntar" más que los localizadores de servicios. Al ordenar que una dependencia sea de un tipo específico, usted "dice" qué dependencias son necesarias. Es imposible crear una instancia de la clase sin pasar las dependencias requeridas. Con un localizador de servicios, usted "solicita" un servicio y si el localizador de servicios no está configurado correctamente, es posible que no obtenga lo que se requiere.
tom6025222
fuente
4

Llego tarde a esta fiesta pero no puedo resistirme.

¿Cuál es la diferencia entre usar inyección de dependencia con un contenedor y usar un localizador de servicios?

A veces ninguno en absoluto. Lo que hace la diferencia es lo que sabe sobre qué.

Usted sabe que está utilizando un localizador de servicios cuando el cliente que busca la dependencia conoce el contenedor. Un cliente que sabe cómo encontrar sus dependencias, incluso cuando atraviesa un contenedor para obtenerlas, es el patrón del localizador de servicios.

¿Esto significa que si desea evitar el localizador de servicios no puede usar un contenedor? No. Solo tiene que evitar que los clientes sepan sobre el contenedor. La diferencia clave es dónde usa el contenedor.

Digamos Clientnecesidades Dependency. El contenedor tiene a Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Acabamos de seguir el patrón de localización del servicio porque Clientsabe cómo encontrarlo Dependency. Claro que usa un código rígido, ClassPathXmlApplicationContextpero incluso si inyecta que todavía tiene un localizador de servicio porque Clientllama beanfactory.getBean().

Para evitar el localizador de servicios, no tiene que abandonar este contenedor. Solo tienes que sacarlo Clientpara Clientque no lo sepas.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Observe cómo Clientahora no tiene idea de que el contenedor existe:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Mueva el contenedor fuera de todos los clientes y péguelo en main donde puede construir un gráfico de objetos de todos sus objetos de larga vida. Elija uno de esos objetos para extraer y llame a un método y comience a marcar todo el gráfico.

Eso mueve toda la construcción estática a los contenedores XML pero mantiene a todos sus clientes felizmente ignorantes de cómo encontrar sus dependencias.

¡Pero main todavía sabe cómo localizar dependencias! Si lo hace Pero al no difundir ese conocimiento, ha evitado el problema central del localizador de servicios. La decisión de usar un contenedor ahora se toma en un solo lugar y podría cambiarse sin tener que reescribir a cientos de clientes.

naranja confitada
fuente
1

Creo que la forma más fácil de entender la diferencia entre los dos y por qué un contenedor DI es mucho mejor que un localizador de servicios es pensar por qué hacemos la inversión de dependencia en primer lugar.

Hacemos una inversión de dependencia para que cada clase explícitamente indique exactamente de qué depende para funcionar. Lo hacemos porque esto crea el acoplamiento más flojo que podemos lograr. Cuanto más flojo es el acoplamiento, más fácil es probar y refactorizar (y generalmente requiere la menor refactorización en el futuro porque el código es más limpio).

Veamos la siguiente clase:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

En esta clase, declaramos explícitamente que necesitamos un IOutputProvider y nada más para que esta clase funcione. Esto es totalmente comprobable y depende de una única interfaz. Puedo mover esta clase a cualquier parte de mi aplicación, incluido un proyecto diferente y todo lo que necesita es acceso a la interfaz IOutputProvider. Si otros desarrolladores desean agregar algo nuevo a esta clase, que requiere una segunda dependencia, tienen que ser explícitos sobre lo que necesitan en el constructor.

Eche un vistazo a la misma clase con un localizador de servicios:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Ahora he agregado el localizador de servicios como la dependencia. Aquí están los problemas que son inmediatamente obvios:

  • El primer problema con esto es que se necesita más código para lograr el mismo resultado. Más código es malo. No es mucho más código, pero aún es más.
  • El segundo problema es que mi dependencia ya no es explícita . Todavía necesito inyectar algo en la clase. Excepto que ahora lo que quiero no es explícito. Está oculto en una propiedad de lo que solicité. Ahora necesito acceso tanto a ServiceLocator como a IOutputProvider si quiero mover la clase a un ensamblado diferente.
  • El tercer problema es que otro desarrollador puede tomar una dependencia adicional que ni siquiera se da cuenta de que la está tomando cuando agrega código a la clase.
  • Finalmente, este código es más difícil de probar (incluso si ServiceLocator es una interfaz) porque tenemos que burlarnos de ServiceLocator y IOutputProvider en lugar de solo IOutputProvider

Entonces, ¿por qué no hacemos que el localizador de servicios sea una clase estática? Vamos a ver:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Esto es mucho más simple, ¿verdad?

Incorrecto.

Digamos que IOutputProvider está implementado por un servicio web de muy larga duración que escribe la cadena en quince bases de datos diferentes en todo el mundo y tarda mucho tiempo en completarse.

Intentemos probar esta clase. Necesitamos una implementación diferente de IOutputProvider para la prueba. ¿Cómo escribimos el examen?

Bueno, para hacer eso necesitamos hacer una configuración elegante en la clase estática ServiceLocator para usar una implementación diferente de IOutputProvider cuando la prueba lo invoca. Incluso escribir esa oración fue doloroso. Implementarlo sería tortuoso y sería una pesadilla de mantenimiento . Nunca deberíamos necesitar modificar una clase específicamente para pruebas, especialmente si esa clase no es la clase que realmente estamos tratando de probar.

Así que ahora te queda con a) una prueba que está causando cambios de código molestos en la clase ServiceLocator no relacionada; o b) ninguna prueba en absoluto. Y también te queda una solución menos flexible.

Así la clase localizador de servicios tiene que ser inyectado en el constructor. Lo que significa que nos quedamos con los problemas específicos mencionados anteriormente. El localizador de servicios requiere más código, le dice a otros desarrolladores que necesita cosas que no necesita, alienta a otros desarrolladores a escribir un código peor y nos da menos flexibilidad para avanzar.

En pocas palabras, los localizadores de servicios aumentan el acoplamiento en una aplicación y alientan a otros desarrolladores a escribir código altamente acoplado .

Stephen
fuente
Service Locators (SL) y Dependency Injection (DI) resuelven el mismo problema de manera similar. La única diferencia es si está "diciéndole" a DI o "solicitando" SL dependencias.
Matthew Whited
@MatthewWhited La principal diferencia es la cantidad de dependencias implícitas que tiene con un localizador de servicios. Y eso hace una gran diferencia en el mantenimiento a largo plazo y la estabilidad del código.
Stephen
Realmente no lo es. Tiene el mismo número de dependencias de cualquier manera.
Matthew Whited
Inicialmente sí, pero puede tomar dependencias con un localizador de servicios sin darse cuenta de que está tomando dependencias. Lo cual derrota una gran parte de la razón por la cual hacemos inversión de dependencia en primer lugar. Una clase depende de las propiedades A y B, otra depende de B y C. De repente, el Localizador de servicios se convierte en una clase divina. El contenedor DI no lo hace y las dos clases solo dependen adecuadamente de A y B y B y C respectivamente. Esto hace que refactorizarlos sea cien veces más fácil. Los Localizadores de servicios son insidiosos porque parecen iguales a DI pero no lo son.
Stephen