Como lidar com a "dependência circular" na injeção de dependência

15

O título diz "Dependência circular", mas não é o texto correto, porque para mim o design parece sólido.
No entanto, considere o cenário a seguir, onde as partes azuis são fornecidas pelo parceiro externo e a laranja é minha própria implementação. Suponha também que exista mais de um ConcreteMain, mas quero usar um específico. (Na realidade, cada classe tem mais algumas dependências, mas tentei simplificá-la aqui)

Cenário

Gostaria de instanciar tudo isso com a Depency Injection (Unity), mas obviamente obtenho um StackOverflowExceptioncódigo a seguir, porque o Runner tenta instanciar o ConcreteMain e o ConcreteMain precisa de um Runner.

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

Como posso evitar isso? Existe alguma maneira de estruturar isso para que eu possa usá-lo com DI? O cenário que estou fazendo agora é configurar tudo manualmente, mas isso coloca uma forte dependência ConcreteMainna classe que o instancia. É isso que estou tentando evitar (com registros do Unity na configuração).

Todo o código fonte abaixo (exemplo muito simplificado!);

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
fonte

Respostas:

10

O que você pode fazer é criar uma fábrica, MainFactory, que retorne uma instância do ConcreteMain como IMain.

Em seguida, você pode injetar essa fábrica no construtor Runner. Crie o Main com a fábrica e passe o próprio inn como parâmetro.

Quaisquer outras dependências no construtor ConcreteMain podem ser passadas para o MyMainFactory via IOC e enviadas manualmente ao construtor de concreto.

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

Use um contêiner IOC que suporte esse cenário. Eu sei que o AutoFac e possíveis outros fazem. Ao usar o AutoFac, a restrição é que uma das dependências deve ter PropertiesAutoWired = true e usar uma propriedade para a dependência.

Esben Skov Pedersen
fonte
4

Alguns contêineres do IOC (por exemplo, Spring ou Weld) podem resolver esse problema usando proxies gerados dinamicamente. Os proxies são injetados nas duas extremidades e o objeto real é instanciado apenas quando o proxy é usado pela primeira vez. Dessa forma, dependências circulares não são um problema, a menos que os dois objetos chamem métodos entre si em seus construtores (o que é fácil de evitar).

vrostu
fonte
4

Com o Unity 3, agora você pode injetar Lazy<T>. É semelhante a injetar um cache de fábrica / objeto.

Apenas certifique-se de que você não trabalhe em seu ctor que exija resolver a dependência do Lazy.

dss539
fonte