LoxodonFramework(以下简称LF)中使用服务定位器而不是单例来管理服务类。单例模式和服务定位器的区别网上有很多讨论。我这里也简单讲述一下自己的想法。
单例模式与服务定位器模式
无论是单例模式还是服务定位器其实本质上都是在处理“如何更好地管理全局变量和全局方法”的问题,介于“尽量不要使用”是几乎每个程序员在认识全局变量的时候都会受到的提醒,单例和服务定位器也是尽量少地使用才好。然而单例泛滥以至于维护困难是项目里经常出现的问题。
单例的其中一个问题在于违反了单一职责原则。它既要给一系列服务提供全局访问点,还要保证对自己的访问结果是唯一的。换句话说就是每一个单例除了要写业务逻辑之外还明确知道自己是单例。而服务定位器中服务的真正提供者只是一个普通的类,你可以对任何类使用服务定位器,该类甚至都不知道自己是服务提供者。这提供了更高一层的解耦。
第二个问题是单例的维护更加困难。比如一个Audio单例如果要反复开关log,只能写宏来处理,或者将开关放到log类中(然而说不定你只需要关闭Audio的log而其他类还需要)。而在服务定位器模式里,我们只需要建立一个继承自Audio类(或者接口)的LoggedAudio,在运行时替换原来的Audio类即可。
第三个单例的访问可能是空的,这在Unity中使用Mono单例,同时又在其他Mono脚本的Awake函数中调用该单例的情况下非常有可能出现。而我们可以事先硬编码一个空服务来防止服务获取失败。
IServiceContainer(服务容器接口)
LF中服务定位器的核心就是IServiceContainer接口。它继承自IServiceRegister和IServiceLocator,前者提供了注册服务和取消注册服务的方法声明,后者提供了定位服务的方法声明。默认的实现是ServiceContainer类。在实际使用中,服务容器是Context的构造函数的参数,如果没有自己实现的服务容器,则会默认使用ServiceContainer。而在获取服务的时候也不必先获取Container然后再调用Resolve方法,可以直接使用Context的GetService方法,它对Resolve方法进行了封装。
Register方法的泛型版并没有对泛型做约束,因此可以注册类也可以注册接口。但是根据LF的面向接口编程风格,建议只注册接口。
ServiceContainer (服务容器)
ServiceContainer使用字典来保存服务,但是并没有直接保存服务的引用,而是保存了创建服务的工厂接口(IFactory)。有两种工厂:单例工厂(SingleInstanceFactory)和泛型工厂(GenericFactory)。单例工厂直接保存服务实例,泛型工厂保存创建服务的委托实例。因此根据注册服务时调用Register形参的不同(实例还是方法),被定位的服务也不同,泛型工厂服务每次都返回一个全新的实例。我们可以把有状态的服务或者需要频繁引用的服务注册为单例服务,而把无状态或者需要每次定位都处于初始状态的服务注册为工厂。(显然服务工厂会造成额外的内存开销,由于没有实际的应用所以不好举例什么时候用合适)
字典的key默认是类名,但也可以传入自定义的名字。比如要注册实现同一接口的两个类,可以通过名字进行区分。
ServiceContainer以及IFactory实现了IDisposable接口,这意味着注册的服务可以使用非托管资源而不用担心释放问题。
IServiceBundle(服务组合包接口)
IServiceBundle是用来将一组相关服务打包处理的接口。提供了Start和Stop两个方法。当有一组服务互相关联并且都要同时打开和关闭就很适合使用IServiceBundle。实现类是抽象类AbstractServiceBundle,它要求从外部注入IServerContainer来提供服务定位器的接口。
默认提供的服务里,MVVM的核心服务就是一组通过BindingServiceBundle打包的服务。
DuplicateRegisterServiceException(重复注册服务异常)
ServiceContainer在Register方法中判断如果name存在则抛出该异常。
声明
该文章只作为本人学习框架的梳理思路用,一切以官方文档为准。如有错误还请指正。