既然是Composite Application ,毫無疑問地將涉及到“模塊(Module)”以及“模塊化(Modularity)”,今天簡單地談談Prism中的模塊,這包括:模塊化,如何在Prism中枚舉和加載模塊等等
1,模塊化
事實上“模塊化”這個標題足以讓我心驚膽戰而無法完成此篇隨筆,因為其是一個非常大的話題,並且在生活中隨處可見,如果你對此感興趣的話,不妨閱讀一下《設計規則:模塊化的力量》這本書。不過就比較狹隘地從軟件開發這個角度上講:我們通常將一個大的軟件系統按照功能劃分成若干子系統,一個子系統完成相對單一的一個功能,這可以讓模塊本身足夠的單純、自包含以及提高重用性。對於開發者而已,一個好的模塊劃分可以讓模塊更好地獨立出來以便相對獨立的開發、測試,因為往往復雜的溝通所帶來的消耗讓我們感到無窮困窘,並且,重用性也是一個非常值得注意的問題。說到重用性,我想引入一個話題是:我們知道,目前國內的大多數公司都會按照模塊化的思想將開發人員的開發責任劃分出來,開發人員D1領到模塊M1的開發任務,開發人員D2領到模塊M2的開發任務,然後各自開發去了,但從代碼的角度上,很可能M1和M2有交叉(重復)的部分,由於沒有很好的溝通導致這些本應該本重用的交叉部分沒有得到重用而是D1和D2各自開發了一套,對於這個問題,我很想了解大家的想法。
OK,繼續我們的話題,從編程的角度上講,關於模塊化,一般會有以下這些規則:
模塊對系統的其余部分而言應該是opaque(透明?不透明?)的,並應該透過Interface(接口)解析初始化
模塊不應該直接引用其他模塊或程序
模塊應該通過Service(服務)來和其他模塊進行溝通
模塊不應該去維護其依賴項,這些依賴項應該有外部提供(比如依賴注入)
模塊不應該依賴靜態方法(這會干擾測試)
模塊應該支持熱插拔(既能夠從系統中添加刪除模塊)
2,在Prism中定義模塊
從語法層面上講,在Prism中實現了IModule接口的類被稱為一個模塊,從實際應用上講我們一般會將一個模塊獨立成一個Project(項目),而在項目中我們往往會發現一個MVP模式或MVC模式的實現,注意這兩個模式中的M(模型)是Model,而語法上的模塊是Module,關於Prism中的設計模式,我會在後續隨筆中專門討論。
IModule是如下定義的:
/// <summary> /// 為部署到應用程序的模塊擬定一個契約 /// </summary> public interface IModule { /// <summary> /// 表明模塊已經被初始化 /// </summary> void Initialize(); }
其中就一個很簡單的方法Initialize(),我們會重寫這個方法來定義自己的模塊的初始化策略,而ModuleLoader在加載模塊的時候也會調用這個方法來繼續模塊的出生化。
如何向模塊注冊依賴項呢,比如我們要向模塊注冊其使用的一個服務,查看下面這段代碼:
public class NewsModule : IModule { private IUnityContainer _container; public NewsModule(IUnityContainer container) { _container = container; } public void Initialize() { RegisterViewsAndServices(); } protected void RegisterViewsAndServices() { _container.RegisterType<INewsFeedService, NewsFeedService>(new ContainerControlledLifetimeManager()); } }
我們知道,要注冊得首先拿到依賴注入容器,上面的代碼中我們直接添加了一個構造參數IUnityContainer container,然後到模塊構造函數被調用了,依賴注入容器就自然被設置進來了,為什麼呢?不必驚訝,實際上是一個“構造器注入”而已,當Resolve(解析)NewModule時,依賴注入容器發現其需要一個IUnityContainer,那麼其會先去解析IUnityContainer,不過前提是IUnityContainer已經在容器中注冊,然後再將解析出來的IUnityContainer實例注入到NewsModule中。當拿到依賴注入容器以後,向其中注冊服務就很Easy了。同理,你需要的其他東西,比如IRegionManager,也可以通過這種方式得到,當我們拿到IRegionManager後就可以向指定的Region中添加我們模塊的View了,以便將我們的模塊在界面上顯示出來。
更多的,關於依賴注入可以參考這裡深入 Unity 1.x 依賴注入容器之四:依賴注入
3,模塊的加載
關於模塊的加載,在本系列隨筆的前幾篇中或多或少地提到過一下,你可以回頭去參考一下,這裡我們主要看看從代碼層面如何進行模塊的加載
3.1 靜態模塊加載
如果采用這中方法的話,我們需要做的便是在重寫Bootstrapper的GetModuleEnumerator()方法時將需要加載的模塊添加到靜態模塊枚舉器中就可以了,Like this:
public class MyBootstrapper : UnityBootstrapper { protected override IModuleEnumerator GetModuleEnumerator() { return new StaticModuleEnumerator() .AddModule(typeof (MyModule1)) .AddModule(typeof (MyModule2)); } }
但,這所帶來的弊端比較多,首先我們Bootstrapper所在的項目(Shell項目)要引用所有需要加載的模塊,其次,這些模塊都會在項目啟動的時候被加載(如果是動態加載的話,可以做到“按需”加載),再者,模塊的替換、添加、刪除都需要重新編譯項目,等等...
3.2 動態加載模塊 之 掃描文件夾
其通過查找指定路徑下的程序集中實現了“Microsoft.Practices.Composite.Modularity.IModule”接口的類型來作為模塊並加載進來。我們知道,如果兩個模塊之間存在著依賴關系的話,那麼被依賴方要優先加載,而掃描文件夾時沒有任何這方面的信息,所以你需要在模塊定義時附加一下Attribute信息來說明這一點,不過要注意不要形成循環依賴(依賴關系成環狀)
[Module(ModuleName = "ModuleA")] public class ModuleA : IModule { … } [Module(ModuleName = "ModuleB")] [ModuleDependency("ModuleA")] public class ModuleB : IModule { … }
采用這種方式時,我們需要重寫Bootstrapper的GetModuleEnumerator()方法並將模塊枚舉器指定成為一個DirectoryLookupModuleEnumerator,並指定模塊查找目錄:
public class MyBootstrapper : UnityBootstrapper { protected override IModuleEnumerator GetModuleEnumerator() { returnnew DirectoryLookupModuleEnumerator(@".\Modules"); } }
3.3 動態加載模塊 之 根據配置文件加載
與DirectoryLookupModuleEnumerator不同的是,其是通過解析指定目錄下的(或應用程序根目錄下的)*.config文件來取得模塊並加載進來。其會查找配置文件中的modules樹下的每一個module節並將其指定的模塊加載進來,不過在加載前其會先加載其所依賴的模塊(dependency )
<modules> <module assemblyFile="Modules/ModuleA.dll" moduleType="ModuleA.ModuleA" moduleName="ModuleA"> … </module> <module assemblyFile="Modules/ModuleB.dll" moduleType="ModuleB.ModuleB" moduleName="ModuleB"> <dependencies> <dependency moduleName="ModuleA"/> </dependencies> </module> … </modules>
這樣按配置加載的好處非常明顯,我們可以改改配置文件便可以實現模塊的添加刪除和替換而無需編譯代碼
3.4 動態加載模塊 之 “按需”加載
事實上無論是3.2還是3.3所說的那一種加載方式,其更多地側重點在模塊的枚舉方式(查找該模塊的方式),無論是掃描文件夾或是根據配置文件,其所枚舉到的模塊並不一定都是在應用程序啟動時加載的,雖然默認情況下是這樣,但我們可以指定ModuleAttribute的StartupLoaded屬性或者更改配置文件中的startupLoaded屬性來指定模塊是否是在程序啟動時進行加載。但注意,如果是使用StaticModuleEnumerator進行靜態加載的話,則是所有加載的模塊都是在啟動是進行加載的。
比如:
<modules> <module assemblyFile="Modules/ModuleC.dll" moduleType="ModuleC.ModuleC" moduleName="ModuleC" startupLoaded="false"/> </modules>
那麼其僅僅會被添加到模塊枚舉器的Modules列表中,但不會在StartupModules列表中。
在運行時,我們確實需要系統加載該模塊時我們可以以編程方式手動加載:
private void btnAddModule_Click(object sender, RoutedEventArgs e) { moduleLoader.Initialize(moduleEnumerator.GetModule("ModuleC")); }
這所帶來一個顯而易見的好處是降低應用程序啟動時間,以及降低空間消耗(如果用戶一直不使用該模塊的功能就完全沒必要加載)
4,其他
“找不到模塊”?
我們知道,啟動項目一般在Shell項目中,而各個模塊是在各自的項目中,如果Shell項目沒有引用到其他項目的話(比如掃描文件夾和按配置文件加載這兩種方式的Shell便不會引用到其他模塊),那麼默認情況下,編譯出來的模塊程序集和Shell項目程序集便不在同一個目錄下,所以Shell項目運行時便找不到模塊,那麼很簡單,你可以手動地將模塊程序集拷貝到Shell項目的Debug或Release目錄下,也可以修改模塊項目屬性Post-build event command line 中添加一條XCopy語句讓其每次Build成功以後自動拷貝到Shell項目的Debug或Release目錄下:
xcopy "$(TargetDir)*.*" "$(SolutionDir)ConfigurationModularity\bin\$(ConfigurationName)\Modules\" /Y
其中:
/E - copy all subfolders /Y - don't prompt to overwrite older files /C - continue copying after error /V - verify files after copy /R - overwrite read-only files /Z - copy in restartable mode