服务容器

Container 服务容器是一个用于管理类的依赖执行依赖注入的强大工具。其实质是通过反射来对构造函数或者标记为[Inject]特性的属性选择器进行注入。

简介

几乎所有的服务绑定都是在服务提供者中完成。如果一个服务没有基于任何接口那么就没有必要将其绑定到容器。

容器并不需要被告知如何构建对象,因为它会使用反射服务自动解析出具体的对象实例。在任何位置,您可以通过App全局变量访问容器。在服务提供者中我们可以使用Bind()或者Singleton()方法来注册一个服务的绑定。

var container = new Container(); // 构建一个单纯的服务容器

构建服务

您可以通过Make()方法来构建服务。如果通过容器去生成一个没有被绑定的服务,那么容器会根据策略尝试解决需要被生成的服务,如果尝试解决失败,那么将会抛出一个UnresolvableException异常。

CatLib Framework 中所有可以直接通过Make()生成的服务请参考:服务表

绑定服务 - 实例

要使用服务容器的强大特性您首先需要将服务绑定到服务容器,如果您不不进行绑定您将没有任何服务可用,这些绑定一般性都是在 服务提供者 中完成的。

绑定服务分为以下两种类型:

通过Bind()方法允许将服务以实例绑定的方式进行绑定。

App.Bind<SocketManager>().Alias<ISocketManager>();

我们通过上述代码将SocketManager这个服务绑定到了服务容器。同时赋予了ISocketManager的别名。

这样您就可以使用使用实例名或者别名来构建服务了,但是请注意:一般情况下我们都建议通过ISocketManager来生成服务。

var manager = App.Make<ISocketManager>(); // 等价与: var manager = new SocketManager();

可能至此您还没有明白服务容器的意义,没关系,请继续阅读。

绑定服务 - 单例

服务容器通过Singleton方法来对服务进行单例绑定。通过 门面 和 服务容器 的结合,颠覆了传统的单例实现方式,以一种更新,更易于维护的方式来实现传统的单例模式。

App.Singleton<SocketManager>().Alias<ISocketManager>();

var manager1 = App.Make<ISocketManager>();
var manager2 = App.Make<ISocketManager>();
// manager1 == manager2

构造函数注入

在您简单的Make()操作中,服务容器已经对绑定的服务进行了一系列复杂操作(服务关系转换,服务构建,服务推测,上下文关系处理,依赖注入,服务修饰,单例化…),这一切对于开发者的您来说完全是透明的无感知的。

服务容器能够自动识别您构造函数中的参数,并为其注入合适的服务实例。注入方案请参考:依赖注入规则

注意,只有构造函数为public才能够被注入

public class CustomizeService
{
public Service(IFileSystem fileSystem)
{
// fileSystem 会被自动注入实现

// 如果您没有在服务容器中绑定过 IFileSystem 服务
// 那么会抛出一个UnresolvableException异常
}
}

简而言之,您只需要向服务容器绑定好您开发的服务,服务容器将会在您生成服务时自动处理好您的服务依赖关系。

属性选择器注入

服务容器采用特性标记的方法来识别哪些属性选择器是允许被注入的。

任何可以被注入的属性选择器必须满足以下规则:

public class CustomizeService
{
[Inject]
public IFileSystem FileSystem { get; set; }
}

注意,您不能在构造函数中访问被标记为注入的属性选择器的实例,因为属性选择器的注入流程会在构造函数之后进行,如果您在构造函数中访问了被标记为注入的属性选择器,这将会导致一个NullReferenceException异常。

服务别名

服务别名是服务容器非常重要的一项功能,它可以提供:接口绑定指定实现的能力。

App.Singleton<SocketManager>().Alias<ISocketManager>();

通过App.Make<ISocketManager>()您可以获得SocketManager的实现,由于您面向接口编程,无需关注底层实现,从而服务可以实现无感知替换。

服务构建事件

服务容器在每一次新构建对象时都会触发服务构建的事件,可以使用OnResolving()方法监听该事件。

该事件可以进行对被构建服务的修饰处理,允许你在对象被传递给开发者之前为其设置额外属性。

构建事件分为2种类型,局部构建事件将会优于全局构建事件被调用,构建事件的特性如下:

基于服务的构建事件

App.Singleton<SocketManager>().OnResolving((binder, instance) =>
{
var socketManager = (SocketManager)instance;
//todo: 对于SocketManager的实例进行修饰
return instance;
});

基于全局的构建事件

App.OnResolving((binder, instance) =>
{
//todo: 对于所有被构建的服务进行修饰
return instance;
});

服务释放事件

容器允许您使用Release方法为已经生成的静态服务进行释放。

App.Release<ISocketManager>();

在释放静态服务后会触发服务释放事件。在释放事件中,您可以对服务进行最后的处理(如:资源释放)。请注意:通过实例绑定的服务被释放并不会触发这个事件。

释放事件分为2种类型,局部释放事件将会优于全局释放事件被调用,您可以使用OnRelease()方法监听该事件。以下行为都可能引发释放事件:

基于服务的释放事件

App.Singleton<SocketManager>().OnRelease((binder, instance) =>
{
//todo: 对于SocketManager的静态实例释放时
});

基于全局服务的释放事件

App.OnRelease((binder, instance) =>
{
//todo: 任何被释放的静态实例都会触发
return instance;
});

重定义事件

当服务实现发生变化时将会触发重定义事件,通过关注这个事件将可以获得服务的变化,重定义事件对单例绑定和实例绑定的服务均有效,下面这些行为将会触发重定义事件:

下面行为将不会引发重定义事件:

您可以通过 OnRebound 或者 Watch 方法对服务重定义进行关注,Watch方法为OnRebound的扩展别名方法。

App.Watch<ISocketManager>((instance)=>{
// instance 为新的服务实现
});

静态化托管

服务容器可以通过Instance托管您自己生成的实例,随后对容器该实例的调用总是返回被托管的实例。

App.Instance("socket" , new SocketManager());

需要注意的是,如果您使用了Bind为指定的服务进行实例绑定,那么如果您尝试将这个服务静态化,服务容器将会抛出一个RuntimeException异常:

App.Bind("socket", (container, userParams)=> new SocketManager());
App.Instance("socket" , new SocketManager()); // throw RuntimeException

绑定函数

服务容器除了可以为服务进行绑定外还可以进行函数绑定,您可以通过BindMethod方法来为函数进行绑定,服务容器会自动对绑定函数所需求的参数进行分析,并提供合适的注入参数。

App.BindMethod("CustomizeFunction" , ()=>{
// todo: 你的函数行为
});

如果您需要为绑定的函数参数进行上下文绑定,您可以这么做:

var binder = App.BindMethod("CustomizeFunction" , ()=>{ });
binder.Needs<INetwork>.Given<ISocketManager>();

当函数参数需求一个INetwork类型服务时将会将服务重定向为ISocketManager

调用绑定函数

通过Invoke方法可以调用一个被绑定的服务。您可以为被调用的函数传入参数,这些参数会根据服务容器的注入规则选择合适的实例注入到函数参数中,如果函数参数存在不能被解决的参数,那么将会抛出一个UnresolvableException异常。

App.Invoke("CustomizeFunction", /* Your params*/);

解除绑定的函数

通过UnbindMethod方法可以解除一个绑定的方法,该方法支持以下几种解除方式:

App.UnbindMethod("CustomizeFunction");

依赖注入调用

通过Call方法可以对任何函数或者Lambda发起依赖注入调用,容器将会根据规则自动解决函数所需求的参数。

App.Call(()=>{
// todo
});

上下文绑定

有时侯我们可能有两个服务使用同一个接口,但我们希望在每个服务中注入不同实现, 我们可以通过上下文绑定来解决这个问题。

App.Singleton<FileLog>().Alias("log.file");
App.Singleton<DatabaseLog>().Alias("log.database");

(FileLogDatabaseLog均实现了ILog接口)

App.Bind<LogServiceDatabase>().Needs<ILog>().Given("log.database");
App.Bind<LogServiceFile>().Needs<ILog>().Given("log.file");

这样当生成LogServiceDatabase服务时给定的ILog实例将会是DatabaseLogLogServiceFile服务时给定的ILog实例将会是FileLog

除此以外我们还可以使用[Inject]标记来对注入实例指定实现,一般情况下服务容器的注入上下文绑定功能是使用就近原则的(除非您对别名做了额外的上下文定义,这种罕见情况不在这份基础文档的描述范围之内)。

public class LogService
{
[Inject("log.database")]
public ILog Log { get; set; }
public LogService([Inject("log.file")]ILog logWithFile)
{
}
}

根据参数名进行推导

CatLib支持对服务进行参数名推导,这要求服务注册的别名以@开头,且大小写完全匹配。

App.Singleton<LogFile>().Alias("@logFile");
App.Singleton<LogDatabase>().Alias("@logDatabase");
public class LogService
{
public LogService(ILog logFile)
{
// logFile = LogFile实例
}
}

服务编组

您可以为多个服务进行编组,编组后将允许您一次生成多个服务。这在一些场景中是非常有用的比如:日志将会被记录到文件日志服务和网络日志服务。

App.Singleton<FileLog>().Alias("log.file");
App.Singleton<NetworkLog>().Alias("log.network");
App.Tag("log" , "log.file" , "log.network");
var logServices = App.Tagged("log"); // object[]{ FileLog, NetworkLog }

解除服务绑定

您可以通过UnBind()来对已经绑定的服务进行解除绑定。如果被绑定的类是静态的,那么解除绑定会自动释放已经被生成的静态实例,同时触发OnRelease()释放事件。

var binder = App.Singleton<SocketManager>();
binder.UnBind();

当查询类型时

当容器尝试去推测一个没有被绑定过的服务时,会通过遍历OnFindType()注册的查询函数来查询服务的Type

一般情况下,这种使用场景多数用于跨程序集的Type查找。

App.OnFindType((finder)=> { return Type.GetType(finder); } , 200);
App.OnFindType((finder)=> { return MyType.GetType(finder); } , 100);

OnFindType() 允许传入一个优先级,优先级高的会被优先调用。

注意,当生成的服务依赖于OnFindType()来推测服务时,服务构建将会使用c#的反射服务,这一过程对性能的损耗非常严重。所以您应该尽可能使用静态绑定而不是依赖推测来动态生成。

暂时性托管

服务容器允许您使用Flash方法来临时托管服务实例,在回调区间完成后这些临时的托管实例将会被释放。

App.Flash(()=>{

}, /* Service name */, /* Your service instance */);

可变类型

使用IVariant接口标记的类将会被容器认为是可变类型,可变类型允许容器使用开发者传入的基础类型参数进行变换(即将基础参数变化为可变类型)。

默认的基础类型参数为:Boolean,Byte,SByte,Int16,Int32,UInt32,Int64,IntPtr,UIntPtr,Char,Double,Single,String.

public class ItemModel : IVariant
{
public int itemId;
public ItemModel(int itemId)
{
this.itemId = itemId;
}
}
public class ItemUI
{
public string name;
public ItemModel model;
public ItemUI(string name, ItemModel model)
{
this.name = name;
this.model = model;
}
}
var ui = container.Make<ItemUI>("道具详情", 10);
// ui.name : 道具详情
// ui.model.itemId : 10

重置服务容器

您可以使用Flush来重置服务容器,清除服务容器中的一切数据恢复到初始状态,这是一个危险操作所以我们将API隐藏在Handler中。

通过Flush重置容器的过程中,会触发OnRelease事件。

App.Handler.Flush();

依赖注入规则

其他常规函数

容器行为定制

当开发者需要深度定制容器行为时可以重构下面提供的虚方法来调整容器默认行为。容器行为定制只有您非常了解虚函数对应的行为才能操作。

性能优化相关

最优的方案是使用Facade来访问静态绑定的服务,这样您将获得和源生代码直接访问差距不大的访问性能。

一般情况下我们推荐使用静态绑定的方式来对无依赖注入的服务进行实例。以便于能够获得更好的性能。

下面的代码将进行静态绑定:

App.Singleton<SocketManager>((binder, param)=> new SocketManager());

下面的代码将进行动态绑定:

App.Singleton<SocketManager>();

除了进行静态绑定外,我们还需要尽可能将服务设置为单例绑定,服务容器对于单例服务的优化要优于实例服务。

我们已经测试了服务生成调用所花费的时间以供您参考,所有的测试均建立于:100万次服务生成测试(测试100次的平均值),和Debug编译的程序集上。

在测试环境下,原生单例模版访问的速度为:21 ms (1ms = 47619次原生调用)

下面代码是原生单例模版参考

public class TestClass
{
public TestClass(){ }
}
public class OriginalBaseFacade<TInterface> where TInterface : new()
{
private static TInterface instance;
public static TInterface Instance
{
get
{
if (instance != null){ return instance; }
return instance = new TInterface();
}
}
}
public class TestOriginalFacade : OriginalBaseFacade<TestClass>
{
}