浅谈基于Prism的软件系统的架构设计
很早就想写这么一篇文章来对近几年使用Prism框架来设计软件来做一次深入的分析了,但直到最近才开始整理,说到软件系统的设计这里面有太多的学问,只有经过大量的探索才能够设计出好的软件产品,就本人的理解,一个好的软件必须有良好的设计,这其中包括:易阅读、易扩展、低耦合、模块化等等,如果你想设计一个好的系统当然还要考虑更多的方面,本篇文章主要是基于微软的Prism框架来谈一谈构建模块化、热插拔软件系统的思路,这些都是经过大量项目实战的结果,希望能够通过这篇文章的梳理能够对构建软件系统有更加深刻的理解。
首先要简单介绍一下Prism这个框架:Prism框架通过功能模块化的思想,将复杂的业务功能和UI耦合性进行分离,通过模块化,来最大限度的降低耦合性,很适合我们进行类似插件化的思想来组织系统功能,并且模块之间,通过发布和订阅事件来完成信息的通信,而且其开放性支持多种框架集成。通过这些简单的介绍就能够对此有一个简单的理解,这里面加入了两种依赖注入容器,即:Unity和MEF两种容器,在使用的时候我们首先需要确定使用何种容器,这个是第一步。第二步就是如何构建一个成熟的模块化软件,这个部分需要我们能够对整个软件系统功能上有一个合理的拆分,只有真正地完全理解整个系统才能够合理抽象Module,然后降低Module之间的耦合性。第三步就是关于模块之间是如何进行通讯的,这一部分也是非常重要的部分,今天这篇文章就以Prism的Unity依赖注入容器为例来说明如何构建模块化软件系统,同时也简要说明一下软件系统的构建思路。
这里以百度地图为例来说一下如果使用WPF+Prism的框架来设计的话,该怎样来设计,当然这里只是举一个例子,当然这篇文章不会就里面具体的代码的逻辑来进行分析,事实上我们也不清楚这个里面具体的内部实现,这里仅仅是个人的观点。
图一 百度地图主界面
注意下面所有的代码并非和上面的截图一致,截图仅供参考
如图一所示,整个界面从功能主体上区分的话,就能够很好的分成这几个部分,左侧是一个搜索区域,右边是两个功能区和一个个人信息区域,中间是地图区域,这个是我们在看完这个地图之后第一眼就能想到的使用Prism能够构建的几个模块(Modules)。在定完整个系统可以分为哪几个模块之后我们紧接着就要分析每一个模块包含哪些功能,并根据这些功能能够定义哪些接口,我们可以新建一个类库,专门用于定义整个应用程序的接口,并放在单独的类库中,比如左侧的地图搜索区域我们可以定义一个IMapSearch的接口,用于定于这个部分有哪些具体的功能,如下面代码所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows;
namespace IGIS.SDK { public delegate List<Models.SearchResult> OnMapSearchHandle(string keyword); public interface IMapSearch { void AddSearchListener(string type, OnMapSearchHandle handle); void RemoveSearchListener(string type); void ShowResults(List<Models.SearchResult> results); void ClearResults(); System.Collections.ObjectModel.ObservableCollection<Models.SearchResult> GetAllResults(); event EventHandler<string> OnSearchCompleted; event EventHandler<System.Collections.ObjectModel.ObservableCollection<Models.SearchResult>> OnClearSearchResult; event EventHandler<System.Collections.ObjectModel.ObservableCollection<Models.SearchResult>> OnExecuteMultiSelected; void ShowFloatPanel(Models.SearchResult targetResult, FrameworkElement ui); } }
这是第一步,为左侧的搜索区域定义好接口,当然模块化的设计必然包括界面和界面抽象,即WPF中的View层和ViewModel层以及Model层,我们可以单独新建一个项目(自定义控件库为佳)来单独实现这一部分的MVVM,然后生成单独的DLL供主程序去调用,比如新建一个自定义空间库命名为Map.SearchModule,然后分别设计这几个部分,这里列出部分代码仅供参考。
<UserControl x:Class="IGIS.MapSearch" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" Title="IGIS" xmlns:cvt="clr-namespace:IGIS.Utils" xmlns:gisui="clr-namespace:IGIS.UI;assembly=IGIS.UI" xmlns:region="http://www.codeplex.com/CompositeWPF" xmlns:ui="clr-namespace:X.UI;assembly=X.UI" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" d:DesignHeight="600" d:DesignWidth="1100"> <Grid> ...... </Grid> </UserControl>
当然最重要的部分代码都是在ViewModel层中去实现的,这个层必须要继承自IMapSearch这个接口,然后和View层通过DataContext绑定到一起,这样一个完整的模块化的雏形就出来了,后面还有几个重要的部分再一一讲述。
using IGIS.SDK.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Collections.ObjectModel; using X; using X.Infrastructure; namespace IGIS.ViewModels { class SearchManager : X.Infrastructure.VMBase, IGIS.SDK.IMapSearch { public SearchManager() { Search = new Microsoft.Practices.Prism.Commands.DelegateCommand(DoSearch); ClearResult = new Microsoft.Practices.Prism.Commands.DelegateCommand(DoClearResult); ShowSelected = new Microsoft.Practices.Prism.Commands.DelegateCommand(DoShowSelected); Listeners.Add(new Listener { Name = "全部", Handle = null }); } private void DoShowSelected() { if (null != OnExecuteMultiSelected) { System.Collections.ObjectModel.ObservableCollection<SearchResult> selected = new ObservableCollection<SearchResult>(); foreach (var itm in SelectedItems) { if (itm is SearchResult) selected.Add(itm as SearchResult); } OnExecuteMultiSelected(this, selected); } } private static SearchManager _instance; public static SearchManager Instance { get { if (null == _instance) _instance = new SearchManager(); return _instance; } set { _instance = value; } } private void DoSearch() { ClearResults(); foreach (var ls in Listeners) { if (string.IsNullOrEmpty(SelectedType) || SelectedType == "全部" || SelectedType == ls.Name) if (ls.Handle != null) { List<SearchResult> res = null; Application.Current.Dispatcher.Invoke(new Action(() => { res = ls.Handle.Invoke(Keyword); }), System.Windows.Threading.DispatcherPriority.Normal); if (null != res && res.Count > 0) { foreach (var itm in res) { Application.Current.Dispatcher.Invoke(new Action(() => { Results.Add(itm); })); } } } } if (null != OnSearchCompleted) OnSearchCompleted(Results, Keyword); DoRemoteSearch(SelectedType, Keyword); } private string _keyword; public string Keyword { get { return _keyword; } set { if (_keyword != value) { _keyword = value; OnPropertyChanged("Keyword"); } } } private string _selectedType = "全部"; public string SelectedType { get { return _selectedType; } set { if (_selectedType != value) { _selectedType = value; OnPropertyChanged("SelectedType"); } } } private ICommand _showSelected; public ICommand ShowSelected { get { return _showSelected; } set { _showSelected = value; } } private ICommand _search; public ICommand Search { get { return _search; } set { if (_search != value) { _search = value; OnPropertyChanged("Search"); } } } private ICommand _ClearResult; public ICommand ClearResult { get { return _ClearResult; } set { _ClearResult = value; } } private void DoClearResult() { ClearResults(); } private System.Collections.ObjectModel.ObservableCollection<SearchResult> _results = new System.Collections.ObjectModel.ObservableCollection<SearchResult>(); public System.Collections.ObjectModel.ObservableCollection<SearchResult> Results { get { return _results; } set { if (_results != value) { _results = value; OnPropertyChanged("Results"); } } } private System.Collections.IList _selectedItems; public System.Collections.IList SelectedItems { get { return _selectedItems; } set { _selectedItems = value; } } #region SDK public class Listener : X.Infrastructure.NotifyObject { private string _name; public string Name { get { return _name; } set { if (_name != value) { _name = value; OnPropertyChanged("Name"); } } } private SDK.OnMapSearchHandle _handle; public SDK.OnMapSearchHandle Handle { get { return _handle; } set { _handle = value; } } } public event EventHandler<string> OnSearchCompleted; public event EventHandler<System.Collections.ObjectModel.ObservableCollection<SDK.Models.SearchResult>> OnClearSearchResult; public event EventHandler<ObservableCollection<SearchResult>> OnExecuteMultiSelected; private System.Collections.ObjectModel.ObservableCollection<Listener> _listeners = new System.Collections.ObjectModel.ObservableCollection<Listener>(); public System.Collections.ObjectModel.ObservableCollection<Listener> Listeners { get { return _listeners; } set { if (_listeners != value) { _listeners = value; OnPropertyChanged("Listeners"); } } } public System.Collections.ObjectModel.ObservableCollection<SearchResult> GetAllResults() { return Results; } public void AddSearchListener(string type, SDK.OnMapSearchHandle handle) { Application.Current.Dispatcher.Invoke(new Action(() => { var itm = Listeners.Where(x => x.Name == type).SingleOrDefault() ?? null; if (null == itm) { itm = new Listener() { Name = type }; Listeners.Add(itm); } itm.Handle = handle; })); } public void RemoveSearchListener(string type) { Application.Current.Dispatcher.Invoke(new Action(() => { try { var itm = Listeners.Where(x => x.Name == type).SingleOrDefault() ?? null; if (null != itm) { Listeners.Remove(itm); } } catch (Exception) { } })); } public void ShowResults(List<SearchResult> results) { ClearResults(); foreach (var itm in results) { Application.Current.Dispatcher.Invoke(new Action(() => { Results.Add(itm); })); } } public void ClearResults() { Application.Current.Dispatcher.Invoke(new Action(() => { if (null != OnClearSearchResult && Results.Count > 0) OnClearSearchResult(this, Results); Results.Clear(); ClearRemoteResults(); })); } public void ShowFloatPanel(SearchResult targetResult, FrameworkElement ui) { if (null != OnShowFloatPanel) OnShowFloatPanel(targetResult, ui); } internal event EventHandler<FrameworkElement> OnShowFloatPanel; #endregion #region 大屏端同步命令 void DoRemoteSearch(string type, string keyword) { X.Factory.GetSDKInstance<X.IDataExchange>().Send(new IGIS.SDK.Messages.RemoteMapSearchMessage() { SelectedType = this.SelectedType, Keyword = this.Keyword }, "IGISMapSearch"); } void ClearRemoteResults() { X.Factory.GetSDKInstance<X.IDataExchange>().Send(new X.Messages.MessageBase(), "IGISClearMapSearch"); } #endregion } }
如果熟悉Prism的开发者肯定知道这部分可以完整的定义为一个Region,在完成这部分之后,最重要的部分就是将当前的实现接口IGIS.SDK.IMapSearch的对象注入到UnityContainer中从而在其他的Module中去调用,这样就能够实现不同的模块之间进行通信,具体注入的方法请参考下面的代码。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Practices.Unity; using X; namespace IGIS { public class IGISProductInfo : IModule { Microsoft.Practices.Prism.Regions.IRegionViewRegistry m_RegionViewRegistry; public IGISProductInfo(Microsoft.Practices.Unity.IUnityContainer container) { m_RegionViewRegistry = _RegionViewRegistry; container.RegisterInstance<IGIS.SDK.IMapSearch>(ViewModels.SearchManager.Instance); } public void Initialize() { m_RegionViewRegistry.RegisterViewWithRegion(“MapSearchRegion”, typeof(Views.IGIS.MapSearch)); } }
}
首先我们通过m_RegionViewRegistry.RegisterViewWithRegion(“MapSearchRegion”, typeof(Views.IGIS.MapSearch))来将当前的View注册到主程序的Shell中,在主程序中我们只需要通过<ContentControl region:RegionManager.RegionName="MapSearchRegion"></ContentControl>就能够将当前的View放到主程序的中,从而作为主程序的界面的一部分,然后通过代码:container.RegisterInstance<IGIS.SDK.IMapSearch>(ViewModels.SearchManager.Instance),就能够将当前实现IMapSearch的接口的实例注入到Prism框架的全局的UnityContainer中,最后一步也是最关键的就是在其它的模块中,如果我们需要调用当前实现IMapSearch的接口的方法,那该怎么来获取到实现这个接口的实例呢?
下面的代码提供了两个方法,一个同步方法和一个异步的方法来获取当前的实例,比如使用同步的方法,我们调用GetSDKInstance这个方法传入类型:IGIS.SDK.IMapSearch时就能够获取到注入到容器中的唯一实例:ViewModels.SearchManager.Instance,这样我们就能够获取到这个实例了。
public static T GetSDKInstance<T>() where T : class { if (currentInstances.ContainsKey(typeof(T))) return currentInstances[typeof(T)] as T; try { var instance = Microsoft.Practices.ServiceLocation.ServiceLocator.Current.GetInstance<T>(); currentInstances[typeof(T)] = instance; return instance; } catch (Exception ex) { System.Diagnostics.Trace.TraceError(ex.ToString()); return null; } } private static object Locker = new object(); public static void GetSDKInstanceAysnc<T>(Action<T> successAction) where T : class { if (currentInstances.ContainsKey(typeof(T))) { successAction.Invoke(currentInstances[typeof(T)] as T); return; } Task.Factory.StartNew(new Action(() => { lock (Locker) { T instance = null; int tryCount = 0; while (instance == null && tryCount <= 100) { tryCount++; try { instance = Microsoft.Practices.ServiceLocation.ServiceLocator.Current.GetInstance<T>(); } catch { } if (null != instance) { currentInstances[typeof(T)] = instance; successAction.Invoke(instance); return; } else { System.Threading.Thread.Sleep(50); } } } })); }
在看完上面的介绍之后我们似乎对基于Prism的模块化开发思路有了一定的理解了,但是这些模块是在何时进行加载的呢?Prism框架是一种预加载模式,即生成的每一个Module在主程序Shell初始化的时候就会去加载每一个继承自IModule的接口的模块,当然这些模块是分散在程序的不同目录中的,在加载的时候需要为其指定具体的目录,这样在主程序启动时就会加载不同的模块,然后每个模块加载时又会将继承自特定接口的实例注册到一个全局的容器中从而供不同的模块之间相互调用,从而实现模块之间的相互调用,同理图一中的功能区、个人信息区、地图区都能够通过继承自IModule接口来实现Prism框架的统一管理,这样整个软件就可以分成不同的模块,从而彼此独立最终构成一个复杂的系统,当然这篇文章只是做一个大概的分析,为对Prism框架有一定理解的开发者可以有一个指导思想,如果想深入了解Prism的思想还是得通过官方的参考代码去一点点理解其指导思想,同时如果需要对Prism有更多的理解,也可以参考我之前的博客,本人也将一步步完善这个系列。
最后我们要看看主程序如何在初始化的时候来加载这些不同的模块的dll的,请参考下面的代码:
using System; using System.Windows; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Practices.Prism.Modularity; using Microsoft.Practices.Unity; using Microsoft.Practices.Prism.UnityExtensions; using Microsoft.Practices.Prism.Logging; namespace Dvap.Shell.CodeBase.Prism { public class DvapBootstrapper : Microsoft.Practices.Prism.UnityExtensions.UnityBootstrapper { private readonly string[] m_PluginsFolder=new string[3] { "FunctionModules", "DirectoryModules", "Apps"}; private readonly CallbackLogger m_callbackLogger = new CallbackLogger(); #region Override /// <summary> /// 创建唯一的Shell对象 /// </summary> /// <returns></returns> protected override DependencyObject CreateShell() { return this.Container.TryResolve<Dvap.Shell.Shell>(); } protected override void InitializeShell() { base.InitializeShell(); Application.Current.MainWindow = (Window)this.Shell; Application.Current.MainWindow.Show(); } /// <summary> /// 创建唯一的Module的清单 /// </summary> /// <returns></returns> protected override IModuleCatalog CreateModuleCatalog() { return new CodeBase.Prism.ModuleCatalogCollection(); } /// <summary> /// 配置唯一的ModuleCatalog,这里我们通过从特定的路径下加载 /// dll /// </summary> protected override void ConfigureModuleCatalog() { try { var catalog = ((CodeBase.Prism.ModuleCatalogCollection)ModuleCatalog); foreach (var pluginFolder in m_PluginsFolder) { if (pluginFolder.Contains("~")) { DirectoryModuleCatalog catApp = new DirectoryModuleCatalog() { ModulePath = pluginFolder.Replace("~", AppDomain.CurrentDomain.BaseDirectory) }; catalog.AddCatalog(catApp); } else { if (!System.IO.Directory.Exists(@".\" + pluginFolder)) { System.IO.Directory.CreateDirectory(@".\" + pluginFolder); } foreach (string dic in System.IO.Directory.GetDirectories(@".\" + pluginFolder)) { DirectoryModuleCatalog catApp = new DirectoryModuleCatalog() { ModulePath = dic }; catalog.AddCatalog(catApp); } } } } catch (Exception) { throw; } } protected override ILoggerFacade CreateLogger() { return this.m_callbackLogger; } #endregion } }
看到没有每一个宿主应用程序都有一个继承自Microsoft.Practices.Prism.UnityExtensions.UnityBootstrapper的类,我们需要重写其中的一些方法来实现Prism程序的模块加载,例如重写 override void ConfigureModuleCatalog() 我们的宿主程序就知道去哪里加载这些继承自IModule的dll,还有必须重载CreateShell和InitializeShell()
这些基类的方法来制定主程序的Window,有了这些我们就能够构造一个完整的Prism程序了,对了还差最后一步,就是启动Prism的Bootstrapper,我们一般是在WPF程序的App.xaml.cs中启动这个,例如:
using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; namespace Dvap.Shell { /// <summary> /// App.xaml 的交互逻辑 /// </summary> public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); new CodeBase.Prism.DvapBootstrapper().Run(); this.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(App_DispatcherUnhandledException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { try { if (e.ExceptionObject is System.Exception) { WriteLogMessage((System.Exception)e.ExceptionObject); } } catch (Exception ex) { WriteLogMessage(ex); } } private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { try { WriteLogMessage(e.Exception); e.Handled = true; } catch (Exception ex) { WriteLogMessage(ex); } } public static void WriteLogMessage(Exception ex) { //如果不存在则创建日志文件夹 if (!System.IO.Directory.Exists("Log")) { System.IO.Directory.CreateDirectory("Log"); } DateTime now = DateTime.Now; string logpath = string.Format(@"Log\Error_{0}{1}{2}.log", now.Year, now.Month, now.Day); System.IO.File.AppendAllText(logpath, string.Format("\r\n************************************{0}*********************************\r\n", now.ToString("yyyy-MM-dd HH:mm:ss"))); System.IO.File.AppendAllText(logpath, ex.Message); System.IO.File.AppendAllText(logpath, "\r\n"); System.IO.File.AppendAllText(logpath, ex.StackTrace); System.IO.File.AppendAllText(logpath, "\r\n"); System.IO.File.AppendAllText(logpath, "\r\n*************************************************r\n"); } } }
在应用程序启动时调用 new CodeBase.Prism.DvapBootstrapper().Run()启动Prism应用程序,从而完成整个过程,当然上面的讲解只能够说明Prism的冰山一角,了解好这个框架将为我们开发复杂的应用程序提供一种新的思路。