Microsoft .NET為應用程式開發人員提供了豐富的處理配置資料的編程模型與類型庫。擁有這些組件,開發人員及使用者可以方便地在無需重新編譯應用程式的情形下,通過對配置資料的設定,對應用程式的執行行為與結果進行幹預,從而使得相同的應用程式能夠在不改變來源程式的情況下,滿足不同應用情境的特殊需求。就應用程式架構而言,在大多數情況下,開發人員也需要能夠對其進行配置,以便獲得不同的架構功能特性。比如,我們會在.NET應用程式的設定檔中加入對NHibernate架構的配置資料,如果應用程式還用到了Microsoft Patterns & Practices Enterprise Library(EntLib)的話,還需要加入對EntLib的配置資料,以使得各種不同的架構能夠滿足應用程式正常啟動並執行需要。因此,對配置資料的讀取、管理和使用,是每個應用程式架構所必備的功能。
在此我們不打算進一步細究基於Microsoft .NET的應用程式是如何讀取和管理配置資料的。做過.NET應用程式開發的讀者朋友都知道,配置資料都是寫在應用程式的設定檔裡的,比如app.config或者web.config,然後使用System.Configuration命名空間下提供的類來讀取設定檔以獲得配置資料;也可以使用ConfigurationSection、ConfigurationElement等類來定義應用程式自己的配置資料結構。有關這部分內容請讀者朋友自己參閱相關文檔或者網頁資料。需要說明的是,app.config也好,web.config也好,都是基於AppDomain的,具體表現是:在可執行應用程式中,app.config以.exe.config或者.dll.config的副檔名形式,與主程式並存在同一個目錄下;在Web應用程式中,web.config則在虛擬目錄的根目錄下。當然,這些都是一般情況,事實上.NET是允許開發人員改變app.config或者web.config的檔案名稱的。
現在,讓我們來簡單地考慮一下單體測試載入器的實現,這是件非常有趣的事情。最簡單的方式就是,在這個單體測試載入器中提供一些用於指定測試類型和測試方法的Attributes,比如類似Microsoft Visual Studio 2010單體測試架構中的TestClassAttribute、TestInitializeAttribute、TestMethodAttribute等,然後建立一個用於單體測試的Class Library,在這個Class Library中使用這些Attributes定義測試類型以及測試方法。接著,你會使用一個基於Windows Forms或者WPF的測試載入器來載入這個Class Library,通過.NET反射技術讀取Class Library中所有的測試類型與測試方法並執行這些方法。在執行的過程中,使用try…catch捕獲來自Assert的異常以此判斷單體測試是否執行通過。這個過程看似簡單,但實際上還是有不少細節是需要仔細琢磨的,其中最重要的一個問題就是設定檔。如果你需要測試的僅僅是一些數值運算或者演算法,那恭喜你,你無須為這個設定檔的問題而煩惱;但如果你需要測試的是一個應用程式架構,而這個架構的運行是需要依賴一些配置資料的,那你就會頭疼了:我應該把這些配置資料寫在哪裡?是寫在這個測試載入器的設定檔裡嗎?肯定不合理,否則每次做測試前就需要對測試載入器的設定檔進行修改;那我應該寫在Class Library的設定檔裡?對不起,這樣做不奏效,因為測試載入器會將Class Library載入到自己的AppDomain中,於是Class Library就無法讀取它自己的設定檔了。
要解決這個問題,就需要變更測試載入器對Class Library的載入方式,由簡單的Assembly.Load方式轉換成將其載入到另一個AppDomain中。在建立這個新的AppDomain時,使用AppDomainSetup.ConfigurationFile來設定設定檔,類似代碼如下:
private void LoadRemoteProxy(){ AppDomainSetup ads = new AppDomainSetup(); ads.ApplicationBase = Path.GetDirectoryName(this.AssemblyFile); ads.ApplicationName = Path.GetFileName(this.AssemblyFile); string configFileName = this.AssemblyFile + ".config"; if (File.Exists(configFileName)) { ads.ConfigurationFile = configFileName; } ads.DisallowBindingRedirects = false; ads.DisallowCodeDownload = true; ads.ShadowCopyFiles = "true"; ads.ShadowCopyDirectories = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache); clientDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, ads); object[] args = new object[]{ this.AssemblyFile, typeof(TestClassAttribute), typeof(ClassInitializeAttribute), typeof(ClassCleanupAttribute), typeof(TestInitializeAttribute), typeof(TestCleanupAttribute), typeof(TestMethodAttribute) }; IRemoteProxy p = (IRemoteProxy)clientDomain .CreateInstanceAndUnwrap( typeof(IRemoteProxy).Assembly.FullName, "VisualBenchmark.Proxies.RemoteProxy", false, 0, null, args, null, null); if (p != null) { this.AssemblyName = p.AssemblyName; this.Proxy = p; }}
這段代碼來自之前我所寫的一個基於單體測試的效能比較工具Visual Benchmark。該工具的首頁地址是:http://visualbenchmark.codeplex.com。有興趣的讀者可以在該首頁上獲得原始碼以進一步參考學習。
在此我們討論的重點並不是這個單體測試載入器,而是那個被測試的架構。通過將這個被測試的架構載入到單獨的AppDomain,我們可以使其在被測試的過程中成功地讀取配置資料。然而問題又來了:如果我們需要在同一次測試中,檢驗被測架構對不同的配置資料所產生的不同行為,而設定檔卻又只能有一個,那麼這樣的測試需求又如何?呢?解決該問題的答案就是:我們應該為應用程式架構提供多樣化的配置方式,而不僅僅是對設定檔的支援。
設計
在使用.NET技術開發應用程式架構的時候,我們通常會設計一種配置資料結構,在代碼中使用繼承於ConfigurationSection、ConfigurationElement以及ConfigurationElementCollection的類來表示這樣的資料結構,之後,在應用程式的設定檔中,就可以很方便地使用與之相對應的XML標記(Tags)來表示配置資料了。這些內容對於一個資深的.NET開發人員來說,應該是非常熟悉的。可以說,.NET中的設定檔是一種最基本最常見的配置資料提供方案,於是,當我們希望為應用程式架構提供多樣化的配置方式時,這種基於設定檔的方式就成為了其中一種必不可少的選擇。除此之外,我們還可以根據架構本身的特性,提供諸如基於其它XML檔案、基於資料庫或者直接代碼編寫的配置方式。
為了能讓架構同時支援設定檔以及其它的配置方式,在設計上就需要將這些不同的配置方式統一起來。上面也分析過,設定檔的方式是必不可少的,因此,我們可以讓這些方式對架構透明,而使得架構僅感知到當前只有設定檔這樣一種方式。換句話說,在架構的實現過程中,當需要用到配置資料時,架構代碼僅會使用到那些繼承於ConfigurationSection、ConfigurationElement以及ConfigurationElementCollection的類型。
首先,定義一個配置源(Configuration Source)介面,該配置源介面會對配置節(Configuration Section)進行封裝,由於配置節包含了對其它配置元素(Configuration Element)和配置元素集(Configuration Element Collection)的引用,因此,配置節實際上是整個配置資訊彙總的彙總根。然後,針對不同的配置方式,建立實現配置源介面的類,並在這些類中以不同的方式初始化配置節對象,比如可以通過System.Configuration.ConfigurationManager來讀取設定檔中的配置節,或者通過訪問資料庫來獲得配置資料等。最後,架構僅需以依賴注入等方式獲得配置源的具體實現,即可獲得配置資料。下面的類圖展示了這種設計的參與者及其之間的關係,為了簡化描述,該類圖僅包含了基於設定檔和基於原始碼編寫的兩種配置實現方式。
中的IConfigurationSource介面就是配置源介面,ConfigFileConfigurationSource和RegularConfigurationSource是實現了該介面的兩個類,ConfigFileConfigurationSource類通過使用標準的.NET配置系統以從設定檔中讀入配置節;而RegularConfigurationSource則向調用方提供了SetElement、AddElement等公有方法,以便開發人員可以直接在代碼中調用這些方法來向架構提供配置資料。應用程式架構的Application類會在建構函式中接收IConfigurationSource的具體實現以便初始化其ConfigSource屬性。ApplicationFactory類是一個單例類(Singleton),它的CreateApplication靜態方法同樣接收IConfigurationSource的具體實現以便建立Application執行個體,與此同時,ApplicationFactory會向外公開CurrentApplication屬性,以便在架構的任何地方都能夠獲得當前啟動並執行Application執行個體,進而獲得定義在Application執行個體中的配置源。
採用這種設計,要為架構提供新的配置方式就變得輕而易舉了。例如,假設我們希望架構還能夠從資料庫讀取配置資料,那麼我們只需要建立一個DatabaseConfigurationSource的類,使其實現IConfigurationSource介面,而在這個類中,通過訪問資料庫來設定Config屬性即可。
實現
假設某架構的配置節定義如下:
using System.Configuration;public class ApplicationElement : ConfigurationElement { [ConfigurationProperty("provider", IsKey=true, IsRequired=true)] public string Provider { get { return (string)base["provider"]; } set { base["provider"] = value; } }}public class ObjectContainerElement : ConfigurationElement { [ConfigurationProperty("provider", IsKey=true, IsRequired=true)] public string Provider { get { return (string)base["provider"]; } set { base["provider"] = value; } }}[ConfigurationCollection(typeof(ObjectContainerElement), AddItemName="objectContainer", CollectionType=ConfigurationElementCollectionType.BasicMap)]public class ObjectContainerElementCollection : ConfigurationElementCollection { protected override string ElementName { get { return "objectContainer"; } } public override ConfigurationElementCollectionType CollectionType { get { return ConfigurationElementCollectionType.BasicMap; } } protected override ConfigurationElement CreateNewElement() { return new ObjectContainerElement(); } protected override object GetElementKey(ConfigurationElement element) { return (element as ObjectContainerElement).Provider; } public void Add(ObjectContainerElement element) { this.BaseAdd(element); }}public class FrameworkConfigSection : ConfigurationSection { [ConfigurationProperty("application", IsRequired=true)] public ApplicationElement Application { get { return (ApplicationElement)base["application"]; } set { base["application"] = value; } } [ConfigurationProperty("objectContainers")] public ObjectContainerElementCollection ObjectContainers { get { return (ObjectContainerElementCollection)base["objectContainers"]; } set { base["objectContainers"] = value; } }}
根據上面的設計分析,配置源的實現如下:
public interface IConfigurationSource { FrameworkConfigSection Config { get; }}public class ConfigFileConfigurationSource : IConfigurationSource { private FrameworkConfigSection config; public ConfigFileConfigurationSource() { this.ReadFromConfigFile(); } private void ReadFromConfigFile() { this.config = (FrameworkConfigSection) ConfigurationManager.GetSection("frameworkConfig"); } public FrameworkConfigSection Config { get { return this.config; } }}public class RegularConfigurationSource : IConfigurationSource { private FrameworkConfigSection config; public RegularConfigurationSource() { config = new FrameworkConfigSection(); config.Application = new ApplicationElement(); config.ObjectContainers = new ObjectContainerElementCollection(); } public void AddElement(ObjectContainerElement element) { config.ObjectContainers.Add(element); } public void SetElement(ApplicationElement element) { config.Application = element; } public void AddObjectContainer(string provider) { config.ObjectContainers.Add(new ObjectContainerElement { Provider = provider }); } public void SetApplication(string provider) { config.Application = new ApplicationElement { Provider = provider }; } public FrameworkConfigSection Config { get { return this.config; } }}
在架構中,通常需要一個引導器(BootStrapper)來啟動一些架構所依賴的服務或者進行一些資料準備工作。為了簡化描述,在此我們僅用上面實現部分介紹的Application以及ApplicationFactory來類比引導器的這部分功能。首先定義IApplication介面,然後建立一個實現該介面的類Application:
public interface IApplication { IConfigurationSource ConfigSource { get; }}public class Application : IApplication { private readonly IConfigurationSource configSource; public Application(IConfigurationSource configSource) { this.configSource = configSource; } public IConfigurationSource ConfigSource { get { return this.configSource; } }}
最後,ApplicationFactory單件類(Singleton)會通過CreateApplication方法建立IApplication的執行個體,並將已建立的執行個體返回給調用者,同時保持對已建立執行個體的引用,以便在架構中任何地方都能夠通過ApplicationFactory單件來獲得當前執行的IApplication執行個體。
public class ApplicationFactory { private static readonly ApplicationFactory instance = new ApplicationFactory(); private IApplication currentApplication; private static readonly object lockObj = new object(); private ApplicationFactory() { } public static ApplicationFactory Instance { get { return instance; } } public static IApplication CreateApplication(IConfigurationSource source) { lock(lockObj) { if (instance.currentApplication == null) { lock(lockObj) { instance.currentApplication = new Application(source); } } return instance.currentApplication; } } public IApplication CurrentApplication { get { return currentApplication; } }}
以上是架構中對多樣化配置方式支援的實現部分。在架構的其它部分,則可以使用下面的方式獲得所需的配置資訊:
// 架構初始化時,可以用以下的方式建立IApplication執行個體。比如可以在控制台// 應用程式或Windows Forms應用程式的Main函數,或者Web應用程式的Global.asax// 中執行這部分代碼:IConfigurationSource configFileConfigSource = new ConfigFileConfigurationSource(); // 使用web/app.config設定檔IApplication application = ApplicationFactory.CreateApplication(configFileConfigSource);// TODO: 在application上執行其它操作// 在架構中,使用下面的方式訪問配置資料:FrameworkConfigSection configSection = ApplicationFactory.Instance.CurrentApplication.ConfigSource.Config;// TODO: 使用configSection擷取配置資料
當然,我們完全可以不依賴於app/web.config設定檔,而直接使用上面定義的RegularConfigurationSource,以編寫代碼的方式向架構提供配置資料。這種方式不僅能解決上面提到的架構測試問題,而且還能為開發人員提供強型別和智能感知的支援。比如:
IConfigurationSource regularSource = new RegularConfigurationSource();regularSource.SetApplication(typeof(Application).AssemblyQualifiedName);regularSource.AddObjectContainer(typeof(UnityContainer).AssemblyQualifiedName);IApplication application = ApplicationFactory.CreateApplication(regularSource);
面向領特定領域語言的配置開發模式
通過上面的分析不難得知,要實現多樣化的配置方式,無論是基於設定檔的,還是採用其它的實現方式,都離不開配置節(Configuration Section)以及配置資料結構的開發和維護。對於小型的應用程式架構,通常可以使用手工的方式來編寫這些配置代碼;但對於中大型應用程式架構,為了提供強大的開發能力,配置節和配置資料結構往往比較複雜,此時使用手工方式來維護配置代碼就顯得費時費力了。為了能夠有效地維護這些配置代碼,我們可以採用某些領特定領域語言(Domain-Specific Language, DSL)來開發配置節和資料結構,然後通過各種代碼產生手段實現自動化代碼產生。於是,在開發應用程式架構的過程中,當需要變更配置節或配置資料結構時,開發人員就無需去面對繁雜的代碼,只需修改領特定領域語言即可,剩下的工作都可直接交給自動化代碼產生引擎去處理。在設計和開發架構的配置系統時,使用領特定領域語言的好處不僅僅在於自動化代碼產生,它還能為如下兩種應用情境提供支援:
- 在產生代碼的同時,可以產生配置資料結構的XML Schema,以便在使用Visual Studio編輯設定檔的時候使用智能感知(IntelliSense)技術
- 在產生的程式碼中加入組件模型(Component Model)相關的CLR特性,這將大大簡化設定檔編輯器的開發工作
開源社區中有一款配置節設計工具,基本上可以滿足上面兩條需求,該工具的官方地址是:http://csd.codeplex.com。它能夠同時支援 Visual Studio 2005、2008和2010等多個版本。對Visual Studio 2010的支援是以擴充包的方式實現的。在我的個人部落格中,有篇文章對該工具進行了簡要的介紹,文章地址是:http://www.cnblogs.com/daxnet/archive/2011/09/16/2178377.html,有興趣的讀者可以閱讀參考。
領特定領域語言在應用程式架構配置系統中的另一個應用就是Fluent Interface。根據《DSLs in Boo Domain Specific Languages in .NET》一書中的介紹,領特定領域語言基本上可以分為四大類:External DSL、Graphical DSL、Internal/Embedded DSL以及Fluent Interface。結合上面所述設計,在IConfigurationSource介面上應用Fluent Interface可以為應用程式架構的使用者帶來更直觀的編程體驗。現在,讓我們對上面的IConfigurationSource進行擴充,看看Fluent Interface為編程帶來的便捷。我們採用.NET 3.0所提供的擴充方法(Extension Methods)來實現這一效果。
public static class FluentInterfaceProvider{ public static IConfigurationSource SetApplication(this IConfigurationSource source, string provider) { source.Config.Application = new ApplicationElement { Provider = provider }; return source; } public static IConfigurationSource AddObjectContainer(this IConfigurationSource source, string provider) { source.Config.ObjectContainers.Add(new ObjectContainerElement { Provider = provider }); return source; }}// 在使用的時候,就可以:IConfigurationSource configSource = new RegularConfigurationSource();configSource .SetApplication(typeof(Application).AssemblyQualifiedName) .AddObjectContainer(typeof(UnityContainer).AssemblyQualifiedName);
對於領特定領域語言本身的相關知識,在此不作過多討論,我會在其它的文章中進行詳細介紹。
總結
本章節首先以一個單體測試載入器為例,論證了為應用程式架構提供多樣化的配置方式的必要性,然後針對該需求給出了一種可行的設計方案,同時以虛擬碼的方式實現了這種設計。在章節的最後,我們還討論了應用程式架構中配置系統設計與開發的最佳實務,即使用領特定領域語言來維護配置節和配置資料結構,並在使用架構的過程中,利用領特定領域語言來獲得開發上的便捷。開發應用程式架構,同時也需要為之設計一套合理的、完整的配置資料結構和配置方式,而本章節則對此給出了一種解決方案。在實際中,開發人員完全可以不必按此解決方案來實現架構的配置系統,但它作為配置系統設計的最佳實務,為開發人員帶來了一定的參考價值。