Inyección de Dependencia vs Ubicación del Servicio

Actualmente estoy sopesando las ventajas y desventajas entre DI y SL. Sin embargo, me he encontrado en el siguiente catch 22 que implica que debería usar SL para todo, y solo inyectar un contenedor IoC en cada clase.

DI Catch 22:

Algunas dependencias, como Log4Net, simplemente no se ajustan a DI. Llamo a estas metadependencias y creo que deberían ser opacas al código de llamada. Mi justificación es que si una clase simple ‘D’ se implementó originalmente sin registro, y luego crece para requerir el registro, las clases dependientes ‘A’, ‘B’ y ‘C’ ahora deben de alguna manera obtener esta dependencia y pasarla de ‘A’ a ‘D’ (asumiendo que ‘A’ compone ‘B’, ‘B’ compone ‘C’, y así sucesivamente). Ahora hemos realizado cambios significativos en el código solo porque necesitamos iniciar sesión en una clase.

Por lo tanto, necesitamos un mecanismo opaco para obtener meta-dependencias. Dos vienen a la mente: Singleton y SL. El primero tiene limitaciones conocidas, principalmente con respecto a las capacidades rígidas de determinación del scope: en el mejor de los casos, Singleton utilizará una fábrica abstracta que se almacena en el ámbito de la aplicación (es decir, en una variable estática). Esto permite cierta flexibilidad, pero no es perfecto.

Una mejor solución sería inyectar un contenedor IoC en dichas clases, y luego usar SL desde esa clase para resolver estas meta-dependencias del contenedor.

De ahí la captura 22: porque la clase ahora está siendo inyectada con un contenedor IoC, entonces ¿por qué no usarlo para resolver todas las otras dependencias también?

Te agradecería mucho tus pensamientos 🙂

Debido a que ahora la clase está siendo inyectada con un contenedor IoC, ¿por qué no usarlo para resolver todas las demás dependencias también?

El uso del patrón de localizador de servicios derrota por completo uno de los puntos principales de la dependency injection. El punto de la dependency injection es hacer que las dependencias sean explícitas. Una vez que oculta esas dependencias al no convertirlas en parámetros explícitos en un constructor, ya no está haciendo una dependency injection completa.

Estos son todos los constructores de una clase llamada Foo (configurada con el tema de la canción de Johnny Cash):

Incorrecto:

 public Foo() { this.bar = new Bar(); } 

Incorrecto:

 public Foo() { this.bar = ServiceLocator.Resolve(); } 

Incorrecto:

 public Foo(ServiceLocator locator) { this.bar = locator.Resolve(); } 

Derecha:

 public Foo(Bar bar) { this.bar = bar; } 

Solo este último hace explícita la dependencia de Bar .

En cuanto al registro, hay una forma correcta de hacerlo sin que penetre en su código de dominio (no debería hacerlo, pero si lo hace, entonces usa el período de dependency injection). Sorprendentemente, los contenedores de IoC pueden ayudar con este problema. Comience aquí .

Service Locator es un anti-patrón, por razones excelentemente descritas en http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx . En términos de registro, puede tratar eso como una dependencia como cualquier otra, e inyectar una abstracción a través de la inyección de propiedad o constructor.

La única diferencia con log4net es que requiere el tipo de la persona que llama que usa el servicio. Usando Ninject (u otro contenedor) ¿Cómo puedo averiguar el tipo que solicita el servicio? describe cómo puedes resolver esto (usa Ninject, pero es aplicable a cualquier contenedor IoC).

De forma alternativa, podría pensar en el inicio de sesión como una preocupación transversal, que no es apropiado para combinar con su código de lógica de negocios, en cuyo caso puede usar la interceptación proporcionada por muchos contenedores de IoC. http://msdn.microsoft.com/en-us/library/ff647107.aspx describe el uso de la interceptación con Unity.

Mi opinión es que depende. A veces, uno es mejor y, a veces, otro. Pero yo diría que, en general, prefiero DI. Hay pocas razones para eso.

  1. Cuando la dependencia se inyecta de alguna manera en el componente, se puede tratar como parte de su interfaz. Por lo tanto, es más fácil para el usuario del componente suministrar estas dependencias, ya que son visibles. En el caso de SL inyectado o SL estático, las dependencias están ocultas y el uso del componente es un poco más difícil.

  2. Las dependencias inyectadas son mejores para las pruebas unitarias porque simplemente puede burlarse de ellas. En el caso de SL, tiene que configurar Locator + dependencias falsas de nuevo. Entonces es más trabajo.

A veces, el registro puede implementarse utilizando AOP , de modo que no se mezcle con la lógica comercial.

De lo contrario, las opciones son:

  • use una dependencia opcional (como la propiedad setter) y para la prueba unitaria no inyecte ningún registrador. El contenedor IOC se ocupará de configurarlo automáticamente si se ejecuta en producción.
  • Cuando tiene una dependencia que casi todos los objetos de su aplicación utilizan (el objeto “logger” es el ejemplo más común), es uno de los pocos casos en que el antipatrón individual se convierte en una buena práctica. Algunas personas llaman a estos “buenos singletons” un Contexto Ambiental : http://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/

Por supuesto, este contexto debe ser configurable, de modo que pueda usar stub / mock para pruebas unitarias. Otro uso sugerido de AmbientContext es colocar allí el proveedor actual de Fecha / Hora, para que pueda anclarlo durante la prueba de la unidad y acelerar el tiempo si lo desea.

Si el ejemplo solo toma log4net como dependencia, entonces solo necesita hacer esto:

 ILog log = LogManager.GetLogger(typeof(Foo)); 

No tiene sentido inyectar la dependencia ya que log4net proporciona un registro granular tomando el tipo (o una cadena) como parámetro.

Además, DI no está correlacionado con SL. En mi humilde opinión, el propósito de ServiceLocator es resolver dependencias opcionales.

Por ejemplo: si el SL proporciona una interfaz ILog, escribiré daa de registro.

He utilizado el framework Google Guice DI en Java y descubrí que hace mucho más que hacer las pruebas más fáciles. Por ejemplo, necesitaba un registro por aplicación (no clase), con el requisito adicional de que todo mi código de biblioteca común use el registrador en el contexto de llamada actual. Inyectar el registrador lo hizo posible. Es cierto que todo el código de la biblioteca debe cambiarse: el registrador se inyectó en los constructores. Al principio, me resistí a este enfoque debido a todos los cambios de encoding requeridos; finalmente me di cuenta de que los cambios tenían muchos beneficios:

  • El código se hizo más simple
  • El código se volvió mucho más robusto
  • Las dependencias de una clase se hicieron obvias
  • Si había muchas dependencias, era una clara indicación de que una clase necesitaba refactorización
  • Los singleton estáticos fueron eliminados
  • La necesidad de objetos de sesión o de contexto desapareció
  • Multi-threading se hizo mucho más fácil, porque el contenedor DI podría construirse para contener solo un hilo, eliminando así la contaminación cruzada inadvertida

Huelga decir que ahora soy un gran admirador de DI, y lo uso para todas las aplicaciones menos triviales.

Hemos llegado a un compromiso: usar DI pero agrupar las dependencias de nivel superior en un objeto evitando refacciones si esas dependencias cambian.

En el ejemplo a continuación, podemos agregar a ‘ServiceDependencies’ sin tener que refactorizar todas las dependencias derivadas.

Ejemplo:

 public ServiceDependencies{ public ILogger Logger{get; private set;} public ServiceDependencies(ILogger logger){ this.Logger = logger; } } public abstract class BaseService{ public ILogger Logger{get; private set;} public BaseService(ServiceDependencies dependencies){ this.Logger = dependencies.Logger; //don't expose 'dependencies' } } public class DerivedService(ServiceDependencies dependencies, ISomeOtherDependencyOnlyUsedByThisService additionalDependency) : base(dependencies){ //set local dependencies here. } 

Sé que la gente realmente está diciendo que DI es el único patrón bueno de COI, pero no entiendo esto. Intentaré vender SL un poco. Utilizaré el nuevo framework MVC Core para mostrarle lo que quiero decir. Los primeros motores DI son realmente complejos. Lo que las personas realmente quieren decir cuando dicen DI, es utilizar algún marco como Unity, Ninject, Autofac … que hacen todo el trabajo pesado por ti, donde SL puede ser tan simple como hacer una clase de fábrica. Para un proyecto pequeño y rápido, esta es una manera fácil de hacer COI sin aprender un marco completo para la DI adecuada, puede que no sea tan difícil de aprender, pero aún así. Ahora al problema que DI puede convertirse. Usaré una cita de documentos de MVC Core. “ASP.NET Core está diseñado desde cero para soportar y aprovechar la dependency injection”. La mayoría de las personas dice que con respecto a DI “el 99% de su base de código no debería tener conocimiento de su contenedor de IoC”. Entonces, ¿por qué tendrían que diseñar desde cero si solo el 1% del código debería tenerlo en cuenta, no era cierto que MVC soportaba DI? Bueno, este es el gran problema de DI depende de DI. Hacer que todo funcione “COMO DEBE HACER” requiere mucho trabajo. Si observa la nueva inyección de acción, esto no depende de DI si usa el atributo [FromServices] . Ahora la gente de DI dirán que NO, se supone que debes ir con fábricas, no con esto, pero como puedes ver, ni siquiera las personas que hacen MVC lo hicieron bien. El problema de DI es visible en Filtros, así que mira lo que tienes que hacer para obtener DI en un filtro

 public class SampleActionFilterAttribute : TypeFilterAttribute { public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl)) { } private class SampleActionFilterImpl : IActionFilter { private readonly ILogger _logger; public SampleActionFilterImpl(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); } public void OnActionExecuting(ActionExecutingContext context) { _logger.LogInformation("Business action starting..."); // perform some business logic work } public void OnActionExecuted(ActionExecutedContext context) { // perform some business logic work _logger.LogInformation("Business action completed."); } } } 

Donde si usaste SL, podrías haber hecho esto con var _logger = Locator.Get () ;. Y luego llegamos a las Vistas. Con todo lo que hay de buena voluntad con respecto a DI, tuvieron que usar SL para las vistas. la nueva syntax @inject StatisticsService StatsService es lo mismo que var StatsService = Locator.Get(); . La parte más anunciada de DI es la prueba unitaria. Pero lo que la gente hace es simplemente probar allí los servicios falsos sin propósito o tener que conectar el motor DI para hacer pruebas reales. Y sé que puedes hacer cualquier cosa mal pero la gente termina haciendo un localizador de SL incluso si no saben de qué se trata. Donde no mucha gente hace DI sin leerlo antes. Mi mayor problema con DI es que el usuario de la clase debe conocer el funcionamiento interno de la clase para usarla.
SL se puede utilizar de una buena manera y tiene algunas ventajas sobre todo su simplicidad.

Esto se refiere al ‘Localizador de servicios es un antipatrón’ de Mark Seeman. Podría estar equivocado aquí. Pero pensé que debería compartir mis pensamientos también.

 public class OrderProcessor : IOrderProcessor { public void Process(Order order) { var validator = Locator.Resolve(); if (validator.Validate(order)) { var shipper = Locator.Resolve(); shipper.Ship(order); } } } 

El método Process () para OrderProcessor en realidad no sigue el principio de ‘Inversión de control’. También rompe el principio de responsabilidad única en el nivel de método. ¿Por qué debería preocuparse un método de instanciar el

objetos (a través de una clase SL o nueva) que necesita para lograr cualquier cosa.

En lugar de tener el método Process () para crear los objetos, el constructor puede tener los parámetros para los objetos respectivos (leer dependencias) como se muestra a continuación. Entonces, ¿cómo puede un localizador de servicios ser diferente de un COI?

envase. Y también ayudará en las pruebas unitarias.

 public class OrderProcessor : IOrderProcessor { public OrderProcessor(IOrderValidator validator, IOrderShipper shipper) { this.validator = validator; this.shipper = shipper; } public void Process(Order order) { if (this.validator.Validate(order)) { shipper.Ship(order); } } } //Caller public static void main() //this can be a unit test code too. { var validator = Locator.Resolve(); // similar to a IOC container var shipper = Locator.Resolve(); var orderProcessor = new OrderProcessor(validator, shipper); orderProcessor.Process(order); } 

Sé que esta pregunta es un poco vieja, solo pensé en dar mi opinión.

En realidad, 9 de cada 10 veces realmente no necesita SL y debe confiar en DI. Sin embargo, hay algunos casos en los que debes usar SL. Un área que me encuentro usando SL (o una variación de eso) es en el desarrollo del juego.

Otra ventaja de SL (en mi opinión) es la capacidad de pasar clases internal .

A continuación hay un ejemplo:

 internal sealed class SomeClass : ISomeClass { internal SomeClass() { // Add the service to the locator ServiceLocator.Instance.AddService(this); } // Maybe remove of service within finalizer or dispose method if needed. internal void SomeMethod() { Console.WriteLine("The user of my library doesn't know I'm doing this, let's keep it a secret"); } } public sealed class SomeOtherClass { private ISomeClass someClass; public SomeOtherClass() { // Get the service and call a method someClass = ServiceLocator.Instance.GetService(); someClass.SomeMethod(); } } 

Como puede ver, el usuario de la biblioteca no tiene idea de que se haya llamado a este método, porque no lo hicimos, no es que pudiéramos hacerlo de todos modos.