Quelle est la meilleure façon de construire une usine en utilisant NInject?

27

Je suis assez à l'aise avec l'injection de dépendances à l'aide de NInject dans MVC3. Tout en travaillant dans une application MVC3, j'ai développé une usine de création de contrôleur personnalisée à l'aide de NInject, de sorte que tout contrôleur créé aura des dépendances injectées via cette usine de contrôleur.

Maintenant, je commence à développer une application Windows, je veux utiliser l'injection de dépendance à l'échelle de l'application. c'est-à-dire que chaque objet doit être créé via NInject, afin de faciliter les tests unitaires. Veuillez me guider pour vous assurer que chaque objet créé doit passer par la NInject Factory uniquement.

Par exemple, si sur un formulaire Windows sur un Button_Clickévénement, j'écris:

TestClass testClass = new TestClass()

et TestClassa une dépendance sur, disons, ITestalors il doit être automatiquement résolu. Je sais que je peux utiliser:

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

Mais je trouve cela fastidieux de le faire chaque fois que je veux créer un objet. Cela oblige également le développeur à créer l'objet d'une manière particulière. Peut-il être amélioré?

Puis-je avoir un référentiel central pour la création d'objets et chaque création d'objet utilisera automatiquement ce référentiel?

Pravin Patil
la source
1
Salut Pravin Patil: grande question. J'ai apporté une modification mineure à votre titre pour clarifier ce que vous demandez; n'hésitez pas à modifier si j'ai raté la marque.
@MarkTrapp: Merci pour le titre approprié. J'ai raté ce slogan ...
Pravin Patil
Comme note secondaire mineure, le projet est orthographié "Ninject", pas "NInject". Bien qu'il puisse s'agir d'In-Inject, ils jouent beaucoup sur le thème nin-ja de nos jours. :) Cf. ninject.org
Cornelius

Réponses:

12

Pour les applications clientes, il est souvent préférable d'adapter un modèle comme MVP (ou MVVM) et d'utiliser la liaison de données du formulaire au ViewModel ou au Presenter sous-jacent.

Pour les ViewModels, vous pouvez injecter les dépendances requises à l'aide de l'injection de constructeur standard.

Dans la racine de composition de votre application, vous pouvez câbler le graphique d'objet entier pour votre application. Vous n'avez pas besoin d'utiliser un conteneur DI (tel que Ninject) pour cela, mais vous pouvez.

Mark Seemann
la source
7

Les applications Windows Forms ont généralement un point d'entrée qui ressemble à ceci:

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

Si vous faites de ce point dans le code votre racine de composition , vous pouvez considérablement réduire le nombre d'emplacements où vous avez du code invoquant explicitement Ninject comme s'il s'agissait d'un localisateur de service.

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

À partir de ce point, vous injectez toutes vos dépendances via l'injection de constructeur.

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

Si votre "dépendance" est quelque chose dont vous avez besoin pour pouvoir produire plusieurs fois, alors ce dont vous avez vraiment besoin est une usine:

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

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

Vous pouvez implémenter l'interface IFactory de cette façon pour éviter d'avoir à créer une tonne d'implémentations uniques:

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
la source
S'il vous plaît, avez-vous la mise en œuvre complète de cette usine?
Tebo
@ColourBlend: Non, mais si vous vous débarrassez des autres interfaces que j'avais InjectionFactory<T>implémentées, alors le code fourni devrait fonctionner très bien. Y a-t-il quelque chose en particulier avec lequel vous rencontrez des problèmes?
StriplingWarrior
Je l'ai déjà implémenté, je veux juste savoir s'il y avait d'autres choses intéressantes dans la classe.
Tebo
@Tebo: Je viens de le faire implémenter quelques autres interfaces liées à DI, comme une usine à laquelle vous pourriez passer Type, mais qui garantirait que les objets qu'il hydrate pour cet Typeimplémentation ou étendent un type générique donné. Rien de bien spécial.
StriplingWarrior
4

J'écris toujours un wrapper d'adaptateur pour tout conteneur IoC, qui ressemble à ceci:

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

Pour Ninject, en particulier, la classe Adapter concrète ressemble à ceci:

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 principale raison pour cela est d'abstraire le cadre IoC, donc je peux le remplacer à tout moment - étant donné que la différence entre les cadres est généralement dans la configuration plutôt que dans l'utilisation.

Mais, en prime, les choses deviennent également beaucoup plus faciles pour l'utilisation du cadre IoC dans d'autres cadres qui ne le supportent pas de manière inhérente. Pour WinForms, par exemple, il s'agit de deux étapes:

Dans votre méthode Main, instanciez simplement un conteneur avant de faire autre chose.

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

Et puis avoir une forme de base, dont dérivent d'autres formes, qui appelle Injecter sur elle-même.

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

Cela indique à l'heuristique de câblage automatique de tenter d'injecter récursivement toutes les propriétés du formulaire qui correspondent aux règles définies dans vos modules.

pdr
la source
Très belle solution ..... je vais l'essayer.
Pravin Patil
10
C'est un localisateur de services, ce qui est une très mauvaise idée: blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
Mark Seemann
2
@MarkSeemann: Un localisateur de service est une mauvaise idée, si vous y accédez de partout, au lieu de le laisser câbler vos objets de niveau supérieur aussi bas que possible. Lisez le commentaire de Mark, un peu en bas de la page: "Dans de tels cas, vous n'avez vraiment aucun autre recours que de déplacer la racine de composition dans chaque objet (par exemple la page) et laissez votre conteneur DI câbler vos dépendances à partir de là. Cela peut ressembler à l'anti-modèle Service Locator, mais ce n'est pas parce que vous gardez toujours l'utilisation du conteneur au minimum absolu. " (Edit: Attendez, vous ÊTES Mark! Alors quelle est la différence?)
pdr
1
La différence est que vous pouvez toujours protéger le reste de votre base de code du compositeur, au lieu de rendre un localisateur de service Singleton disponible pour n'importe quelle classe.
Mark Seemann
2
@pdr: D'après mon expérience, si vous essayez d'injecter des services dans des choses comme les classes d'attributs, vous ne séparez pas correctement les problèmes. Il y a des cas où le cadre que vous utilisez rend pratiquement impossible d'utiliser l'injection de dépendance appropriée, et parfois nous sommes obligés d'utiliser un localisateur de service, mais j'essaierais certainement de prendre la vraie DI autant que possible avant de revenir à cela modèle.
StriplingWarrior
1

Une bonne utilisation de l'injection de dépendance repose généralement sur la séparation du code qui crée les objets et la logique métier réelle. En d'autres termes, je ne voudrais pas que mon équipe utilise newet crée fréquemment une instance d'une classe de cette façon. Une fois cela fait, il n'y a aucun moyen de remplacer facilement ce type créé par un autre, car vous avez déjà spécifié le type de béton.

Il y a donc deux façons de résoudre ce problème:

  1. Injectez les instances dont une classe aura besoin. Dans votre exemple, injectez un TestClassdans votre Windows Form afin qu'il ait déjà une instance quand il en a besoin. Lorsque Ninject instancie votre formulaire, il crée automatiquement la dépendance.
  2. Dans les cas où vous ne voulez vraiment pas créer d'instance tant que vous n'en avez pas besoin, vous pouvez injecter une usine dans la logique métier. Par exemple, vous pouvez injecter un IKerneldans votre formulaire Windows, puis l'utiliser pour instancier TestClass. En fonction de votre style, il existe également d'autres moyens d'y parvenir (injection d'une classe d'usine, délégué d'usine, etc.).

Cela permet d'échanger facilement à la fois le type concret de TestClass, ainsi que de modifier la construction réelle de la classe de test, sans réellement modifier le code qui utilise la classe de test.

Chris Pitman
la source
1

Je n'ai pas utilisé Ninject, mais la façon standard de créer des choses lorsque vous utilisez un IoC est de le faire via un Func<T>Test le type d'objet que vous souhaitez créer. Donc, si l'objet T1doit créer des objets de type, T2le constructeur de T1doit avoir un paramètre de type Func<T1>qui est ensuite stocké en tant que champ / propriété de T2. Maintenant , quand vous voulez créer des objets de type T2à T1vous invoquez la Func.

Cela vous dissocie totalement de votre infrastructure IoC et est la bonne façon de coder dans un état d'esprit IoC.

L'inconvénient de cela est qu'il peut devenir ennuyeux lorsque vous devez câbler manuellement un Funcexemple ou lorsque votre créateur a besoin de certains paramètres, de sorte que l'IoC ne peut pas câbler automatiquement le Funcpour vous.

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


la source