Cómo manejar la "dependencia circular" en la inyección de dependencia

15

El título dice "Dependencia circular", pero no es la redacción correcta, porque el diseño me parece sólido.
Sin embargo, considere el siguiente escenario, donde las partes azules provienen de un socio externo y el naranja es mi propia implementación. También suponga que hay más de uno ConcreteMain, pero quiero usar uno específico. (En realidad, cada clase tiene algunas dependencias más, pero traté de simplificarlo aquí)

Guión

Me gustaría instanciar todo esto con Depency Injection (Unity), pero obviamente obtengo un StackOverflowExceptioncódigo siguiente, porque Runner intenta crear una instancia de ConcreteMain, y ConcreteMain necesita un Runner.

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

¿Cómo puedo evitar esto? ¿Hay alguna forma de estructurar esto para que pueda usarlo con DI? El escenario que estoy haciendo ahora es configurar todo manualmente, pero eso pone una gran dependencia ConcreteMainen la clase que lo instancia. Esto es lo que estoy tratando de evitar (con los registros de Unity en la configuración).

Todo el código fuente a continuación (¡ejemplo muy 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
fuente

Respuestas:

10

Lo que puede hacer es crear una fábrica, MainFactory, que devuelva una instancia de ConcreteMain como IMain.

Entonces puedes inyectar esta Fábrica en tu constructor Runner. Crea el Main con la fábrica y pasa la posada como parámetro.

Cualquier otra dependencia del constructor ConcreteMain puede pasarse a MyMainFactory a través de IOC y enviarse al constructor concreto manualmente.

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

Use un contenedor IOC que admita este escenario. Sé que AutoFac y otros posibles sí. Cuando se usa AutoFac, la restricción es que una de las dependencias debe tener PropertiesAutoWired = true y usar una Propiedad para la dependencia.

Esben Skov Pedersen
fuente
4

Algunos contenedores de COI (por ejemplo, Spring o Weld) pueden resolver este problema utilizando proxies generados dinámicamente. Los proxies se inyectan en ambos extremos y el objeto real solo se instancia cuando el proxy se usa por primera vez. De esa manera, las dependencias circulares no son un problema a menos que los dos objetos invoquen métodos entre sí en sus constructores (lo cual es fácil de evitar).

vrostu
fuente
4

Con Unity 3, ahora puede inyectar Lazy<T>. Esto es similar a inyectar una caché Factory / object.

Solo asegúrese de no trabajar en su ctor que requiera resolver la dependencia de Lazy.

dss539
fuente