dependency-injection - patron - manejo de dependencias c#
Uso de marcos de inyección de dependencias para clases con muchas dependencias (6)
Primero:
Puede abordarlo creando un contenedor para contener sus dependencias "poco interesantes" (ILog, ICache, IApplicationSettings, etc.) y usar inyección de constructor para inyectar eso, luego interno al constructor, hidratar los campos del servicio del contenedor. Resolver ( )? No estoy seguro de que me gustaría, pero, bueno, es una posibilidad.
Alternativamente, es posible que desee utilizar la nueva interfaz común IServiceLocator ( http://blogs.msdn.com/gblock/archive/2008/10/02/iservicelocator-a-step-toward-ioc-container-service-locator-detente .aspx ) en lugar de inyectar las dependencias?
Segundo:
¿Podría usar la inyección setter para las dependencias opcionales / bajo demanda? Creo que iría a inyectar fábricas y novedades desde allí a pedido.
He estado buscando varios frameworks de inyección de dependencia para .NET ya que creo que el proyecto en el que estoy trabajando se beneficiaría enormemente. Aunque creo que tengo una buena comprensión de las capacidades de estos marcos, todavía no estoy muy claro sobre la mejor manera de introducirlos en un sistema grande. La mayoría de las demos (comprensiblemente) tienden a ser de clases bastante simples que tienen una o dos dependencias.
Tengo tres preguntas ...
En primer lugar , ¿cómo se manejan las dependencias comunes pero poco interesantes, por ejemplo, ILog, IApplicationSettings, IPermissions, IAudit? Parece excesivo para cada clase tener estos como parámetros en su constructor. ¿Sería mejor utilizar una instancia estática del contenedor DI para obtener estos cuando sean necesarios?
MyClass(ILog log, IAudit audit, IPermissions permissions, IApplicationSettings settings)
// ... versus ...
ILog log = DIContainer.Get<ILog>();
En segundo lugar , ¿cómo aborda las dependencias que podrían usarse, pero puede ser costoso de crear? Ejemplo: una clase puede tener una dependencia en una interfaz ICDBurner pero no quiere que se cree la implementación concreta a menos que se haya utilizado la función de grabación de CD. ¿Transfiere interfaces a fábricas (por ejemplo, ICDBurnerFactory) en el constructor, o vuelve con alguna forma estática de llegar directamente al Contenedor DI y solicitarlo en el punto en que se necesita?
En tercer lugar , supongamos que tiene una aplicación grande de Windows Forms, en la que el componente GUI de nivel superior (por ejemplo, MainForm) es el padre de potencialmente cientos de subpaneles o formularios modales, cada uno de los cuales puede tener varias dependencias. ¿Significa esto que MainForm debería configurarse para tener como dependencias el superconjunto de todas las dependencias de sus hijos? Y si lo hicieras, ¿no terminaría creando un enorme monstruo autoinflable que construye todas las clases que podría necesitar en el momento de crear MainForm, perdiendo tiempo y memoria en el proceso?
Primero:
Puede inyectar estos objetos, cuando sea necesario, como miembros en lugar de en el constructor. De esta forma, no tiene que hacer cambios en el constructor a medida que cambia su uso, y tampoco necesita usar una función estática.
Segundo:
Pase en algún tipo de constructor o fábrica.
Tercero:
Cualquier clase solo debería tener aquellas dependencias que ella misma requiera. Las subclases deberían inyectarse con sus propias dependencias específicas.
Primero: agregue las dependencias simples a su constructor según sea necesario. No es necesario agregar cada tipo a cada constructor, simplemente agregue los que necesita. Necesito otro, simplemente expanda el constructor. El rendimiento no debería ser una gran cosa ya que la mayoría de estos tipos probablemente sean únicos, por lo que ya se crearon después de la primera llamada. No use un contenedor DI estático para crear otros objetos. En su lugar, agregue el contenedor DI a sí mismo para que pueda resolverse como una dependencia. Entonces algo como esto (asumiendo la Unidad por el momento)
IUnityContainer container = new UnityContainer();
container.RegisterInstance<IUnityContainer>(container);
De esta forma, puede agregar una dependencia en IUnityContainer y usarla para crear objetos costosos o que rara vez se necesitan. La principal ventaja es que es mucho más fácil cuando se prueban las unidades ya que no hay dependencias estáticas.
Segundo: no es necesario pasar en una clase de fábrica. Usando la técnica anterior, puede usar el contenedor DI para crear objetos caros cuando sea necesario.
Tres: agregue el contenedor DI y las dependencias ligeras singleton al formulario principal y cree el resto a través del contenedor DI según sea necesario. Toma un poco más de código, pero como dijiste, el costo de inicio y el consumo de memoria de la forma principal irían por las nubes si creas todo en el momento del inicio.
Para responder parcialmente a mi primera pregunta, acabo de encontrar una publicación de blog de Jeremy Miller, que muestra cómo Structure Map y setter injection se pueden usar para poblar automáticamente las propiedades públicas de tus objetos. Él usa ILogger como un ejemplo:
var container = new Container(r =>
{
r.FillAllPropertiesOfType<ILogger>().TheDefault.Is
.ConstructedBy(context => new Logger(context.ParentType));
});
Esto significa que cualquier clase con una propiedad ILogger, por ejemplo:
public class ClassWithLogger
{
public ILogger Logger { get; set; }
}
public class ClassWithLogger2
{
public ILogger Logger { get; set; }
}
tendrá su propiedad Logger configurada automáticamente cuando esté construida:
container.GetInstance<ClassWithLogger>();
Tengo un caso similar relacionado con "costoso de crear y podría usarse", donde en mi propia implementación de IoC, estoy agregando soporte automático para servicios de fábrica.
Básicamente, en lugar de esto:
public SomeService(ICDBurner burner)
{
}
harías esto:
public SomeService(IServiceFactory<ICDBurner> burnerFactory)
{
}
ICDBurner burner = burnerFactory.Create();
Esto tiene dos ventajas:
- Detrás de escena, el contenedor de servicio que resolvió su servicio también se usa para resolver el quemador, siempre y cuando se solicite
- Esto alivia las preocupaciones que he visto antes en este tipo de casos donde la forma típica sería inyectar el contenedor del servicio como un parámetro para su servicio, diciendo básicamente: "Este servicio requiere otros servicios, pero no voy a poder hacerlo fácilmente". te digo cuáles "
El objeto de fábrica es bastante fácil de hacer y resuelve muchos problemas.
Aquí está mi clase de fábrica:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LVK.IoC.Interfaces;
using System.Diagnostics;
namespace LVK.IoC
{
/// <summary>
/// This class is used to implement <see cref="IServiceFactory{T}"/> for all
/// services automatically.
/// </summary>
[DebuggerDisplay("AutoServiceFactory (Type={typeof(T)}, Policy={Policy})")]
internal class AutoServiceFactory<T> : ServiceBase, IServiceFactory<T>
{
#region Private Fields
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly String _Policy;
#endregion
#region Construction & Destruction
/// <summary>
/// Initializes a new instance of the <see cref="AutoServiceFactory<T>"/> class.
/// </summary>
/// <param name="serviceContainer">The service container involved.</param>
/// <param name="policy">The policy to use when resolving the service.</param>
/// <exception cref="ArgumentNullException"><paramref name="serviceContainer"/> is <c>null</c>.</exception>
public AutoServiceFactory(IServiceContainer serviceContainer, String policy)
: base(serviceContainer)
{
_Policy = policy;
}
/// <summary>
/// Initializes a new instance of the <see cref="AutoServiceFactory<T>"/> class.
/// </summary>
/// <param name="serviceContainer">The service container involved.</param>
/// <exception cref="ArgumentNullException"><paramref name="serviceContainer"/> is <c>null</c>.</exception>
public AutoServiceFactory(IServiceContainer serviceContainer)
: this(serviceContainer, null)
{
// Do nothing here
}
#endregion
#region Public Properties
/// <summary>
/// Gets the policy that will be used when the service is resolved.
/// </summary>
public String Policy
{
get
{
return _Policy;
}
}
#endregion
#region IServiceFactory<T> Members
/// <summary>
/// Constructs a new service of the correct type and returns it.
/// </summary>
/// <returns>The created service.</returns>
public IService<T> Create()
{
return MyServiceContainer.Resolve<T>(_Policy);
}
#endregion
}
}
Básicamente, cuando construyo el contenedor de servicios de mi clase de generador de contenedor de servicio, todos los registros de servicio reciben automáticamente otro co-servicio, implementando IServiceFactory para ese servicio, a menos que el programador se haya registrado explícitamente en ese servicio para ese servicio. El servicio anterior se utiliza, con un parámetro que especifica la política (que puede ser nulo si no se utilizan las políticas).
Esto me permite hacer esto:
var builder = new ServiceContainerBuilder();
builder.Register<ISomeService>()
.From.ConcreteType<SomeService>();
using (var container = builder.Build())
{
using (var factory = container.Resolve<IServiceFactory<ISomeService>>())
{
using (var service = factory.Instance.Create())
{
service.Instance.DoSomethingAwesomeHere();
}
}
}
Por supuesto, un uso más típico sería con su objeto CD Burner. En el código anterior, resolvería el servicio en lugar de hacerlo por supuesto, pero es una ilustración de lo que sucede.
Entonces con su servicio de quemador de cd:
var builder = new ServiceContainerBuilder();
builder.Register<ICDBurner>()
.From.ConcreteType<CDBurner>();
builder.Register<ISomeService>()
.From.ConcreteType<SomeService>(); // constructor used in the top of answer
using (var container = builder.Build())
{
using (var service = container.Resolve<ISomeService>())
{
service.Instance.DoSomethingHere();
}
}
Dentro del servicio, ahora puede tener un servicio, un servicio de fábrica, que sepa cómo resolver su servicio de quemador de cd a pedido. Esto es útil por las siguientes razones:
- Es posible que desee resolver más de un servicio al mismo tiempo (grabar dos discos simultáneamente?)
- Es posible que no lo necesite, y puede ser costoso crearlo, por lo que solo lo resolverá si es necesario
- Es posible que tenga que resolver, eliminar, resolver, eliminar varias veces, en lugar de esperar / intentar limpiar una instancia de servicio existente
- También está marcando en su constructor qué servicios necesita y cuáles puede necesitar
Aquí hay dos al mismo tiempo:
using (var service1 = container.Resolve<ISomeService>())
using (var service2 = container.Resolve<ISomeService>())
{
service1.Instance.DoSomethingHere();
service2.Instance.DoSomethingHere();
}
Aquí hay dos después de cada uno, sin reutilizar el mismo servicio:
using (var service = container.Resolve<ISomeService>())
{
service.Instance.DoSomethingHere();
}
using (var service = container.Resolve<ISomeService>())
{
service.Instance.DoSomethingElseHere();
}
Bueno, si bien puedes hacer esto como se describe en otras respuestas, creo que hay algo más importante que responder con respecto a tu ejemplo, y es que probablemente estés violando el principio de SRP con clases que tienen muchas dependencias.
Lo que consideraría en su ejemplo es dividir la clase en un par de clases más coherentes con preocupaciones específicas y, por lo tanto, el número de sus dependencias caerá.
La ley de Nikola de SRP y DI
"Cualquier clase que tenga más de 3 dependencias debe ser cuestionada por la violación de SRP"
(Para evitar una respuesta larga, publiqué en detalle mis respuestas en la publicación del blog de IoC y SRP )