Как обрабатывать «круговую зависимость» при внедрении зависимости

15

Название гласит «Круговая зависимость», но это не правильная формулировка, потому что дизайн мне кажется солидным.
Однако рассмотрим следующий сценарий, где синие части даны от внешнего партнера, а оранжевый - моя собственная реализация. Также предположим, что есть более одного ConcreteMain, но я хочу использовать конкретный. (На самом деле у каждого класса есть еще несколько зависимостей, но я попытался упростить это здесь)

сценарий

Я хотел бы реализовать все это с помощью Depency Injection (Unity), но я, очевидно, получаю StackOverflowExceptionследующий код, потому что Runner пытается создать экземпляр ConcreteMain, а ConcreteMain нужен Runner.

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

Как я могу избежать этого? Есть ли способ структурировать это так, чтобы я мог использовать его с DI? Сценарий, который я сейчас делаю, настраивает все вручную, но это создает жесткую зависимость от ConcreteMainкласса, который его создает. Это то, чего я пытаюсь избежать (с регистрацией Unity в конфигурации).

Весь исходный код ниже (очень упрощенный пример!);

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
источник

Ответы:

10

Что вы можете сделать, это создать фабрику MainFactory, которая возвращает экземпляр ConcreteMain как IMain.

Затем вы можете добавить эту Фабрику в свой конструктор Runner. Создайте Основное с фабрикой и передайте саму гостиницу в качестве параметра.

Любые другие зависимости от конструктора ConcreteMain могут быть переданы в MyMainFactory через IOC и переданы в конкретный конструктор вручную.

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
источник
4

Используйте контейнер IOC, который поддерживает этот сценарий. Я знаю, что AutoFac и, возможно, другие делают. При использовании AutoFac ограничение заключается в том, что одна из зависимостей должна иметь PropertiesAutoWired = true и использовать свойство для зависимости.

Эсбен Сков Педерсен
источник
4

Некоторые контейнеры IOC (например, Spring или Weld) могут решить эту проблему с помощью динамически генерируемых прокси. Прокси вводятся с обоих концов, и реальный объект создается только при первом использовании прокси. Таким образом, циклические зависимости не являются проблемой, если два объекта не вызывают методы друг в друге в своих конструкторах (чего легко избежать).

vrostu
источник
4

С Unity 3 теперь можно вводить Lazy<T>. Это похоже на внедрение кеша Factory / Object.

Просто убедитесь, что вы не выполняете работу в своем ctor, которая требует разрешения зависимости Lazy.

dss539
источник