Qual é a melhor maneira de construir uma fábrica usando o NInject?

27

Estou bastante confortável com a injeção de dependência usando o NInject no MVC3. Enquanto trabalhava em um aplicativo MVC3, desenvolvi uma Controller Creation Factory personalizada usando o NInject, para que qualquer controlador criado tenha dependências injetadas nele através desta Controller Factory.

Agora estou começando a desenvolver um aplicativo do Windows, quero usar a Injeção de Dependência em todo o aplicativo. Cada objeto deve ser criado através do NInject, para facilitar o Teste de Unidade. Por favor, me guie para garantir que todos os objetos criados sejam apenas do NInject Factory.

Por exemplo, se em qualquer janela do Button_Clickevento eu escrever:

TestClass testClass = new TestClass()

e TestClasstem alguma dependência, digamos, ITestentão deve ser resolvido automaticamente. Eu sei que posso usar:

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

Mas acho tedioso fazer isso toda vez que quero criar um objeto. Ele também força o desenvolvedor a criar o objeto de uma maneira específica. Pode ser melhorado?

Posso ter um repositório central para criação de objeto e, em seguida, toda criação de objeto usará esse repositório automaticamente?

Pravin Patil
fonte
1
Oi Pravin Patil: ótima pergunta. Fiz uma pequena alteração no seu título para deixar mais claro o que você está perguntando; fique à vontade para modificar se eu errei o alvo.
@ MarkTrapp: Obrigado pelo título adequado. Eu perdi essa slogan ...
Pravin Patil
Como nota secundária secundária, o projeto está escrito "Ninject", não "NInject". Embora possa ter sido o En-Inject, eles tocam no tema nin-ja hoje em dia. :) Cf. ninject.org
Cornelius

Respostas:

12

Para aplicativos clientes, geralmente é melhor adaptar um padrão como MVP (ou MVVM) e usar a ligação de dados do formulário ao ViewModel ou Presenter subjacente.

Para os ViewModels, você pode injetar as dependências necessárias usando a Injeção de Construtor padrão.

Na Raiz de composição do seu aplicativo, você pode conectar todo o gráfico de objetos do seu aplicativo. Você não precisa usar um DI Container (como o Ninject) para isso, mas pode.

Mark Seemann
fonte
7

Os aplicativos Windows Forms normalmente têm um ponto de entrada parecido com este:

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

Se você definir esse ponto no código como sua raiz de composição , poderá reduzir bastante o número de locais em que você tem código invocando explicitamente o Ninject como se fosse um Localizador de Serviço.

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

A partir deste ponto, você injeta todas as suas dependências via injeção de construtor.

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

Se sua "dependência" é algo que você precisa produzir várias vezes, o que você realmente precisa é de uma fábrica:

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

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

Você pode implementar a interface IFactory dessa maneira para evitar a necessidade de criar várias implementações pontuais:

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
fonte
Por favor, você tem a implementação completa para esta fábrica?
Tebo
@ ColorBlend: Não, mas se você se livrar das outras interfaces que eu tinha InjectionFactory<T>implementado, o código fornecido deve funcionar perfeitamente. Há algo em particular com o qual você está tendo problemas?
StriplingWarrior
Eu já o implementei, só quero saber se havia outras coisas interessantes dentro da classe.
Tebo
@Tebo: Acabei de implementar algumas outras interfaces relacionadas ao DI, como uma fábrica para a qual você poderia passar Type, mas que garantiria que os objetos hidratados Typeimplementassem ou estendessem um determinado tipo genérico. Nada muito especial.
precisa saber é o seguinte
4

Eu sempre escrevo um wrapper Adapter para qualquer Container IoC, que se parece com isso:

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 o Ninject, especificamente, a classe Adapter concreta é assim:

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);
    }
}

A principal razão para fazer isso é abstrair a estrutura da IoC, para que eu possa substituí-la a qualquer momento - dado que a diferença entre as estruturas geralmente está na configuração e não no uso.

Mas, como um bônus, as coisas também se tornam muito mais fáceis para o uso da estrutura de IoC em outras estruturas que não a suportam por natureza. Para o WinForms, por exemplo, são duas etapas:

No seu método Main, basta instanciar um contêiner antes de fazer qualquer outra coisa.

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());
        }
    }
}

E, em seguida, tenha um formulário base, do qual derivam outros formulários, que chama o próprio Injetar.

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

Isso informa à heurística da fiação automática para tentar injetar recursivamente todas as propriedades no formulário que atendem às regras definidas em seus módulos.

pdr
fonte
Solução muito boa ..... vou tentar.
Pravin Patil
10
Isso é um Locator Service, que é uma espetacular má idéia: blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
Mark Seemann
2
@ MarkSeemann: Um localizador de serviço é uma má idéia, se você acessá-lo de qualquer lugar, em vez de deixá-lo conectar seus objetos de nível superior o mais baixo possível. Leia o próprio comentário de Mark, um pouco abaixo da página: "Nesses casos, você realmente não tem recurso, a não ser mover a Raiz de Composição para cada objeto (por exemplo, Página) e deixar seu DI Container conectar suas dependências a partir daí. Isso pode parecer o antipadrão do Localizador de serviços, mas não é porque você ainda mantém o uso de contêineres no mínimo absoluto ". (Edit: Aguarde, você está Mark Então, qual é a diferença!?)
PDR
1
A diferença é que você ainda pode proteger o restante de sua base de código do Composer, em vez de disponibilizar um Singleton Service Locator para qualquer classe.
Mark Seemann
2
@pdr: Na minha experiência, se você está tentando injetar serviços em coisas como classes de atributos, não está separando as preocupações corretamente. Há casos em que a estrutura que você está usando torna praticamente impossível o uso de injeção de dependência adequada e, às vezes, somos forçados a usar um localizador de serviço, mas eu definitivamente tentaria levar a verdadeira DI o mais longe possível antes de voltar a isso padronizar.
StriplingWarrior
1

O bom uso da injeção de dependência geralmente depende da separação do código que cria objetos e da lógica de negócios real. Em outras palavras, eu não gostaria que minha equipe usasse newe criasse uma instância de uma classe dessa maneira. Uma vez feito, não há como trocar facilmente esse tipo criado por outro, pois você já especificou o tipo concreto.

Portanto, existem duas maneiras de corrigir isso:

  1. Injete as instâncias que uma classe precisará. No seu exemplo, injete um TestClassno seu Windows Form para que ele já tenha uma instância quando precisar. Quando o Ninject instancia seu formulário, ele cria automaticamente a dependência.
  2. Nos casos em que você realmente não deseja criar uma instância até precisar, pode injetar uma fábrica na lógica de negócios. Por exemplo, você pode injetar um IKernelno Windows Form e usá-lo para instanciar TestClass. Dependendo do seu estilo, existem outras maneiras de fazer isso (injetar uma classe de fábrica, um representante da fábrica, etc.).

Isso facilita a troca do tipo concreto de TestClass e a construção real da classe de teste, sem modificar o código que usa a classe de teste.

Chris Pitman
fonte
1

Eu não usei o Ninject, mas a maneira padrão de criar coisas quando você está usando uma IoC é através de um Func<T>where onde Té o tipo de objeto que você deseja criar. Portanto, se o objeto T1precisar criar objetos do tipo T2, o construtor de T1deve ter um parâmetro do tipo Func<T1>que será armazenado como um campo / propriedade de T2. Agora, quando você deseja criar objetos do tipo T2, T1invoca o Func.

Isso o separa totalmente da sua estrutura de IoC e é o caminho certo para codificar em uma mentalidade de IoC.

A desvantagem de fazer isso é que isso pode ser irritante quando você precisa conectar manualmente Funcs ou, por exemplo, quando o criador exige algum parâmetro, para que a IoC não consiga instalar automaticamente Funcpara você.

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


fonte