背景知识
Fluent Interface是一种通过连续的方法调用以完成特定逻辑处理的API实现方式,在代码中引入Fluent Interface不仅能够提高开发效率,而且在提高代码可读性上也有很大的帮助。从C# 3.0开始,随着扩展方法的引入,Fluent Interface也更多地被开发人员熟悉和使用。例如,当我们希望从一个整数列表中找出所有的偶数,并将这些偶数通过降序排列的方式添加到另一个列表中时,可以使用下面的代码:
i.Where(p => p % 2 == 0)
.OrderByDescending(q => q)
.ToList()
.ForEach(r => result.Add(r));
</div>
这段代码不仅看起来非常清晰,而且在编写的时候也更符合人脑的思维方式,通过这些连续的方法调用,我们首先从列表i中寻找所有的偶数,然后对这些偶数进行排序并将排序后的值逐个添加到result列表中。
在实际应用中,Fluent Interface不仅仅是使用在类似上面的查询逻辑上,而它更多地是被应用开发框架的配置功能所使用,比如在Entity Framework Code First中可以使用Fluent API对实体(Entity)和模型(Model)进行配置,此外还有流行的ORM框架NHibernate以及企业服务总线框架NServiceBus等等,都提供了类似的Fluent API,以简化框架的配置过程。这些API都是Fluent Interface的具体实现。由于Fluent Interface的方法链中各方法的名称都具有很强的描述性,而且具有单一职责的特点,所以Fluent Interface也可以看成是完成某一领域特定任务的“领域特定语言(Domain Specific Language)”,比如在上面的例子中,Fluent Interface被用于查询领域,而在Entity Framework、NHiberante和NServiceBus等框架中,它又被用于框架的配置领域。
接下来,让我们首先看一下Fluent Interface的简单实现方式,并简要地讨论一下这种实现方式的优缺点,再来了解一下一种使用装饰器(Decorator)模式和扩展接口的实现方式。
Fluent Interface的简单实现
Fluent Interface的一种简单实现就是在类型的每个方法中对传入参数进行处理,然后返回该类型本身的实例,因此,当该类型的某个方法被调用后,进而还可以连续地直接调用其它的方法而无需在调用时指定该类型的实例。现假设我们需要实现某个服务接口IService,在这个接口中,要用到一个提供缓存功能的接口ICache以及一个提供日志记录的接口ILogger,为了让IService的实例能够以Fluent Interface的方式指定自己所需要的ICache接口和ILogger接口的实例,我们可以这样定义IService接口:
public interface IService
{
ICache Cache { get; }
ILogger Logger { get; }
IService UseCache(ICache cache); // return ‘this' in implemented classes
IService UseLogger(ILogger logger); // return ‘this' in implemented classes
}
</div>
于是,对IService实例的配置就变得非常简单,比如:
IService aService = new Service();
aService.UseCache(new AppfabricCache()).UseLogger(new ConsoleLogger());
</div>
这是最简单的Fluent Interface的实现方式,对于一些简单的应用场景,使用这种简单快捷的方式的确是个不错的选择,但在体验着这种便捷的同时,我们或许还需要进行更进一步的思考:
1.直接定义在IService接口上的UseCache和UseLogger方法会破坏IService本身的单一职责性,而这又是与软件设计的思想是冲突的。到底是用哪种缓存服务和哪种日志服务,这并不是IService需要考虑的问题。当然,C#的扩展方法可以很方便地把UseCache和UseLogger等方法从IService接口中剥离出去,但更合理的做法是,使用工厂来创建IService的实例,而创建实例的依据(上下文)则应该由其它的配置信息来源提供
2.无法保证上下文的正确性。在上面的例子中,这个问题并不明显,先调用UseCache还是先调用UseLogger并不会给结果造成任何影响。但在某些应用场景中,设置的对象之间本身就存在一定的依赖关系,比如在Entity Framework Code First的Entity Type Configuration中,只有当所配置的属性是字符串的前提下,才能够进一步对该属性的最大长度、是否是Unicode等选项进行设置,否则Fluent Interface将不会提供类似的方法调用。显然目前这个简单的实现并不能满足这种需求
3.需要首先创建IService类型的实例,然后才能使用UseCache和UseLogger等方法对其进行设置,如果在实例的创建过程中存在对ICache或者ILogger的依赖的话(比如在构造函数中希望能够使用ILogger的实例写一些日志信息等),那么实现起来就会比较困难了
鉴于以上三点分析,当需要在应用程序或开发框架中更为合理地引入Fluent Interface时,上述简单的实现方式就无法满足所有需求了。为此,我采用装饰器模式,并结合C#的扩展方法特性来实现Fluent Interface,这种方式不仅能够解决上面的三种问题,而且面向对象的设计会使Fluent Interface的扩展变得更加简单。
使用装饰器模式和扩展方法实现Fluent Interface
仍然以上文中的IService接口为例,通过分析我们可以得到两个启示:首先,对于IService的实例究竟应该是采用哪种缓存机制以及哪种日志记录机制,这就是一种对IService的实例进行配置的过程;其次,这种配置过程就相当于在每个配置阶段逐渐地向已有的配置信息上添加新的信息,比如最开始创建一个空的配置信息,在第一阶段确定了所选用的缓存机制时,就会在这个空的配置信息基础上添加与缓存相关的配置信息,而在第二阶段确定了所选用的日志记录机制时,又会在前一阶段获得的配置信息基础上再添加与日志记录相关的配置信息,这个过程正好是装饰器模式的一种应用场景。最后一步就非常简单了,程序只需要根据最终得到的配置信息初始化IService接口的实例即可。为了简化实现过程,我选择Microsoft Patterns & Practices Unity Application Block的IoC容器来实现这个配置信息的管理机制。选用Unity IoC容器的好处是,对接口及其实现类型的注册并没有先后顺序的要求,IoC容器会自动分析类型之间的依赖关系并对类型进行注册。事实上在很多应用程序开发框架中,也是用这种方式在框架的配置部分实现Fluent Interface的。
装饰器模式的引入
首先我们引入“配置器”的概念,配置器的作用就是对IService实例初始化过程中的某个方面(例如缓存或者日志)进行配置,它会向调用者返回一个Unity IoC容器的实例,以便调用方能够在该配置的基础上进行其它方面的配置操作(为了简化起见,下文中所描述的“配置”仅表示选择某种特定类型的实现,而不包含其它额外的配置内容)。我们可以使用如下接口对配置器进行定义:
public interface IConfigurator
{
IUnityContainer Configure();
}
</div>
为了实现的方便,我们还将引入一个抽象类,该抽象类实现了IConfigurator接口,并将其中的Configure方法标识为抽象方法。于是,对于任何一种配置器而言,它只需要继承于该抽象类,并且重载Configure方法即可实现配置逻辑。该抽象类的定义如下:
public abstract class Configurator : IConfigurator
{
readonly IConfigurator context;
public Configurator(IConfigurator context)
{
this.context = context;
}
protected IConfigurator Context
{
get
{
return this.context;
}
}
public abstract IUnityContainer Configure();
}
</div>
接下