Wie kann man mit NInject am besten eine Fabrik bauen?

27

Ich bin ziemlich vertraut mit der Abhängigkeitsinjektion mit NInject in MVC3. Während der Arbeit in einer MVC3-Anwendung habe ich mit NInject eine benutzerdefinierte Controller Creation Factory entwickelt, sodass in jeden Controller, der erstellt wird, Abhängigkeiten über diese Controller Factory eingefügt werden.

Jetzt beginne ich mit der Entwicklung einer Windows-Anwendung. Ich möchte die anwendungsweite Abhängigkeitsinjektion verwenden. Das heißt, jedes Objekt muss über NInject erstellt werden, um das Testen von Einheiten zu vereinfachen. Bitte leiten Sie mich, um sicherzustellen, dass jedes erstellte Objekt nur von der NInject Factory stammt.

Wenn Button_Clickich zum Beispiel in einem Windows-Formular bei einem Ereignis schreibe:

TestClass testClass = new TestClass()

und TestClasshat eine Abhängigkeit von, sagen wir, ITestdann muss es automatisch aufgelöst werden. Ich weiß, dass ich verwenden kann:

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

Aber ich finde es mühsam, dies jedes Mal zu tun, wenn ich ein Objekt erstellen möchte. Es zwingt auch den Entwickler, das Objekt auf eine bestimmte Art und Weise zu erstellen. Kann es besser gemacht werden?

Kann ich ein zentrales Repository für die Objekterstellung haben und dann verwendet jede Objekterstellung automatisch dieses Repository?

Pravin Patil
quelle
1
Hallo Pravin Patil: tolle Frage. Ich habe Ihren Titel geringfügig geändert, um klarer zu machen, wonach Sie fragen. Fühlen Sie sich frei zu ändern, wenn ich die Marke verpasst habe.
@MarkTrapp: Danke für den passenden Titel. Ich habe diesen Slogan verpasst ...
Pravin Patil
Als kleine Randnotiz wird das Projekt "Ninject" geschrieben, nicht "NInject". Es mag sein, dass es En-Inject war, aber sie spielen heutzutage einiges über das Nin-Ja-Thema. :) Vgl. ninject.org
Cornelius

Antworten:

12

Für Clientanwendungen ist es häufig am besten, ein Muster wie MVP (oder MVVM) anzupassen und die Datenbindung vom Formular zum zugrunde liegenden ViewModel oder Presenter zu verwenden.

Für die ViewModels können Sie die erforderlichen Abhängigkeiten mithilfe der Standardkonstruktorinjektion einfügen.

Im Composition Root Ihrer Anwendung können Sie das gesamte Objektdiagramm für Ihre Anwendung verbinden. Sie müssen dafür keinen DI-Container (wie z. B. Ninject) verwenden, können dies aber.

Mark Seemann
quelle
7

Windows Forms-Anwendungen haben normalerweise einen Eintrittspunkt, der wie folgt aussieht:

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

Wenn Sie diesen Punkt in Code zu Ihrem Kompositionsstamm machen , können Sie die Anzahl der Stellen, an denen Sie Code haben, der Ninject explizit aufruft, wie bei einem Service Locator, erheblich reduzieren.

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

Ab diesem Zeitpunkt injizieren Sie alle Abhängigkeiten über die Konstruktorinjektion.

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

Wenn Ihre "Abhängigkeit" etwas ist, das Sie in der Lage sein müssen, mehrmals zu produzieren, dann ist das, was Sie wirklich brauchen, eine Fabrik:

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

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

Sie können die IFactory-Schnittstelle auf diese Weise implementieren, um zu vermeiden, dass Tonnen von einmaligen Implementierungen erstellt werden müssen:

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
quelle
Haben Sie die vollständige Implementierung für diese Fabrik?
Tebo
@ColourBlend: Nein, aber wenn Sie die anderen Schnittstellen, die ich InjectionFactory<T>implementiert habe, loswerden , sollte der bereitgestellte Code einwandfrei funktionieren. Gibt es etwas Besonderes, mit dem Sie Probleme haben?
StriplingWarrior
Ich habe es bereits implementiert, ich möchte nur wissen, ob es andere interessante Dinge in der Klasse gibt.
Tebo
@Tebo: Ich hatte es gerade geschafft, ein paar andere DI-bezogene Interfaces zu implementieren, wie eine Factory, an die Sie übergeben Typekönnen, die aber garantieren würde, dass die Objekte, für die es hydriert wurde Type, einen bestimmten generischen Typ implementieren oder erweitern. Nichts zu spezielles.
StriplingWarrior
4

Ich schreibe immer einen Adapter-Wrapper für jeden IoC-Container, der so aussieht:

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

Insbesondere für Ninject sieht die konkrete Adapterklasse folgendermaßen aus:

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

Der Hauptgrund dafür ist, das IoC-Framework zu abstrahieren, sodass ich es jederzeit ersetzen kann, da der Unterschied zwischen den Frameworks im Allgemeinen in der Konfiguration und nicht in der Verwendung liegt.

Als Bonus wird es jedoch auch viel einfacher, das IoC-Framework in anderen Frameworks zu verwenden, die es von Natur aus nicht unterstützen. Für WinForms sind es beispielsweise zwei Schritte:

Instanziieren Sie in Ihrer Main-Methode einfach einen Container, bevor Sie etwas anderes tun.

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

Und dann haben Sie eine Basisform, von der andere Formen ableiten, die Inject auf sich selbst aufruft.

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

Dies weist die Autoverkabelungsheuristik an, zu versuchen, alle Eigenschaften in dem Formular rekursiv einzufügen, das den in Ihren Modulen festgelegten Regeln entspricht.

pdr
quelle
Sehr schöne Lösung ..... Ich werde es versuchen.
Pravin Patil
10
Das ist ein Service Locator, was eine spektakulär schlechte Idee ist: blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
Mark Seemann
2
@MarkSeemann: Ein Service Locator ist eine schlechte Idee, wenn Sie von überall darauf zugreifen, anstatt Ihre Objekte der obersten Ebene so weit wie möglich zu verkabeln. Lesen Sie Marks eigenen Kommentar ein Stück weiter unten: "In solchen Fällen haben Sie wirklich keine andere Möglichkeit, als die Kompositionswurzel in jedes Objekt (z. B. Seite) zu verschieben, und lassen Sie Ihren DI-Container Ihre Abhängigkeiten von dort verbinden. Dies könnte so aussehen das Service Locator-Anti-Pattern, aber es liegt nicht daran, dass Sie den Containerverbrauch immer noch auf einem absoluten Minimum halten. " (Edit: Warte, du bist Mark! Also, was ist der Unterschied?)
pdr
1
Der Unterschied besteht darin, dass Sie den Rest Ihrer Codebasis weiterhin vor dem Composer schützen können, anstatt einen Singleton Service Locator für jede Klasse verfügbar zu machen.
Mark Seemann
2
@pdr: Wenn Sie meiner Erfahrung nach versuchen, Dienste in Dinge wie Attributklassen einzufügen, trennen Sie Bedenken nicht richtig. Es gibt Fälle, in denen das von Ihnen verwendete Framework die Verwendung einer ordnungsgemäßen Abhängigkeitsinjektion praktisch unmöglich macht, und manchmal sind wir gezwungen, einen Service-Locator zu verwenden, aber ich würde definitiv versuchen, eine echte DI so weit wie möglich zu verwenden, bevor ich darauf zurückkomme Muster.
StriplingWarrior
1

Eine gute Verwendung von Dependency Injection beruht normalerweise auf der Trennung des Codes, mit dem Objekte erstellt werden, und der eigentlichen Geschäftslogik. Mit anderen Worten, ich möchte nicht, dass mein Team auf diese Weise häufig neweine Instanz einer Klasse verwendet und erstellt. Sobald dies erledigt ist, gibt es keine Möglichkeit, den erstellten Typ einfach gegen einen anderen auszutauschen, da Sie den konkreten Typ bereits angegeben haben.

Es gibt also zwei Möglichkeiten, dies zu beheben:

  1. Injizieren Sie die Instanzen, die eine Klasse benötigt. Fügen Sie TestClassin Ihrem Beispiel ein in Ihr Windows Form ein, sodass es bereits eine Instanz hat, wenn es benötigt wird. Wenn Ninject Ihr Formular instanziiert, wird die Abhängigkeit automatisch erstellt.
  2. In Fällen , in denen Sie wirklich nicht wollen , eine Instanz erstellen , bis Sie sie benötigen, können Sie eine Fabrik in der Business - Logik injizieren. Beispielsweise können Sie IKernelin Windows Form eine einfügen und diese dann zum Instanziieren verwenden TestClass. Abhängig von Ihrem Stil gibt es auch andere Möglichkeiten, dies zu erreichen (Injizieren einer Fabrikklasse, eines Fabrikdelegierten usw.).

Dadurch ist es einfach, sowohl den konkreten TestClass-Typ als auch die tatsächliche Konstruktion der Testklasse auszutauschen, ohne den Code zu ändern, der die Testklasse verwendet .

Chris Pitman
quelle
1

Ich habe Ninject nicht verwendet, aber die Standardmethode zum Erstellen von Inhalten bei Verwendung einer IoC ist, dies über einen Bereich zu tun, Func<T>in dem Tder Typ des Objekts angegeben ist, das Sie erstellen möchten. Wenn also ein Objekt T1Objekte vom Typ erstellen muss, muss T2der Konstruktor von T1einen Parameter vom Typ haben, Func<T1>der dann als Feld / Eigenschaft von gespeichert wird T2. Nun , wenn Sie Objekte des Typs schaffen T2in T1rufen Sie auf der Func.

Dies entkoppelt Sie vollständig von Ihrem IoC-Framework und ist der richtige Weg, um in einer IoC-Denkweise zu programmieren.

Der Nachteil dabei ist, dass es ärgerlich werden kann, wenn Sie Funcs manuell verdrahten müssen oder wenn Ihr Ersteller beispielsweise einige Parameter benötigt, sodass die IoC das nicht automatisch verdrahten kannFunc für Sie kann.

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


quelle