摘要
如果我们已经知道了一个类所有的依赖项,在我们只需要依赖项的一个实例的场景中,在类的构造函数中引入一系列的依赖项是容易的。但是有些情况,我们需要在一个类里创建依赖项的多个实例,这时候Ninject注入就不够用了。也有些情况,我们不知道一个消费者可能需要哪个服务,因为他可能在不同的场合下需要不同的服务,而且在创建类的时候实例化所有依赖项也不合理。这样的情况,动态工厂可以帮忙。我们可以设计我们的类让他依赖一个工厂,而不是依赖这个工厂能够创建的对象。然后,我们能够命令工厂去通过命令创建需要的类型和任意需要的数量。下面两个例子解决上面两个问题。Ninject动态工厂创建指定数量的依赖项和创建指定类型的依赖项。
例子:形状工厂
附:代码下载
在第一个例子中,我们将创建一个图形动态库。它包含一个ShapService类,提供一个AddShapes方法来给指定的ICanvas对象添加指定数量具体的IShape对象:
public void AddShapes(int circles, int squares, ICanvas canvas)
{
for (int i = ; i < circles; i++)
{
var circle = new Circle();
canvas.AddShap(circle);
}
for (int i = ; i < squares; i++)
{
var square = new Square();
canvas.AddShap(square);
}
}
传统的方法是直接在AddShapes方法里创建新的Circle和Square类实例。然而,这个方法我们将ShapService类和具体的Circle和Square类耦合起来,这和DI原则相反。另外,通过参数引入这些依赖项不符合我们的需求,因为那样一个形状只注入一个实例,这样不够。为了解决这个问题,我们应该像下面首先创建一个简单工厂接口:
public interface IShapeFactory
{
ICircle CreateCircle();
ISquare CreateSquare();
}
然后,我们可以引入这个工厂接口作为ShapeService类的依赖项。
public class ShapeService
{
private readonly IShapeFactory _factory; public ShapeService(IShapeFactory factory)
{
this._factory = factory;
} public void AddShapes(int circles, int squares, ICanvas canvas)
{
for (int i = ; i < circles; i++)
{
var circle = _factory.CreateCircle();
canvas.AddShap(circle);
}
for (int i = ; i < squares; i++)
{
var square = _factory.CreateSquare();
canvas.AddShap(square);
}
}
}
好消息是我们不需要担心怎样实现IShapeFactory。Ninject能够动态地实现它,再注入这个实现的工厂到这个ShapeService类。我们只需要添加下面的代码到我们类型注册部分:
Bind<IShapeFactory>().ToFactory();
Bind<ISquare>().To<Square>();
Bind<ICircle>().To<Circle>();
为了使用Ninject工厂,我们需要添加Ninject.Extensions.Factory动态库的引用。可以通过NuGet添加,也可以通过从Ninject官方网站上下载。
记住工厂可以有需要的尽可能多的方法,每个方法可以返回任意需要的类型。这些方法可以有任意的名字,有任意数量的参数。唯一的限制是名字和参数类型必须跟具体类名字和构造函数参数的类型一致,但是跟他们的顺序没关系。甚至参数的数量都不需要一致,Ninject将试着解析那些没有通过工厂接口提供的参数。
因此,如果具体Square类是下面这样:
public class Square
{
public Square(Point startPoint, Point endPoint)
{ ... }
}
这个IShapeFactory工厂接口就应该像下面这样:
public interface IShapeFactory
{
ICircle CreateCircle();
ISquare CreateSquare(Point startPoint, Point endPoint);
}
或者,CreateSquare方法可能像下面这样:
ISquare CreateSquare(Point endPoint, Point startPoint);
这是Ninject动态工厂默认的行为。然而,通过创建自定义实例提供者,默认行为可以被重写。后面的文章将要介绍这个。
对动态工厂注册基于约定的绑定和常规的约定绑定稍微有点不同。不同在于,一旦我们选择了程序集,我们应该选择服务类型而不是组件,然后绑定他们到工厂。下面描述怎样实现这两个步骤。
- 选择服务类型
使用下面的方法选择一个抽象类或接口:
- SelectAllIncludingAbstractClasses(): 这个方法选择所有的类,包括抽象类。
- SelectAllAbstractClasses(): 这个方法只选择抽象类。
- SelectAllInterfaces(): 这个方法选择所有接口。
- SelectAllTypes(): 这个方法选择所有类型(类、接口、结构、共用体和原始类型)
下面的代码绑定选择的程序集下的所有接口到动态工厂:
kernel.Bind(x => x
.FromAssembliesMatching("factories")
.SelectAllInterfaces()
.BindToFactory());
2. 定义绑定生成器
使用下面的方法定义合适的绑定生成器:
- BindToFactory: 这个方法注册映射的类型作为动态工厂。
- BindWith: 这个方法使用绑定生成器参数创建绑定。创建一个绑定生成器只是关于实现IBindingGenerator接口的问题
下面的例子绑定当前程序集中所有那些以Factory结尾的接口到动态工厂。
kernel.Bind(x => x
.FromThisAssembly()
.SelectAllInterfaces()
.EndingWith("Factory")
.BindToFactory());
例子:电信交换机
附:代码下载
在下面的例子中,我们将为电信中心写一个服务,这个服务返回指定交换机当前状态信息。电信交换机生产于不同的厂家,可能提供不同的方法查询状态。一些支持TCP/IP协议通信,一些只是简单地将状态写入一个文件。
先按下面这样创建Switch类:
public class Switch
{
public string Name { get; set; }
public string Vendor { get; set; }
public bool SupportsTcpIp { get; set; }
}
收集交换机状态像下面创建一个接口:
public interface IStatusCollector
{
string GetStatus(Switch @switch);
}
为两种不同的交换机类型,我们需要对这个接口的两个不同的实现。支持TCP/IP通信的交换机和那些不支持的。分别创建TcpStatusCollector类和FileStatusCollector类:
public class TcpStatusCollector : IStatusCollector
{
public string GetStatus(Switch @switch)
{
System.Console.WriteLine("TCP Get Status");
return "TCP Status";
}
} public class FileStatusCollector : IStatusCollector
{
public string GetStatus(Switch @switch)
{
System.Console.WriteLine("File Get Status");
return "File Status";
}
}
我们还需要声明一个可以创建者两种具体StatusCollector实例的工厂接口:
public interface IStatusCollectorFactory
{
IStatusCollector GetTcpStatusCollector();
IStatusCollector GetFileStatusCollector();
}
最后是SwitchService类:
public class SwitchService
{
private readonly IStatusCollectorFactory factory; public SwitchService(IStatusCollectorFactory factory)
{
this.factory = factory;
} public string GetStatus(Switch @switch)
{
IStatusCollector collector;
if (@switch.SupportsTcpIp)
{
collector = factory.GetTcpStatusCollector();
}
else
{
collector = factory.GetFileStatusCollector();
}
return collector.GetStatus(@switch);
}
}
这个SwitchService类将绝不会创建一个FileStatusCollector实例,如果所有给定的交换机都支持TCP/IP。按这种方法,SwitchService类只注入他真实需要的依赖项,而不是所有他可能需要的依赖项。
IStatusCollectorFactory有两个工厂方法,两个都返回相同的类型。现在,Ninject这个工厂的实现如何理解怎样解析IStatusCollector?魔法在于工厂方法的名字。无论何时工厂方法的名字以Get开头,它指明这个类型将用名称绑定来解析,类型名称就是方法名后面那一串。例如,如果工厂方法名称是GetXXX,这个工厂将试着去找一个名称为XXX的绑定。因此,这个例子的类型注册段应该像下面这样:
Kernel.Bind(x => x.FromThisAssembly()
.SelectAllInterfaces()
.EndingWith("Factory")
.BindToFactory()); Kernel.Bind(x => x.FromThisAssembly()
.SelectAllClasses()
.InheritedFrom<IStatusCollector>()
.BindAllInterfaces()
.Configure((b, comp) => b.Named(comp.Name)));
第一个约定绑定那些名称以Factory结尾的接口到工厂。
第二个为所有的IStatusCollector的实现注册名称绑定,按这种方式,每个绑定用他的组件名称命名。它等同于下面单独的两行绑定:
Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().Named("TcpStatusCollector");
Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().Named("FileStatusCollector");
然而,使用这样的单独绑定依赖于字符串名称,这很容易出错,这些关系可能很容易被拼写错误打断。有另一种特别为这种情况设计的单独绑定命名的方式,当引用了Ninject.Extensions.Factory时才可用。我们可以使用NamedLikeFactoryMethod这个帮助方法而不用Named帮助方法来为一个工厂绑定命名:
Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetTcpStatusCollector());
Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetFileStatusCollector());
他意思是说我们在用工厂方法建议的名称定义一个名称绑定。
请注意使用约定经常是首选的方式。
自定义实例提供者
动态工厂不直接实例化请求的类型。然而,它使用另一个称为实例提供者的对象(不要把他跟提供者混淆)来创建一个类型的实例。一些关于工厂方法的信息提供给了这个实例提供者。基于哪一个实例提供者应该来解析请求对象,这些信息包含方法名称、它的返回类型和它的参数。如果一个工厂没有赋给一个自定义实例提供者,它将使用它默认的实例提供者,名称是StandardInstanceProvider。我们可以在注册的时候赋给一个自定义实例提供者到一个工厂,像下面这样:
Kernel.Bind(x => x.FromThisAssembly()
.SelectAllInterfaces()
.EndingWith("Factory")
.BindToFactory(() => new MyInstanceProvider()));
为了使Ninject接受一个类作为一个实例提供者,实现IInstanceProvider接口的类就足够了。然而,更简单的方法是继承StandardInstanceProvider类并重载相应的成员。
下面的代码显示如何定义一个实例提供者,从NamedAttribute得到绑定名称,而不是从方法名称:
public class NameAttributeInstanceProvider : StandardInstanceProvider
{
protected override string GetName(System.Reflection.MethodInfo methodInfo, object[] arguments)
{
var nameAttribute = methodInfo
.GetCustomAttributes(typeof(NamedAttribute), true)
.FirstOrDefault() as NamedAttribute;
if (nameAttribute != null)
{
return nameAttribute.Name;
}
return base.GetName(methodInfo, arguments);
}
}
使用自定义实例提供者,我们能够选择任意名称作为我们的工厂名称,然后使用一个特性来指定请求的绑定名称。
由于Ninject的NamedAttribute特性不能运用在方法上,我们需要创建我们自己的特性:
public class BindingNameAttribute : Attribute
{
public BindingNameAttribute(string name)
{
this.Name = name;
}
public string Name { get; set; }
}
NameAttributeInstanceProvider改为下面这样:
public class NameAttributeInstanceProvider : StandardInstanceProvider
{
protected override string GetName(System.Reflection.MethodInfo methodInfo, object[] arguments)
{
var nameAttribute = methodInfo
.GetCustomAttributes(typeof(BindingNameAttribute), true)
.FirstOrDefault() as BindingNameAttribute;
if (nameAttribute != null)
{
return nameAttribute.Name;
}
return base.GetName(methodInfo, arguments);
}
}
工厂接口现在可以像下面这样定义:
public interface IStatusCollectorFactory
{
[BindingName("TcpCollector")]
IStatusCollector GetTcpCollector(); [BindingName("FileCollector")]
IStatusCollector GetFileCollector();
}
工厂类型注册应该变成下面这样:
Kernel.Bind(x => x.FromThisAssembly()
.SelectAllInterfaces()
.EndingWith("Factory")
.BindToFactory(() => new NameAttributeInstanceProvider()));
IStatusCollector注册应该改成下面这样:
Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().Named("TcpCollector");
Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().Named("FileCollector");
或者下面这样:
Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetTcpCollector());
Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetFileCollector());