Я использую 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()?
Итак, чтобы подвести итог: что такое хороший способ привязки к нескольким "версиям" того же типа?
Совершенно прекрасно создавать две привязки для одного и того же типа, которые отличаются только параметрами. Итак, что вам нужно сделать:
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);
}
Если вы не используете условные привязки, разрешение этих "множественных версий" во время выполнения, когда контейнер обнаруживает зависимость, приведет к исключению из-за двусмысленности. Он, безусловно, мог работать в доступе "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. Вы можете прочитать об этом здесь.