当系统中的组件需要调用某一服务来完成特定的任务时,通常最简单的做法是使用new关键字来创建该服务的实例,或者通过工厂模式来解耦该组件与服务的具体实现部分,以便通过配置信息等更为灵活的方式获得该服务的实例。然而,这些做法都有着各自的弊端:
- 在组件中直接维护对服务实例的引用,会造成组件与服务之间的关联依赖,当需要替换服务的具体实现时,不得不修改组件中调用服务的部分并重新编译解决方案;即使采用工厂模式来根据配置信息动态地获得服务的实例,也无法针对不同的服务类型向组件提供一个管理服务实例的中心位置
- 由于组件与服务之间的这种关联依赖,使得项目的开发过程受到约束。在实际项目中,开发过程往往是并行的,但又不是完全同步的,比如组件的开发跟其所需要的服务的开发同时进行,但很有可能当组件需要调用服务时,服务却还没完成开发和单体测试。遇到这种问题时,通常会将组件调用服务的部分暂时空缺,待到服务完成开发和单体测试之后,将其集成到组件的代码中。但这种做法不仅费时,而且增大了出错的风险
- 针对组件的单体测试变得复杂。每当对组件进行单体测试时,不得不为其配置并运行所需要的服务,而无法使用Service Stub来解决组件与服务之间的依赖
- 在组件中可能存在多个地方需要引用服务的实例,在这种情况下,直接创建服务实例的代码会散布到整个程序中,造成一段程序存在多个副本,大大增加维护和排错成本
- 当组件需要调用多个服务时,不同服务初始化各自实例的方式又可能存在差异。开发人员不得不了解所有服务初始化的API,以便在程序中能够正确地使用这些服务
- 某些服务的初始化过程需要耗费大量资源,因此多次重复地初始化服务会大大增加系统的资源占用和性能损耗。程序中需要有一个管理服务初始化过程的机制,在统一初始化接口的同时,还需要为程序提供部分缓存功能
要解决以上问题,我们可以在应用程序中引入企业应用体系结构模式。
服务定位器(Service Locator)模式
服务定位器(Service Locator)模式是一种企业级应用程序体系结构模式,它能够为应用程序中服务的创建和初始化提供一个中心位置,并解决了上文中所提到的各种设计和开发问题。服务定位器模式主要有以下几种参与者:
服务(Service)
服务是服务定位器需要返回给调用方的具体实例。比如服务定位器可以根据调用方的需要,返回一个向控制台输出信息的服务,或者返回一个向文件系统输出信息的服务。在这种情形下,这两种服务可能会有着不同的接口:对于向控制台输出信息的服务而言,它只需要接收一个参数(即需要输出的信息)就可以完成输出任务;而对于向文件系统输出信息的服务而言,它不仅要获得待输出的信息,而且还要获得一个正确的文件名,以便将信息输出到这个文件中。
因此,在实际应用中,我们通常会为不同的服务类型设计不同的接口,而服务定位器则应该根据调用方给定的服务类型,返回相应的服务实例。
服务工厂(Service Factory)
服务工厂是工厂模式的一种实现,它的职责是创建并初始化某种类型的服务。例如,向控制台输出信息的服务,是由一个工厂创建并初始化的;而向文件系统输出信息的服务,则是由另一个工厂所创建。由此可见,不同的服务类型有其特定的服务工厂,在实际应用中,服务工厂与一个特定的服务接口所对应。
使用服务工厂不仅可以解耦服务的定义部分和具体实现部分,应用程序无需重新编译即可变更服务的不同实现方式,而且对于初始化过程需要消耗大量资源的服务而言,服务工厂还能够提供缓存功能,从而提高应用程序的性能。
Initial Context
由于不同的服务需要由不同的服务工厂创建并初始化,因此对于服务定位器来说,还需要一个特定的管理器来统一管理这些服务工厂,InitialContext就充当了这个角色。在调用方向服务定位器请求一个服务的实例时,服务定位器通过InitialContext获得服务工厂的实例,然后由服务工厂创建服务实例并返回给调用方。使用InitialContext的优点是,它简化了服务定位器的职责,并为服务工厂的管理和缓存提供了有力保障。
服务定位器
服务定位器为调用方获得所需服务的实例提供了中心位置。
模式的实现
假设某系统中存在两种类型的服务,一种服务会将调用方传递的信息输出到控制台,我们将其称为控制台输出服务;而另一种服务则会将调用方传递的信息输出到文件系统中,我们将其称为文件系统输出服务。通过分析可以了解到,这两种服务的接口是不同的:控制台输出服务只需要获得待输出的信息即可,而文件系统输出服务还需要获得一个文件名,以便将信息输出到指定的文件中。因此,针对这两种不同的服务,需要设计两种接口定义,这一点是显而易见的。更进一步,为了隐藏服务的初始化过程,并提供一定的缓存机制,我们还将针对每种服务类型(或者说每种服务接口)设计一个服务工厂,其职责在上面已经说过了,在此就不再赘述。在实际项目中,我们可以为服务工厂提供一个ServiceFactory的抽象基类,这样做的目的是为了将与诸如缓存相关的机制统一在基类中实现,而子类则直接负责服务实例的创建和初始化即可。
接下来Initial Context的实现就相对比较简单了,只需要针对不同的服务类型维护服务工厂即可。我觉得Initial Context的实现也不是必须的,对于一些简单的应用场景完全可以由Service Locator替代。但是在实现了Initial Context的设计中,Service Locator则需要将获取服务实例的工作转交给Initial Context执行。事实上IoC Containers就是Service Locator的实现,但它们往往都比较复杂,包括诸如循环引用的解析和生命周期管理等复杂功能,时间有限,我也没深入地去研究IoC框架,也不打算进一步探讨了。
根据上面的分析,Service Locator有着如下的结构:
执行过程可以用下面的UML序列图描述:
可以【】下载上述实现的源代码(思考题:请将代码中Service Locator改为泛型实现)。
Byteart Retail案例中的实现
在Byteart Retail案例中,服务定位器模式的实现已经“退化”成对现有IoC容器(Enterprise Library Unity)的封装,封装的目的是为了简化开发过程,从而为应用程序访问IoC容器提供一个中心位置。另外,这样的封装解耦了应用程序对IoC容器的依赖,比如在Byteart Retail案例的源代码中,并没有直接引用IoC容器,而是通过ServiceLocator类进行代理,这就为今后更换服务定位器的实现提供了便捷(当然一般情况下也不会去更换这种组件)。
有关ServiceLocator类的代码,请参考Byteart Retail案例的源代码中ByteartRetail.Infrastructure命名空间下的类。
参考资料
- Service Locator Pattern(Wikipedia):
- Service Locator Pattern(Core J2EE Patterns):
- Service Locator Pattern(MSDN):
- 依赖注入(Dependency Injection):
- 控制反转(Inversion of Control):