在C#中使用裝飾器模式和擴展方法實現Fluent Interface


寫在前面:之前,我有發布一篇題為《暫別博客園》的文章,在發布之后,得到了很多讀者朋友的反饋意見,很多朋友希望我能夠繼續在博客園中撰寫文章,綜合考慮,我仍打算繼續在博客園發表文章。同時會將文章收集到我的個人站點apworks.org上,歡迎讀者朋友參閱。

【注:本文已被收錄到MSDN,詳細地址:http://msdn.microsoft.com/zh-cn/library/739776d1-50e8-47c1-a7c5-008cad2fe14a

背景知識

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));

這段代碼不僅看起來非常清晰,而且在編寫的時候也更符合人腦的思維方式,通過這些連續的方法調用,我們首先從列表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
}

於是,對IService實例的配置就變得非常簡單,比如:

IService aService = new Service();
aService.UseCache(new AppfabricCache()).UseLogger(new ConsoleLogger());

這是最簡單的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();
}

為了實現的方便,我們還將引入一個抽象類,該抽象類實現了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();
}

接下來就是針對不同的配置環節實現各自的配置器了。我們以緩存機制的配置為例,簡要介紹一下“緩存配置器”的實現方式。

先定義一個名為ICacheConfigurator的接口,該接口實現了IConfigurator的接口,但它是一個空接口,並不包含任何屬性、事件或方法的接口定義。引入這個接口的目的就是要在接下來的擴展方法定義中能夠實現面向該接口的方法擴展,於是上文中討論的第二個問題就能引刃而解,這將在接下來的“擴展方法的引入”部分進行討論。事實上在很多成熟的應用程序和框架中也有類似的設計,比如將接口用作泛型約束類型等。因此,ICacheConfigurator的實現代碼非常簡單:

public interface ICacheConfigurator : IConfigurator
{
}

而作為“緩存配置器”而言,它只需要繼承於Configurator類並實現ICacheConfigurator接口就可以了,代碼如下:

public class CacheConfigurator<TCache> : Configurator, 
    ICacheConfigurator
    where TCache : ICache
{

    public CacheConfigurator(IConfigurator configurator)
        : base(configurator)
    {
    }

    public override IUnityContainer Configure()
    {
        var container = this.Context.Configure();
        container.RegisterType<ICache, TCache>();
        return container;
    }
}

從上面的代碼中可以看到,TCache約束於ICache接口類型,而在Configure方法中,首先調用配置上下文(也就是配置器本身所包含的上一層配置器實例)的Configure方法,同時獲得已配置的Unity IoC容器實例container,之后在container上繼續調用RegisterType方法,將給定的緩存機制實現類型注冊到container中,最后將container返回給調用者。

整個配置器部分的實現,可以用下面的類圖進行總結:

1

擴展方法的引入

前面已經提到過,擴展方法可以將職責無關的方法定義從類型中移出,並在一個靜態類中進行集中實現。在目前的這個例子中,擴展方法還能夠幫助我們將類型繼承的層次結構“扁平化”,使得Fluent Interface中各方法的銜接邏輯變得更加清晰。仍然以緩存配置部分為例,假設我們希望在獲得了服務的配置之后,能夠接着對緩存機制進行配置,在完成了緩存機制的配置后,才能開始對日志記錄機制進行配置,那么我們就可以定義擴展方法如下:

public static ICacheConfigurator WithDictionaryCache(this IServiceConfigurator configurator)
{
    return new CacheConfigurator<DictionaryCache>(configurator);
}
public static ILoggerConfigurator WithConsoleLogger(this ICacheConfigurator configurator)
{
    return new LoggerConfigurator<ConsoleLogger>(configurator);
}

上面的WithDictionaryCache方法表示需要在Service的配置上采用基於字典的緩存機制,而WithConsoleLogger則表示在緩存配置的基礎上,還需要選用控制台作為日志記錄機制。

從上面的代碼中我們還能了解到,擴展方法還能夠很直觀地定義各種配置之間的先后順序,更改起來也非常方便。例如,如果緩存機制和日志記錄機制的配置沒有一個前后關系的話,那么我們可以將IServiceConfigurator作為WithConsoleLogger的第一個參數類型,而無需去修改代碼中的其它任何部分。

接下來要做的,就是設計一個工廠類,使其能夠根據我們的配置信息創建一個新的IService實例。

工廠類的實現

工廠類的實現就非常簡單了,同樣使用擴展方法,對IConfigurator類型進行擴展,在獲得了Unity IoC容器的實例之后,只需要調用Resolve方法直接返回IService類型的實現類型就可以了。Resolve方法的使用,直接解決了上文中提到的第三個問題。工廠類的代碼如下:

public static class ServiceFactory
{
    public static IToConfigConfigurator ToConfig()
    {
        return new ToConfigConfigurator();
    }

    public static IService Create()
    {
        return ToConfig().Service().Create();
    }

    public static IService Create(this IConfigurator configurator)
    {
        var container = configurator.Configure();
        if (!container.IsRegistered<ICache>())
            container.RegisterType<ICache, DictionaryCache>();
        if (!container.IsRegistered<ILogger>())
            container.RegisterType<ILogger, ConsoleLogger>();
        if (!container.IsRegistered<IService>())
            container.RegisterType<IService, Service>();
        return container.Resolve<IService>();
    }
}

測試

創建一個測試項目以便對我們所做的工作進行測試,比如下面的測試方法將會對IService的實現所采用的緩存機制類型和日志記錄機制類型進行測試:

[TestMethod]
public void UseAppfabricCacheAndDatabaseLoggerTest()
{
    var service = ServiceFactory
        .ToConfig()
        .Service()
        .WithAppfabricCache()
        .WithDatabaseLogger()
        .Create();
    Assert.IsInstanceOfType(service.Cache, typeof(AppfabricCache));
    Assert.IsInstanceOfType(service.Logger, typeof(DatabaseLogger));
}

現在我們已經可以使用Fluent Interface對IService實例的初始化過程進行配置了。Fluent Interface的引入,更像是在使用一種自然語言對配置過程進行表述:Service factory, to config (the) service with Appfabric Cache (mechanism) (and) with Database Logger (mechanism)。

總結

本文首先介紹了Fluent Interface的相關知識,並給出了一種簡單的實現方式。通過對簡單實現方式的討論,引出了可能存在的設計問題,進而選擇了一種更為合理的實現方式,即通過使用裝飾器模式和C#的擴展方法特性來實現Fluent Interface。這種全新的實現方式不僅能夠解決所討論的設計問題,而且這種面向對象的設計方式還為Fluent Interface的實現帶來了一定的可擴展性。文章最后對這種實現方式進行了簡單測試,同時也展示了Fluent Interface在實際中的應用。

源代碼

本文所討論的案例源代碼可以在http://sdrv.ms/SxRKqG 站點下載。

apworks.org站點本文鏈接地址:http://apworks.org/?p=334


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM