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

11

Supposons que j'ai le Servicequi 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.Initializepour pouvoir appeler service.DoSomethinget service.DoOtherThingensuite.

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.

Dusan
la source

Réponses:

18

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

  • Comme indiqué dans /software//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 Clientconstructeur et laisser le constructeur lancer si le service n'est pas initialisé. Cela transfère la responsabilité à celui qui vous donne l' IServiceobjet.

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

  • Ajoutez un IServiceFactoryet passez-le au Clientconstructeur. Ensuite, vous pouvez appeler serviceFactory.createService(new Context(...))ce qui vous donne un initialisé IServicequi 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 Contextdonnées), l'usine doit être appelée directement dans le Clientconstructeur. Si ce n'est pas possible, vous devez stocker le IServiceFactoryet l'appeler OnStartup().

Lorsque les Servicedépendances ne sont pas fournies par Clientelles, elles seront 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);
    }
}
pschill
la source
1
Merci, comme je le pensais, au dernier point ... Et dans ServiceFactory, utiliseriez-vous le constructeur DI dans l'usine elle-même pour les dépendances nécessaires pour le constructeur de service ou le localisateur de service serait-il plus approprié?
Dusan
1
@Dusan n'utilise pas Service Locator. Si Servicea des dépendances autres que le Context, qui ne seraient pas fournies par le Client, elles peuvent être fournies via DI au ServiceFactorypour être transmises au Servicequand createServiceest appelé.
Mr.Mindor
@Dusan Si vous devez fournir différentes dépendances à différents services (c'est-à-dire: celui-ci a besoin de dependency1_1 mais le suivant a besoin de dependency1_2), mais si ce modèle fonctionne autrement pour vous, alors vous pouvez utiliser un modèle similaire souvent appelé modèle Builder. Un générateur vous permet de configurer un objet au fur et à mesure au besoin. Ensuite, vous pouvez le faire ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);et vous retrouver avec votre service partiellement configuré, puis le faire plus tardService s = partial.context(context).build()
Aaron
1

La Initializeméthode doit être supprimée de l' IServiceinterface, car il s'agit d'un détail d'implémentation. À la place, 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 ContextDependentServiceclasse est initialisée. Vous limitez au moins les parties de votre application qui ont besoin de connaître cette procédure d'initialisation bancale.

Greg Burghardt
la source
1

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, et si l'usine n'a pas encore de contexte initialisé.

Ewan
la source
0

Vous ne devez pas dépendre votre interface de n'importe quel 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 l' 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 injection de propriété . 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.

Engineert
la source
D'accord, parfait, mais ... chaque instance du client doit avoir sa propre instance du service initialisée avec des données de contexte différentes. 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. Ensuite, comment puis-je obtenir / créer une instance du service avec d'autres dépendances dans mes clients?
Dusan
hmm ne fonctionnera pas ce constructeur statique avant de définir le contexte? et initialiser dans le constructeur risque des exceptions
Ewan
Je penche vers l'injection d'usine qui peut créer et initialiser le service avec les données de contexte données (plutôt que d'injecter le service lui-même), mais je ne suis pas sûr qu'il existe de meilleures solutions.
Dusan
@Ewan Vous avez raison. Je vais essayer de trouver une solution. Mais avant cela, je vais le supprimer pour l'instant.
Engineert
0

Misko Hevery a un article de blog très utile sur l'affaire que vous avez rencontrée. Vous avez tous les deux besoin de produits nouveaux et injectables pour votre Serviceclasse et ce billet de blog peut vous aider.

Bold P.
la source