Связывание нескольких версий одного типа с Ninject

1

Я использую Ninject для создания набора "плагинов", например, у меня есть:

Bind<IFoo>().To<Class1>(); 
Bind<IFoo>().To<Class2>();
Bind<IFoo>().To<Class3>();

... и позже я использую kernel.GetAll<IFoo>() и перебираю результаты. Class1/Class2/Class3 каждый из Class1/Class2/Class3 реализует IFoo и имеет конструкторы, которые имеют набор параметров, также вводимых Ninject, например, конструктор Class1 является public Class1(IBar bar, IBaz baz), причем оба IBar и IBaz введенные Ninject. Все идет нормально.

Однако теперь я хочу иметь две разные "версии" Class1, оба связанные с IFoo, отличающиеся только значением, переданным во время построения. То есть, например, предположим, что конструктор Class1 был теперь public Class1(IBar bar, IBaz baz, bool myParameter), и я хочу сделать следующее:

Bind<IFoo>().To<Class1>(); //Somehow pass 'true' to myParameter here
Bind<IFoo>().To<Class1>(); //Somehow pass 'false' to myParameter here
Bind<IFoo>().To<Class2>();
Bind<IFoo>().To<Class3>();

... Потом, когда я называю kernel.GetAll<IFoo>(), я хочу 4 версии IFoo вернулась (Class1 "истинная" версия, Class1 ложной версия, Class2 и Class3). Я прочитал документацию Ninject и не могу найти способ сделать это.

Вот некоторые идеи, которые я пробовал, но никто из них не работает хорошо:

1) Я мог бы просто разделять классы (например, Class1True и Class1False), с одним, происходящим от другого, и привязываться к ним. Проблема в том, что это решение действительно не масштабируется, когда я должен делать это для многих классов - я в конечном итоге загрязняю свою иерархию классов множеством бесполезных классов, и проблема ухудшается, когда параметр конструктора, который я хочу передать, - это что-то более сложный, чем bool. Реалистичный пример:

Bind<IDrawingTool>().To<Brush>(); //Somehow pass '5' to brushThickness to create a fine brush
Bind<IDrawingTool>().To<Brush>(); //Somehow pass '25' to brushThickness to create a medium brush
Bind<IDrawingTool>().To<Brush>(); //Somehow pass '50' to brushThickness to create a coarse brush
Bind<IDrawingTool>().To<Pencil>();
Bind<IDrawingTool>().To<SprayCan>();

Конечно, это всего лишь одна возможная конфигурация бесконечного числа возможных. Создание нового класса для каждой толщины кисти кажется неправильным.

2) Я рассмотрел возможность использования привязки.ToMethod, что-то вроде этого:

Bind<IDrawingTool>().ToMethod(c => new Brush(5));
Bind<IDrawingTool>().ToMethod(c => new Brush(25));
Bind<IDrawingTool>().ToMethod(c => new Pencil());

Но в этом случае я запутался в следующем:

a) Что делать, если конструктор Brush() действительно требует и других параметров, которые должны быть введены через Ninject?

b) Фактически разрешены ли множественные привязки ToMethod?

c) Будет ли это работать с InSingletonScope()?

Итак, чтобы подвести итог: что такое хороший способ привязки к нескольким "версиям" того же типа?

Теги:
dependency-injection
ninject

2 ответа

2
Лучший ответ

Совершенно прекрасно создавать две привязки для одного и того же типа, которые отличаются только параметрами. Итак, что вам нужно сделать:

Bind<IFoo>().To<Class1>().WithConstructorArgument("boolParameterName", true);
Bind<IFoo>().To<Class1>().WithConstructorArgument("boolParameterName", false);

Используйте параметр WithConstructorArgument для передачи параметра. Вы можете либо присвоить Ninject параметру по имени - в приведенном выше примере Class1 ctor необходимо будет указать параметр bool, имя которого точно boolParameterName. Или вы можете сопоставить тип, и в этом случае у вас может быть только один параметр этого типа в конструкторе. Пример: WithConstructorArgument(typeof(bool), true). Все параметры, которые вы не указываете WithConstructorArgument получают ctor-inject "как обычно".


Полный рабочий пример (с использованием пакетов xunit и FluentAssertions nuget):

public interface IBar { }
public class Bar : IBar { }

public interface IFoo { }

class Foo1 : IFoo
{
    public Foo1(IBar bar) { }
}

class Foo2 : IFoo
{
    public Foo2(IBar bar, bool theParametersName) { }
}

    [Fact]
    public void FactMethodName()
    {
        var kernel = new StandardKernel();
        kernel.Bind<IBar>().To<Bar>();
        kernel.Bind<IFoo>().To<Foo1>();
        kernel.Bind<IFoo>().To<Foo2>().WithConstructorArgument("theParametersName", true);
        kernel.Bind<IFoo>().To<Foo2>().WithConstructorArgument("theParametersName", false);

        List<IFoo> foos = kernel.GetAll<IFoo>().ToList();

        foos.Should().HaveCount(3);
    } 
2

Если вы не используете условные привязки, разрешение этих "множественных версий" во время выполнения, когда контейнер обнаруживает зависимость, приведет к исключению из-за двусмысленности. Он, безусловно, мог работать в доступе "service-locator" -based, но в истинном DI, составляющем граф объектов, вы столкнетесь с проблемой, используя этот подход.

В вашем изображенном сценарии эта двусмысленность возникла бы, если бы существовала следующая гипотетическая ситуация:

public class MyDraw
{
    public MyDraw(IDrawingTool drawingTool)
    {
        // Your code here
    }
}

kernel.Bind<IDrawingTool>().ToMethod(c => new Brush(5));
kernel.Bind<IDrawingTool>().ToMethod(c => new Brush(25));
kernel.Bind<IDrawingTool>().ToMethod(c => new Pencil);

// Runtime exception due to ambiguity: How would the container know which drawing tool to use?
var md = container.Get<MyDraw>();

Однако, если у вас есть этот класс, который нужно ввести:

public class MyDraw
{
    public MyDraw(IEnumerable<IDrawingTool> allTools)
    {
        // Your code here
    }
}

Это будет работать благодаря многократной инъекции. Контейнер просто вызовет все привязки, соответствующие IDrawingTool. В этом случае допускается несколько привязок, даже ToMethod(...).

Что вам нужно сделать, это полагаться на такие механизмы, как Named Bindings или Contextual Bindings (используя WhenXXX(...), чтобы позволить цели инъекции определить, какую конкретную реализацию он требует. Ninject имеет обширную поддержку для этого и фактически является одним определяющих особенностей для этого ядра DI Framework. Вы можете прочитать об этом здесь.

  • 0
    Я всегда использую мультиинъекцию (через kernel.GetAll <T>) на этом конкретном интерфейсе. Тем не менее, вы не ответили на мой вопрос. Похоже, вы предлагаете, чтобы я использовал подход ToMethod (), но как насчет проблемы, которую я обозначил (а) в своем вопросе - что, если конструктору Brush требуются другие параметры, которые должны быть введены, - как я могу создать экземпляр Brush внутри ToMethod ()?

Ещё вопросы

Сообщество Overcoder
Наверх
Меню