web-dev-qa-db-fra.com

Comment utiliser l'injection de dépendance et éviter le couplage temporel?

Supposons que j'ai le Service qui reçoit les dépendances via le constructeur mais doit également être initialisé avec des données personnalisées (contexte) avant de pouvoir être utilisé:

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;
}

Maintenant - les données de contexte ne sont pas connues à l'avance, donc je ne peux pas l'enregistrer en tant que dépendance et utiliser DI pour l'injecter dans le service

Voici à quoi ressemble un exemple de client:

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();
    }
}

Comme vous pouvez le voir - il y a un couplage temporel et initialise les odeurs de code de méthode impliquées, car je dois d'abord appeler service.Initialize pour pouvoir appeler service.DoSomething et service.DoOtherThing ensuite.

Quelles sont les autres approches dans lesquelles je peux éliminer ces problèmes?

Clarification supplémentaire du comportement:

Chaque instance du client doit avoir sa propre instance du service initialisée avec les données de contexte spécifiques du client. Ainsi, ces données de contexte ne sont pas statiques ou connues à l'avance, elles ne peuvent donc pas être injectées par DI dans le constructeur.

11
Dusan

Il existe plusieurs façons de résoudre le problème d'initialisation:

  • Comme répondu dans https://softwareengineering.stackexchange.com/a/334994/301401 , les méthodes init () sont une odeur de code. L'initialisation d'un objet est la responsabilité du constructeur - c'est pourquoi nous avons des constructeurs après tout.
  • Ajouter Le service donné doit être initialisé au commentaire doc du constructeur Client et laisser le constructeur lancer si le service n'est pas initialisé. Cela déplace la responsabilité vers celui qui vous donne l'objet IService.

Cependant, dans votre exemple, le Client est le seul à connaître les valeurs transmises à Initialize(). Si vous voulez que cela reste ainsi, je suggère ce qui suit:

  • Ajoutez un IServiceFactory et passez-le au constructeur Client. Ensuite, vous pouvez appeler serviceFactory.createService(new Context(...)) qui vous donne un IService initialisé qui peut être utilisé par votre client.

Les usines peuvent être très simples et vous permettent également d'éviter les méthodes init () et d'utiliser des constructeurs à la place:

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

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

Dans le client, OnStartup() est également une méthode d'initialisation (elle utilise simplement un nom différent). Donc si possible (si vous connaissez les données Context), la fabrique doit être appelée directement dans le constructeur Client. Si ce n'est pas possible, vous devez stocker le IServiceFactory et l'appeler dans OnStartup().

Lorsque Service a des dépendances non fournies par Client elles seraient fournies par DI via 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);
    }
}
18
pschill

Il me semble que vous avez deux options ici

  1. Déplacez le code d'initialisation dans le contexte et injectez un contexte initialisé

par exemple.

public InitialisedContext Initialise()
  1. Avoir le premier appel à Exécuter l'appel Initialiser si ce n'est pas déjà fait

par exemple.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Lancez simplement des exceptions si Context n'est pas initialisé lorsque vous appelez Execute. Comme SqlConnection.

Injecter une fabrique est très bien si vous voulez juste éviter de passer le contexte en paramètre. Supposons que cette implémentation particulière ait besoin d'un contexte et que vous ne souhaitiez pas l'ajouter à l'interface

Mais vous avez essentiellement le même problème, que se passe-t-il si l'usine n'a pas encore de contexte initialisé.

1
Ewan

La méthode Initialize doit être supprimée de l'interface IService, car il s'agit d'un détail d'implémentation. Au lieu de cela, définissez une autre classe qui prend l'instance concrète de Service et appelle la méthode initialize dessus. Ensuite, cette nouvelle classe implémente l'interface IService:

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

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Cela permet au code client d'ignorer la procédure d'initialisation, sauf lorsque la classe ContextDependentService est initialisée. Vous limitez au moins les parties de votre application qui ont besoin de connaître cette procédure d'initialisation bancale.

1
Greg Burghardt

Vous ne devez pas dépendre votre interface d'un contexte db et initialiser la méthode. Vous pouvez le faire dans un constructeur de classe concret.

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()
    {
        // ...
    }
}

Et, une réponse à votre question principale serait Injection de propriété.

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 cette façon, vous pouvez appeler toutes les dépendances par Property Injection. Mais cela pourrait être énorme. Si c'est le cas, vous pouvez utiliser l'injection de constructeur pour eux, mais vous pouvez définir votre contexte par propriété en vérifiant s'il est nul.

0
Engineert

Misko Hevery a un blog très utile sur l'affaire que vous avez rencontrée. Vous avez tous les deux besoin de newable et injectable pour votre Service classe et cet article de blog peut vous aider.

0
Bold P.