¿Cuál es la mejor manera de construir una fábrica con NInject?

27

Estoy bastante cómodo con la inyección de dependencia usando NInject en MVC3. Mientras trabajaba en una aplicación MVC3, desarrollé una fábrica de creación de controladores personalizada utilizando NInject, por lo que cualquier controlador que se cree tendrá dependencias inyectadas a través de esta fábrica de controladores.

Ahora estoy comenzando a desarrollar una aplicación de Windows, quiero usar la inyección de dependencia de toda la aplicación. es decir, cada objeto debe crearse a través de NInject, para facilitar las pruebas unitarias. Guíeme para asegurarme de que cada objeto creado debe ser a través de NInject Factory únicamente.

Por ejemplo, si en cualquier formulario de Windows en el Button_Clickevento escribo:

TestClass testClass = new TestClass()

y TestClasstiene alguna dependencia, digamos, ITestentonces debe resolverse automáticamente. Sé que puedo usar:

Ikernel kernel = new StandardKenel()
//AddBinding()
TestClass testClass = kenel.get<TestClass>();

Pero me resulta tedioso hacer esto cada vez que quiero crear un objeto. También obliga al desarrollador a crear el objeto de una manera particular. ¿Se puede mejorar?

¿Puedo tener un repositorio central para la creación de objetos y luego cada creación de objetos usará automáticamente ese repositorio?

Pravin Patil
fuente
1
Hola Pravin Patil: gran pregunta. Hice un pequeño cambio en su título para aclarar lo que está preguntando; siéntase libre de modificar si perdí la marca.
@ MarkTrapp: Gracias por el título adecuado. Me perdí ese lema ...
Pravin Patil
Como nota al margen menor, el proyecto se deletrea "Ninject", no "NInject". Si bien puede ser que haya sido En-Inject, hoy en día juegan bastante en el tema nin-ja. :) Cf. ninject.org
Cornelius

Respuestas:

12

Para las aplicaciones cliente, a menudo es mejor adaptar un patrón como MVP (o MVVM) y utilizar el enlace de datos del formulario al ViewModel o Presentador subyacente.

Para los ViewModels, puede inyectar las dependencias requeridas utilizando la inyección de constructor estándar.

En la raíz de composición de su aplicación, puede conectar todo el gráfico de objetos para su aplicación. No necesita usar un contenedor DI (como Ninject) para esto, pero puede hacerlo.

Mark Seemann
fuente
7

Las aplicaciones de Windows Forms generalmente tienen un punto de entrada que se ve así:

    // Program.cs
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new MainForm());
    }

Si hace de este punto en el código la raíz de su composición , puede reducir enormemente el número de lugares donde tiene código que invoca explícitamente a Ninject como si fuera un Localizador de servicios.

    // Program.cs
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        var kernel = InitializeNinjectKernel();
        Application.Run(kernel.Get<MainForm>());
    }

Desde este punto, inyecta todas sus dependencias mediante la inyección del constructor.

public MainForm(TestClass testClass) {
    _testClass = testClass;
}

Si su "dependencia" es algo que necesita poder producir varias veces, entonces lo que realmente necesita es una fábrica:

public MainForm(IFactory<TestClass> testClassFactory) {
    _testClassFactory = testClassFactory;
}

...
var testClass = _testClassFactory.Get();

Puede implementar la interfaz IFactory de esta manera para evitar tener que crear toneladas de implementaciones únicas:

public class InjectionFactory<T> : IFactory<T>, IObjectFactory<T>, IDependencyInjector<T>
{
    private readonly IKernel _kernel;
    private readonly IParameter[] _contextParameters;

    public InjectionFactory(IContext injectionContext)
    {
        _contextParameters = injectionContext.Parameters
            .Where(p => p.ShouldInherit).ToArray();
        _kernel = injectionContext.Kernel;
    }

    public T Get()
    {
        try
        {
            return _kernel.Get<T>(_contextParameters.ToArray());
        }
        catch (Exception e)
        {
            throw new Exception(
                string.Format("An error occurred while attempting to instantiate an object of type <{0}>",
                typeof(T)));
        }
    }

...
Bind(typeof (IFactory<>)).To(typeof (InjectionFactory<>));
Bind(typeof (IContext)).ToMethod(c => c.Request.ParentContext);
StriplingWarrior
fuente
Por favor, ¿tiene la implementación completa para esta fábrica?
Tebo
@ColourBlend: No, pero si se deshace de las otras interfaces que tenía InjectionFactory<T>implementadas, entonces el código proporcionado debería funcionar bien. ¿Hay algo en particular con lo que tienes problemas?
StriplingWarrior
Ya lo implementé, solo quiero saber si había otras cosas interesantes dentro de la clase.
Tebo
@Tebo: Acabo de implementar un par de otras interfaces relacionadas con DI, como una fábrica a la que podría pasarle una Type, pero que garantizaría que los objetos que hidratara para esa Typeimplementación o extendieran un tipo genérico dado. Nada muy especial
StriplingWarrior
4

Siempre escribo un contenedor de Adaptador para cualquier Contenedor IoC, que se ve así:

public static class Ioc
{
    public static IIocContainer Container { get; set; }
}

public interface IIocContainer 
{
    object Get(Type type);
    T Get<T>();
    T Get<T>(string name, string value);
    void Inject(object item);
    T TryGet<T>();
}

Para Ninject, específicamente, la clase de adaptador concreta se ve así:

public class NinjectIocContainer : IIocContainer
{
    public readonly IKernel Kernel;
    public NinjectIocContainer(params INinjectModule[] modules) 
    {
        Kernel = new StandardKernel(modules);
        new AutoWirePropertyHeuristic(Kernel);
    }

    private NinjectIocContainer()
    {
        Kernel = new StandardKernel();
        Kernel.Load(AppDomain.CurrentDomain.GetAssemblies());

        new AutoWirePropertyHeuristic(Kernel);
    }

    public object Get(Type type)
    {
        try
        {
            return Kernel.Get(type);
        }
        catch (ActivationException exception)
        {
            throw new TypeNotResolvedException(exception);
        }              
    }

    public T TryGet<T>()
    {
        return Kernel.TryGet<T>();
    }

    public T Get<T>()
    {
        try
        {
            return Kernel.Get<T>();
        }
        catch (ActivationException exception)
        {
            throw new TypeNotResolvedException(exception);
        }           
    }

    public T Get<T>(string name, string value)
    {
        var result = Kernel.TryGet<T>(metadata => metadata.Has(name) &&
                     (string.Equals(metadata.Get<string>(name), value,
                                    StringComparison.InvariantCultureIgnoreCase)));

        if (Equals(result, default(T))) throw new TypeNotResolvedException(null);
            return result;
    }

    public void Inject(object item)
    {
        Kernel.Inject(item);
    }
}

La razón principal para hacer esto es abstraer el marco de IoC, de modo que pueda reemplazarlo en cualquier momento, dado que la diferencia entre los marcos generalmente está en la configuración y no en el uso.

Pero, como beneficio adicional, las cosas también se vuelven mucho más fáciles para usar el marco de IoC dentro de otros marcos que no lo admiten de manera inadvertida. Para WinForms, por ejemplo, son dos pasos:

En su método Main, simplemente cree una instancia de un contenedor antes de hacer cualquier otra cosa.

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        try
        {
            Ioc.Container = new NinjectIocContainer( /* include modules here */ );
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MyStartupForm());
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.ToString());
        }
    }
}

Y luego tenga una Forma base, de la cual derivan otras formas, que llama Inyectar sobre sí misma.

public IocForm : Form
{
    public IocForm() : base()
    {
        Ioc.Container.Inject(this);
    }
}

Esto le dice a la heurística de cableado automático que intente inyectar recursivamente todas las propiedades en la forma que se ajuste a las reglas establecidas en sus módulos.

pdr
fuente
Muy buena solución ..... Lo intentaré.
Pravin Patil
10
Es un Localizador de servicios, que es una idea espectacularmente mala: blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
Mark Seemann
2
@ MarkSeemann: un localizador de servicios es una mala idea, si accede a él desde cualquier lugar, en lugar de dejar que conecte sus objetos de nivel superior tan lejos como sea posible. Lea el propio comentario de Mark, un poco más abajo en la página: "En tales casos, realmente no tiene más remedio que mover la raíz de composición a cada objeto (por ejemplo, página) y dejar que su contenedor DI conecte sus dependencias desde allí. Esto puede parecer el antipatrón del Localizador de servicios, pero no es porque aún mantenga el uso del contenedor en un mínimo absoluto ". (Editar: ¡Espera, ERES Mark! Entonces, ¿cuál es la diferencia?)
pdr
1
La diferencia es que aún puede proteger el resto de su base de código del Composer, en lugar de poner a disposición de cualquier clase un Localizador de servicios Singleton.
Mark Seemann
2
@pdr: En mi experiencia, si intentas inyectar servicios en cosas como clases de atributos, no estás separando las preocupaciones correctamente. Hay casos en los que el marco que está utilizando hace que sea prácticamente imposible usar una inyección de dependencia adecuada, y a veces nos vemos obligados a usar un localizador de servicios, pero definitivamente trataría de llevar la verdadera DI lo más lejos posible antes de volver a esto modelo.
StriplingWarrior
1

El buen uso de la inyección de dependencia generalmente se basa en separar el código que crea los objetos y la lógica comercial real. En otras palabras, no quisiera que mi equipo use newy cree con frecuencia una instancia de una clase de esa manera. Una vez hecho esto, no hay forma de cambiar fácilmente ese tipo creado por otro, ya que ya ha especificado el tipo concreto.

Así que hay dos formas de solucionar esto:

  1. Inyecte las instancias que necesitará una clase. En su ejemplo, inyecte un TestClassen su formulario de Windows para que ya tenga una instancia cuando la necesite. Cuando Ninject crea una instancia de su formulario, crea automáticamente la dependencia.
  2. En los casos en los que realmente no desea crear una instancia hasta que la necesite, puede inyectar una fábrica en la lógica empresarial. Por ejemplo, puede inyectar un IKernelen su formulario de Windows y luego usarlo para crear una instancia TestClass. Dependiendo de su estilo, también hay otras formas de lograr esto (inyectando una clase de fábrica, delegado de fábrica, etc.).

Hacer esto hace que sea fácil intercambiar tanto el tipo concreto de TestClass, como modificar la construcción real de la clase de prueba, sin modificar realmente el código que usa la clase de prueba.

Chris Pitman
fuente
1

No he usado Ninject, pero la forma estándar de crear cosas cuando está usando un IoC es hacerlo a través de Func<T>donde Tes el tipo de objeto que desea crear. Entonces, si el objeto T1necesita crear objetos de tipo, T2entonces el constructor de T1debe tener un parámetro de tipo Func<T1>que luego se almacena como un campo / propiedad de T2. Ahora bien, cuando se desea crear objetos del tipo T2en T1que la invocación Func.

Esto lo desacopla totalmente de su marco de IoC y es la forma correcta de codificar en una mentalidad de IoC.

La desventaja de hacer esto es que puede ser molesto cuando necesita cablear manualmente Funco, por ejemplo, cuando su creador requiere algún parámetro, por lo que IoC no puede conectarlo automáticamente Func.

http://code.google.com/p/autofac/wiki/RelationshipTypes


fuente