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

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

背景知識

Fluent Interface是一種經過連續的方法調用以完成特定邏輯處理的API實現方式,在代碼中引入Fluent Interface不只可以提升開發效率,並且在提升代碼可讀性上也有很大的幫助。從C# 3.0開始,隨着擴展方法的引入,Fluent Interface也更多地被開發人員熟悉和使用。例如,當咱們但願從一個整數列表中找出全部的偶數,並將這些偶數經過降序排列的方式添加到另外一個列表中時,可使用下面的代碼:html

1緩存

2框架

3ide

4函數

i.Where(p => p % 2 == 0)測試

    .OrderByDescending(q => q)this

    .ToList()spa

    .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接口:

1

2

3

4

5

6

7

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實例的配置就變得很是簡單,好比:

1

2

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容器的實例,以便調用方可以在該配置的基礎上進行其它方面的配置操做(爲了簡化起見,下文中所描述的「配置」僅表示選擇某種特定類型的實現,而不包含其它額外的配置內容)。咱們可使用以下接口對配置器進行定義:

1

2

3

4

public interface IConfigurator

{

    IUnityContainer Configure();

}

爲了實現的方便,咱們還將引入一個抽象類,該抽象類實現了IConfigurator接口,並將其中的Configure方法標識爲抽象方法。因而,對於任何一種配置器而言,它只須要繼承於該抽象類,而且重載Configure方法便可實現配置邏輯。該抽象類的定義以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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的實現代碼很是簡單:

1

2

3

public interface ICacheConfigurator : IConfigurator

{

}

而做爲「緩存配置器」而言,它只須要繼承於Configurator類並實現ICacheConfigurator接口就能夠了,代碼以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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中各方法的銜接邏輯變得更加清晰。仍然以緩存配置部分爲例,假設咱們但願在得到了服務的配置以後,可以接着對緩存機制進行配置,在完成了緩存機制的配置後,才能開始對日誌記錄機制進行配置,那麼咱們就能夠定義擴展方法以下:

1

2

3

4

5

6

7

8

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方法的使用,直接解決了上文中提到的第三個問題。工廠類的代碼以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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的實現所採用的緩存機制類型和日誌記錄機制類型進行測試:

1

2

3

4

5

6

7

8

9

10

11

12

[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

相關文章
相關標籤/搜索