Wie man mit "zirkulären Abhängigkeiten" bei der Abhängigkeitsinjektion umgeht

15

Der Titel sagt "Circular Dependency", aber es ist nicht der richtige Wortlaut, weil mir das Design solide erscheint.
Stellen Sie sich jedoch das folgende Szenario vor, in dem die blauen Teile von einem externen Partner stammen und Orange meine eigene Implementierung ist. Nehmen wir auch an, es gibt mehr als einen ConcreteMain, aber ich möchte einen bestimmten verwenden. (In Wirklichkeit hat jede Klasse einige Abhängigkeiten mehr, aber ich habe versucht, es hier zu vereinfachen)

Szenario

Ich möchte all dies mit Depency Injection (Unity) StackOverflowExceptioninstanziieren , aber ich erhalte offensichtlich einen Hinweis auf den folgenden Code, da Runner versucht, ConcreteMain zu instanziieren, und ConcreteMain einen Runner benötigt.

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

Wie kann ich das vermeiden? Gibt es eine Möglichkeit, dies so zu strukturieren, dass ich es mit DI verwenden kann? Das Szenario, das ich jetzt mache, besteht darin, alles manuell einzurichten, aber das setzt eine harte Abhängigkeit von ConcreteMainder Klasse voraus, die es instanziiert. Dies ist, was ich zu vermeiden versuche (mit Unity-Registrierungen in der Konfiguration).

Der gesamte Quellcode unten (sehr vereinfachtes Beispiel!);

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
quelle

Antworten:

10

Sie können eine Factory, MainFactory, erstellen, die eine Instanz von ConcreteMain als IMain zurückgibt.

Dann können Sie diese Factory in Ihren Runner-Konstruktor einfügen. Erstellen Sie das Main mit der Factory und übergeben Sie inn selbst als Parameter.

Alle anderen Abhängigkeiten vom ConcreteMain-Konstruktor können über IOC an MyMainFactory übergeben und manuell an den Concrete-Konstruktor übertragen werden.

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
quelle
4

Verwenden Sie einen IOC-Container, der dieses Szenario unterstützt. Ich weiß, dass AutoFac und mögliche andere es tun. Bei Verwendung von AutoFac muss eine der Abhängigkeiten PropertiesAutoWired = true haben und eine Eigenschaft für die Abhängigkeit verwenden.

Esben Skov Pedersen
quelle
4

Einige IOC-Container (z. B. Spring oder Weld) können dieses Problem mithilfe dynamisch generierter Proxys lösen. Proxies werden an beiden Enden injiziert und das reale Objekt wird nur instanziiert, wenn der Proxy zum ersten Mal verwendet wird. Auf diese Weise sind zirkuläre Abhängigkeiten kein Problem, es sei denn, die beiden Objekte rufen in ihren Konstruktoren Methoden aufeinander auf (was leicht zu vermeiden ist).

Vrostu
quelle
4

Mit Unity 3 können Sie jetzt injizieren Lazy<T>. Dies ähnelt dem Injizieren eines Factory- / Objekt-Cache.

Stellen Sie nur sicher, dass Sie nicht in Ihrem Ctor arbeiten, für den die Lazy-Abhängigkeit aufgelöst werden muss.

dss539
quelle