¿Cómo usar la inyección de dependencia y evitar el acoplamiento temporal?


11

Supongamos que tengo el Serviceque recibe dependencias a través del constructor pero también necesita inicializarse con datos personalizados (contexto) antes de que pueda usarse:

public interface IService
{
    void Initialize(Context context);
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3)
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
    }

    public void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

public class Context
{
    public int Value1;
    public string Value2;
    public string Value3;
}

Ahora, los datos de contexto no se conocen de antemano, por lo que no puedo registrarlos como una dependencia y usar DI para inyectarlos en el servicio

Así es como se ve el cliente de ejemplo:

public class Client
{
    private readonly IService service;

    public Client(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public void OnStartup()
    {
        service.Initialize(new Context
        {
            Value1 = 123,
            Value2 = "my data",
            Value3 = "abcd"
        });
    }

    public void Execute()
    {
        service.DoSomething();
        service.DoOtherThing();
    }
}

Como puede ver, hay un acoplamiento temporal e inicialización de olores de código de método involucrados, porque primero necesito llamar service.Initializepara poder llamar service.DoSomethingy service.DoOtherThingluego.

¿Cuáles son los otros enfoques en los que puedo eliminar estos problemas?

Aclaración adicional del comportamiento:

Cada instancia del cliente debe tener su propia instancia del servicio inicializada con datos de contexto específicos del cliente. Por lo tanto, los datos de contexto no son estáticos ni se conocen de antemano, por lo que DI no puede inyectarlos en el constructor.

Respuestas:


18

Hay varias formas de lidiar con el problema de inicialización:

  • Como se respondió en /software//a/334994/301401 , los métodos init () son un olor a código. Inicializar un objeto es responsabilidad del constructor, por eso tenemos constructores después de todo.
  • Agregar El servicio proporcionado se debe inicializar al comentario de documento del Clientconstructor y dejar que el constructor lance si el servicio no se inicializa. Esto traslada la responsabilidad a quien te da el IServiceobjeto.

Sin embargo, en su ejemplo, Clientes el único que conoce los valores a los que se pasa Initialize(). Si quieres mantenerlo así, te sugiero lo siguiente:

  • Agregue un IServiceFactoryy páselo al Clientconstructor. Luego puede llamar, lo serviceFactory.createService(new Context(...))que le proporciona una inicialización IServiceque puede ser utilizada por su cliente.

Las fábricas pueden ser muy simples y también le permiten evitar los métodos init () y usar constructores en su lugar:

public interface IServiceFactory
{
    IService createService(Context context);
}

public class ServiceFactory : IServiceFactory
{
    public Service createService(Context context)
    {
        return new Service(context);
    }
}

En el cliente, OnStartup()también es un método de inicialización (solo usa un nombre diferente). Entonces, si es posible (si conoce los Contextdatos), se debe llamar directamente a la fábrica en el Clientconstructor. Si eso no es posible, debe almacenarlo IServiceFactoryy llamarlo OnStartup().

Cuando Servicetiene dependencias no Clientproporcionadas por DI, las proporcionaría a través de ServiceFactory:

public interface IServiceFactory
{
    IService createService(Context context);
}    

public class ServiceFactory : IServiceFactory
{        
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public ServiceFactory(object dependency1, object dependency2, object dependency3)
    {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
        this.dependency3 = dependency3;
    }

    public Service createService(Context context)
    {
        return new Service(context, dependency1, dependency2, dependency3);
    }
}

1
Gracias, tal como pensé, en el último punto ... Y en ServiceFactory, ¿usaría el constructor DI en la propia fábrica para las dependencias necesarias para el constructor de servicios o el localizador de servicios sería más adecuado?
Dusan

1
@Dusan no utiliza el Localizador de servicios. Si Servicetiene dependencias distintas de la Contextque no se proporcionarían por el Client, se pueden proporcionar a través de DI al ServiceFactoryque se pasará Servicecuando createServicese llame.
Mr.Mindor

@Dusan Si necesita suministrar diferentes dependencias a diferentes Servicios (es decir: este necesita dependencia1_1 pero el siguiente necesita dependencia1_2), pero si este patrón funciona para usted, entonces puede usar un patrón similar a menudo llamado patrón de Constructor. Un generador le permite configurar un objeto por partes a lo largo del tiempo si es necesario. Entonces puedes hacer esto ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);y quedarte con tu servicio parcialmente configurado, luego más tardeService s = partial.context(context).build()
Aaron

1

El Initializemétodo debe eliminarse de la IServiceinterfaz, ya que este es un detalle de implementación. En su lugar, defina otra clase que tome la instancia concreta de Servicio y llame al método de inicialización. Entonces esta nueva clase implementa la interfaz IService:

public class ContextDependentService : IService
{
    public ContextDependentService(Context context, Service service)
    {
        this.service = service;

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Esto mantiene el código del cliente ignorante del procedimiento de inicialización, excepto donde ContextDependentServicese inicializa la clase. Al menos limita las partes de su aplicación que necesitan saber sobre este procedimiento de inicialización inestable.


1

Me parece que tienes dos opciones aquí

  1. Mueva el código de inicialización al contexto e inyecte un contexto inicializado

p.ej.

public InitialisedContext Initialise()
  1. Tenga la primera llamada para Ejecutar Inicialización de llamada si no está listo

p.ej.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Simplemente arroje excepciones si el contexto no se inicializa cuando llama a Ejecutar. Como SqlConnection.

Inyectar una fábrica está bien si solo desea evitar pasar el contexto como parámetro. Digamos que solo esta implementación particular necesita un contexto y no desea agregarlo a la interfaz

Pero esencialmente tiene el mismo problema, ¿qué pasa si la fábrica aún no tiene un contexto inicializado?


0

No debe depender de su interfaz con ningún contexto db ni método de inicialización. Puedes hacerlo en constructor de clase concreto.

public interface IService
{
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;
    private readonly object context;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3,
        object context )
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));

        // context is concrete class details not interfaces.
        this.context = context;

        // call init here constructor.
        this.Initialize(context);
    }

    protected void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

Y, una respuesta a su pregunta principal sería Inyección de propiedades .

public class Service
    {
        public Service(Context context)
        {
            this.context = context;
        }

        private Dependency1 _dependency1;
        public Dependency1 Dependency1
        {
            get
            {
                if (_dependency1 == null)
                    _dependency1 = Container.Resolve<Dependency1>();

                return _dependency1;
            }
        }

        //...
    }

De esta manera, puede llamar a todas las dependencias por inyección de propiedad . Pero podría ser un gran número. Si es así, puede usar la inyección de constructor para ellos, pero puede establecer su contexto por propiedad comprobando si es nulo.


Bien, genial, pero ... cada instancia del cliente necesita tener su propia instancia del servicio inicializada con diferentes datos de contexto. Los datos de contexto no son estáticos ni se conocen de antemano, por lo que DI no puede inyectarlos en el constructor. Entonces, ¿cómo obtengo / creo una instancia del servicio junto con otras dependencias en mis clientes?
Dusan

hmm, ¿no se ejecutará ese constructor estático antes de establecer el contexto? e inicializar en el constructor riesgos excepciones
Ewan

Me estoy inclinando hacia la fábrica de inyección que puede crear e inicializar el servicio con los datos de contexto dados (en lugar de inyectar el servicio en sí), pero no estoy seguro de si hay mejores soluciones.
Dusan

@Ewan Tienes razón. Intentaré encontrar una solución para ello. Pero antes de eso, lo eliminaré por ahora.
Engineert

0

Misko Hevery tiene una publicación de blog muy útil sobre el caso que has enfrentado. Ambos necesitan nuevos e inyectables para su Serviceclase y esta publicación de blog puede ayudarlos.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.