Jaki jest najlepszy sposób na zbudowanie fabryki za pomocą NInject?

27

Czuję się swobodnie dzięki wstrzykiwaniu zależności za pomocą NInject w MVC3. Pracując w aplikacji MVC3, stworzyłem niestandardową fabrykę tworzenia kontrolerów za pomocą NInject, więc każdy utworzony kontroler będzie miał wstrzykiwane w nim zależności za pośrednictwem tej fabryki kontrolerów.

Teraz zaczynam opracowywać aplikację dla systemu Windows, chcę korzystać z wstrzykiwania zależności w całej aplikacji. tzn. Każdy obiekt musi zostać utworzony za pomocą NInject, aby ułatwić Testowanie Jednostek. Proszę, poprowadź mnie, aby upewnić się, że każdy utworzony obiekt musi być obsługiwany tylko przez NInject Factory.

Na przykład, jeśli w dowolnym oknie formularza na Button_Clickzdarzenie piszę:

TestClass testClass = new TestClass()

i TestClassma jakąkolwiek zależność, powiedzmy, ITestwtedy to musi zostać automatycznie rozwiązane. Wiem, że mogę użyć:

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

Ale uważam, że to nużące robić to za każdym razem, gdy chcę stworzyć obiekt. Zmusza także programistę do stworzenia obiektu w określony sposób. Czy można to poprawić?

Czy mogę mieć centralne repozytorium do tworzenia obiektów, a następnie każde stworzenie obiektu będzie automatycznie korzystać z tego repozytorium?

Pravin Patil
źródło
1
Cześć Pravin Patil: świetne pytanie. Wprowadziłem niewielką zmianę w tytule, aby wyjaśnić, o co pytasz; nie krępuj się zmodyfikować, jeśli przegapię znak.
@MarkTrapp: Dziękujemy za odpowiedni tytuł. Brakowało mi tego sloganu ...
Pravin Patil,
Jako drobna uwaga dodatkowa, projekt ma literę „Ninject”, a nie „NInject”. Chociaż być może był to En-Inject, obecnie grają na temat nin-ja. :) Por. ninject.org
Cornelius

Odpowiedzi:

12

W przypadku aplikacji klienckich często najlepiej jest dostosować wzorzec, taki jak MVP (lub MVVM) i użyć powiązania danych z formularza do bazowego ViewModel lub Presenter.

W przypadku ViewModels można wstrzyknąć wymagane zależności za pomocą standardowego wstrzykiwania konstruktora.

W aplikacji Root kompozycji można połączyć cały wykres obiektów dla aplikacji. Nie musisz do tego używać kontenera DI (takiego jak Ninject), ale możesz.

Mark Seemann
źródło
7

Aplikacje Windows Forms zazwyczaj mają punkt wejścia, który wygląda następująco:

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

Jeśli ustawisz ten punkt w kodzie jako główny komponent , możesz znacznie zmniejszyć liczbę miejsc, w których masz kod jawnie wywołujący Ninject tak, jakby był Lokalizatorem usług.

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

Od tego momentu wstrzykujesz wszystkie swoje zależności za pomocą wstrzykiwania konstruktora.

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

Jeśli twoja „zależność” jest czymś, czego potrzebujesz, aby móc tworzyć wiele razy, to tak naprawdę potrzebujesz fabryki:

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

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

Możesz zaimplementować interfejs IFactory w ten sposób, aby uniknąć konieczności tworzenia mnóstwa jednorazowych implementacji:

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
źródło
Czy masz pełną implementację tej fabryki?
Tebo
@ColourBlend: Nie, ale jeśli pozbędziesz się innych interfejsów, które miałem InjectionFactory<T>zaimplementowane, dostarczony kod powinien działać dobrze. Czy jest coś szczególnego, z czym masz problemy?
StriplingWarrior
Już go wdrożyłem, chcę tylko wiedzieć, czy w klasie były inne interesujące rzeczy.
Tebo
@Tebo: Właśnie wdrożyłem kilka innych interfejsów związanych z DI, na przykład fabrykę, do której można przekazać Type, ale która zagwarantuje, że uwodnione obiekty dla tej Typeimplementacji lub rozszerzenia danego typu ogólnego. Nic specjalnego.
StriplingWarrior
4

Zawsze piszę opakowanie adaptera dla dowolnego kontenera IoC, który wygląda następująco:

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

Konkretnie dla Ninject konkretna klasa adaptera wygląda następująco:

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

Głównym powodem tego jest wyodrębnienie frameworka IoC, dzięki czemu mogę go zastąpić w dowolnym momencie - biorąc pod uwagę, że różnica między frameworkami jest zasadniczo w konfiguracji, a nie w użyciu.

Ale jako bonus, rzeczy stają się o wiele łatwiejsze w korzystaniu z frameworka IoC wewnątrz innych frameworków, które nie obsługują go od razu. Na przykład w przypadku WinForms są to dwa kroki:

W metodzie Main po prostu utwórz instancję kontenera, zanim zrobisz cokolwiek innego.

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

A potem mają podstawową Formę, z której wywodzą się inne formy, która sama wywołuje Inject.

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

To mówi heurystyce automatycznego okablowania, aby spróbowała rekurencyjnie wstrzyknąć wszystkie właściwości w formie zgodnej z regułami ustawionymi w modułach.

pdr
źródło
Bardzo fajne rozwiązanie ... Spróbuję.
Pravin Patil,
10
To lokalizator usług, który jest spektakularnie złym pomysłem: blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
Mark Seemann
2
@MarkSeemann: Lokalizator usług jest złym pomysłem, jeśli masz do niego dostęp z dowolnego miejsca, zamiast pozwalać mu łączyć obiekty najwyższego poziomu tak daleko, jak to możliwe. Przeczytaj własny komentarz Marka, trochę w dół strony: „W takich przypadkach tak naprawdę nie masz innego wyjścia, jak tylko przenieść Korzeń Kompozycji do każdego obiektu (np. Strony) i pozwolić kontenerowi DI połączyć stamtąd swoje zależności. Może to wyglądać jak anty-wzorzec Lokalizatora usług, ale nie dlatego, że nadal utrzymujesz użycie kontenera na absolutnym minimum ”. (Edycja: Czekaj, JESTEŚ Mark! Więc jaka jest różnica?)
pdr
1
Różnica polega na tym, że nadal możesz chronić resztę bazy kodu przed Kompozytorem, zamiast udostępniać Lokalizator usług Singleton dowolnej klasie.
Mark Seemann,
2
@pdr: Z mojego doświadczenia wynika, że ​​jeśli próbujesz wstrzyknąć usługi do rzeczy takich jak klasy atrybutów, nie rozdzielasz problemów we właściwy sposób. Zdarzają się sytuacje, w których używana platforma sprawia, że ​​praktycznie niemożliwe jest zastosowanie właściwego wstrzykiwania zależności, a czasem jesteśmy zmuszeni do użycia lokalizatora usług, ale zdecydowanie powrócę tak daleko, jak to możliwe, przed powrotem do tego wzór.
StriplingWarrior
1

Dobre wykorzystanie wstrzykiwania zależności zwykle polega na oddzieleniu kodu tworzącego obiekty i rzeczywistej logiki biznesowej. Innymi słowy, nie chciałbym, aby mój zespół często używał newi tworzył instancję klasy w ten sposób. Po zakończeniu nie ma możliwości łatwej zamiany utworzonego typu na inny, ponieważ określono już konkretny typ.

Istnieją więc dwa sposoby rozwiązania tego problemu:

  1. Wstrzyknij instancje, które będą potrzebne klasie. W swoim przykładzie wstrzyknij a TestClassdo formularza Windows, aby miał już instancję, gdy jej potrzebuje. Kiedy Ninject tworzy twoją formę, automatycznie tworzy zależność.
  2. W przypadkach, gdy naprawdę nie chcesz tworzyć instancji, dopóki jej nie potrzebujesz, możesz wprowadzić fabrykę do logiki biznesowej. Na przykład możesz wstrzyknąć IKerneldo formularza Windows, a następnie użyć go do utworzenia wystąpienia TestClass. W zależności od stylu istnieją również inne sposoby osiągnięcia tego celu (wstrzyknięcie klasy fabrycznej, delegata fabryki itp.).

W ten sposób można łatwo zamienić zarówno konkretny typ TestClass, jak i zmodyfikować faktyczną budowę klasy testowej, bez modyfikowania kodu używającego klasy testowej.

Chris Pitman
źródło
1

Nie używałem Ninject, ale standardowy sposób tworzenia rzeczy kiedy używasz IoC jest to zrobić poprzez A Func<T>gdzie Tjest typ obiektu, który chcesz utworzyć. Więc jeśli obiekt T1musi tworzyć obiekty typu, T2to konstruktor T1musi mieć parametr typu, Func<T1>który jest następnie przechowywany jako pole / właściwość T2. Teraz, gdy chcemy utworzyć obiekty typu T2w T1wywołaniu Func.

To całkowicie oddziela cię od twojego środowiska IoC i jest właściwym sposobem na kodowanie w myśleniu IoC.

Wadą tego jest to, że może być denerwujące, gdy musisz ręcznie połączyć Funcs lub przykład, gdy twój twórca wymaga jakiegoś parametru, więc IoC nie może automatycznie uruchomić Funcdla ciebie.

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


źródło