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 .