寫在前面:之前,我有發布一篇題為《暫別部落格園》的文章,在發布之後,得到了很多讀者朋友的反饋意見,很多朋友希望我能夠繼續在部落格園中撰寫文章,綜合考慮,我仍打算繼續在部落格園發表文章。同時會將文章收集到我的個人網站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的實現方式,對於一些簡單的應用情境,使用這種簡單快捷的方式的確是個不錯的選擇,但在體驗著這種便捷的同時,我們或許還需要進行更進一步的思考:
- 直接定義在IService介面上的UseCache和UseLogger方法會破壞IService本身的單一職責性,而這又是與軟體設計的思想是衝突的。到底是用哪種快取服務和哪種Log Service,這並不是IService需要考慮的問題。當然,C#的擴充方法可以很方便地把UseCache和UseLogger等方法從IService介面中剝離出去,但更合理的做法是,使用工廠來建立IService的執行個體,而建立執行個體的依據(上下文)則應該由其它的配置資訊來源提供
- 無法保證內容相關的正確性。在上面的例子中,這個問題並不明顯,先調用UseCache還是先調用UseLogger並不會給結果造成任何影響。但在某些應用情境中,設定的對象之間本身就存在一定的依賴關係,比如在Entity Framework Code First的Entity Type Configuration中,只有當所配置的屬性是字串的前提下,才能夠進一步對該屬性的最大長度、是否是Unicode等選項進行設定,否則Fluent Interface將不會提供類似的方法調用。顯然目前這個簡單的實現並不能滿足這種需求
- 需要首先建立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返回給調用者。
整個配置器部分的實現,可以用下面的類圖進行總結:
擴充方法的引入
前面已經提到過,擴充方法可以將職責無關的方法定義從類型中移出,並在一個靜態類中進行集中實現。在目前的這個例子中,擴充方法還能夠協助我們將類型繼承的階層“扁平化”,使得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】