Comment gérer la «dépendance circulaire» dans l'injection de dépendances

15

Le titre dit "Circular Dependency", mais ce n'est pas le bon libellé, car pour moi le design semble solide.
Cependant, considérez le scénario suivant, où les parties bleues sont fournies par un partenaire externe, et l'orange est ma propre implémentation. Supposons également qu'il y en ait plus d'un ConcreteMain, mais je veux en utiliser un spécifique. (En réalité, chaque classe a plus de dépendances, mais j'ai essayé de la simplifier ici)

Scénario

Je voudrais instancier tout cela avec Depency Injection (Unity), mais je reçois évidemment un StackOverflowExceptionsur le code suivant, car Runner essaie d'instancier ConcreteMain, et ConcreteMain a besoin d'un Runner.

IUnityContainer ioc = new UnityContainer();
ioc.RegisterType<IMain, ConcreteMain>()
   .RegisterType<IMainCallback, Runner>();
var runner = ioc.Resolve<Runner>();

Comment puis-je éviter cela? Existe-t-il un moyen de structurer cela afin que je puisse l'utiliser avec DI? Le scénario que je fais maintenant est de tout configurer manuellement, mais cela met une forte dépendance surConcreteMain dans la classe qui l'instancie. C'est ce que j'essaie d'éviter (avec les enregistrements Unity dans la configuration).

Tout le code source ci-dessous (exemple très simplifié!);

public class Program
{
    public static void Main(string[] args)
    {
        IUnityContainer ioc = new UnityContainer();
        ioc.RegisterType<IMain, ConcreteMain>()
           .RegisterType<IMainCallback, Runner>();
        var runner = ioc.Resolve<Runner>();

        Console.WriteLine("invoking runner...");
        runner.DoSomethingAwesome();

        Console.ReadLine();
    }
}

public class Runner : IMainCallback
{
    private readonly IMain mainServer;

    public Runner(IMain mainServer)
    {
        this.mainServer = mainServer;
    }

    public void DoSomethingAwesome()
    {
        Console.WriteLine("trying to do something awesome");
        mainServer.DoSomething();
    }

    public void SomethingIsDone(object something)
    {
        Console.WriteLine("hey look, something is finally done.");
    }
}

public interface IMain
{
    void DoSomething();
}

public interface IMainCallback
{
    void SomethingIsDone(object something);
}

public abstract class AbstractMain : IMain
{
    protected readonly IMainCallback callback;

    protected AbstractMain(IMainCallback callback)
    {
        this.callback = callback;
    }

    public abstract void DoSomething();
}

public class ConcreteMain : AbstractMain
{
    public ConcreteMain(IMainCallback callback) : base(callback){}

    public override void DoSomething()
    {
        Console.WriteLine("starting to do something...");
        var task = Task.Factory.StartNew(() =>{ Thread.Sleep(5000);/*very long running task*/ });
        task.ContinueWith(t => callback.SomethingIsDone(true));
    }
}
RoelF
la source

Réponses:

10

Ce que vous pouvez faire est de créer une fabrique, MainFactory qui renvoie une instance de ConcreteMain en tant qu'IMain.

Ensuite, vous pouvez injecter cette usine dans votre constructeur Runner. Créez le Main avec l'usine et passez l'auberge en tant que paramètre.

Toutes les autres dépendances sur le constructeur ConcreteMain peuvent être transmises à MyMainFactory via IOC et transmises manuellement au constructeur de béton.

public class MyMainFactory
{
    MyOtherDependency _dependency;

    public MyMainFactory(MyOtherDependency dependency)
    {
        _dependency = dependency;
    }

    public IMain Create(Runner runner)
    {
        return new ConcreteMain(runner, _dependency);
    }
}

public class Runner
{
    IMain _myMain;
    public Runner(MyMainFactory factory)
    {
        _myMain = factory.Create(this)
    }
}
hkon
la source
4

Utilisez un conteneur IOC qui prend en charge ce scénario. Je sais que AutoFac et d'autres possibles le font. Lors de l'utilisation d'AutoFac, la restriction est que l'une des dépendances doit avoir PropertiesAutoWired = true et utiliser une propriété pour la dépendance.

Esben Skov Pedersen
la source
4

Certains conteneurs IOC (par exemple Spring ou Weld) peuvent résoudre ce problème en utilisant des proxys générés dynamiquement. Les proxys sont injectés aux deux extrémités et l'objet réel n'est instancié que lors de la première utilisation du proxy. De cette façon, les dépendances circulaires ne sont pas un problème à moins que les deux objets ne s'appellent mutuellement des méthodes dans leurs constructeurs (ce qui est facile à éviter).

vrostu
la source
4

Avec Unity 3, vous pouvez désormais injecter Lazy<T>. Cela revient à injecter un cache Factory / objet.

Assurez-vous simplement que vous ne travaillez pas dans votre ctor qui nécessite de résoudre la dépendance Lazy.

dss539
la source