4 模块化
1. 背景
将应用程序分成一个一个模块是非常有好处,利于协同开发,扩展,测试,以及维护。
1.1. Prism对模块化的支持
使用Prism框架组织代码就是为了对一个个部件模块化,降低耦合度。Prism提供如下方法实现模块化。
- 模块目录,用于注册模块信息,如名称以及模块位置。
- 为模块声明元属性,支持模式初始化以及依赖
- 整合依赖注入容器,支持模块间解耦
- 在模块导入方面,实行依赖模块管理,防止重复导入。并且支持OnDemand以及Background导入。
2. 核心概念
2.1. IModule
一个模块可包含多个程序集,每个模块均需实现IModule接口,IModule是模块对外暴露部分。IModule接口很简单,只有一个Initialize方法,你可以在该方法整合需要的内容。
public class MyModule : IModule
{
public void Initialize()
{
// Do something here.
}
}
2.2. 模块导入过程
模块导入分为以下过程:
- 注册/发现模块,将模块添加至模块目录即完成注册过程。模块目录包含许多模块信息,包括模块名称,位置,以及导入顺序。
- 导入模块,模块中包含的所有程序集均导入内存中,这个过程可能需要从远程服务器导入或者本地目录导入。
- 初始化模块,本阶段创建一个模块类的实例,同时调用初始化方法Initialize。
3. 模块注册
有多种方法注册模块。模块目录就是一个包含多个类ModuleInfo的集合,填充该目录。依据注册位置分为不同方法。
3.1 代码注册
在Bootstrapper文件重写方法ConfigureModuleCatalog,导入模块:
protected override void ConfigureModuleCatalog()
{
var catalog = (ModuleCatalog)ModuleCatalog;
catalog.AddModule(typeof(ModuleAModule));
}
可执行程序需添加对模块程序集的引用。
3.2 配置文件注册
Prism自动从配置文件读取信息,查找对应dll中的模块,示例如下:
<!--注册信息包括dll名称,模块类名,版本,设定程序启动是否导入-->
<modules>
<module assemblyFile="ViewSwitchingNavigation.Contacts.dll" moduleType="ViewSwitchingNavigation.Contacts.ContactsModule, ViewSwitchingNavigation.Contacts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ContactsModule" startupLoaded="true" />
</modules>
在bootstrapper类中重写方法CreateModuleCatalog,
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
可执行程序无需添加对模块程序集的引用。
3.3 文件夹注册
在bootstrapper类中重写方法CreateModuleCatalog,使用文件夹目录,
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}
你只需要将对应的模块拷贝到目录下,上述为Modules文件夹,Prism自动从文件夹导入。可执行程序无需添加对模块程序集的引用。
3.4. 延迟导入
默认情况下模块注册后系统就导入模块程序集,通常这需要耗费一定时间。在复杂的企业应用中,并不是所有模块均需在程序启动时导入,这将导致程序启动缓慢,占用内存高等问题。模块化的应用我们可以依据需要导入模块。遵循以下原则:
- 应用程序启动时需具备模块需要在启动时导入。
- 应用常使用的模块可在模块被检测到时以背景方式导入和初始化。
- 那些很少使用模块可在需要时导入。
后台代码,配置文件支持按需导入。具体实现是在注册时声明模块的初始化模式是按需导入。
protected override void ConfigureModuleCatalog()
{
var moduleAType = typeof(ModuleAModule);
ModuleCatalog.AddModule(new ModuleInfo()
{
ModuleName = moduleAType.Name,
ModuleType = moduleAType.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand
});
}
运行时使用接口IModuleManager的方法LoadModule导入模块,可以使用依赖注入方式获取IModuleManager实例,
using Prism.Modularity;
using System.Windows;
namespace Modules.Views
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
IModuleManager _moduleManager;
public MainWindow(IModuleManager moduleManager)
{
InitializeComponent();
_moduleManager = moduleManager;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_moduleManager.LoadModule("ModuleAModule");
}
}
}
4. 模块初始化
模块初始化工作一般是在前面提到的Initialize()方法中,模块初始化一般完成如下工作:
- 注册当前模块对外提供的服务
- 注册相应的View
如果需要在View之间转换,还需要注册相应的View,并配置用于导航,如下:
//注册硬件服务
_container.RegisterInstance(new Hardware());
//注册需要的view,这里仅注册,不会填充到相应的域中
_container.RegisterTypeForNavigation<SelectDrugView>();
//注册view到相应的域中
_regionReg.RegisterViewWithRegion(RegionName.MainRegion,typeof(MainView));
注意只要注册相应的View就自动注册与该View对应的ViewModel以及对应Popup窗口。
4.1 注意
当一个模块导入并且初始化以后,这个模块程序集不能被卸载。模块实例并不由Prism库管理,所以模块实例将在初始化完成以后进行垃圾回收。
5. 模块间交流
将应用解耦成各个模块后,为了实现某个功能,又需要多个模块协作。这就涉及模块间的交流。模块交流有很多方式。
5.1 低耦合事件
当一个模块广播事件,其他模块订阅该事件。
优点
- 容易实现
- 轻量级
缺点
- 难以维护
- 不适合多个事件组合完成一个单一任务情况
5.2 共享服务
共享服务是一个能通过统一接口访问的类,一般定义在共享程序集或者系统服务中。
优点
- 容易维护
- 可实现复杂交流
缺点
- 未知
5.3 共享资源
这里的资源不是指图片,声音,文字等媒体资源,而是某种系统或者公共接口,如数据库系统,WebService集。模块与模块间不直接交流,使用资源作为中介。
graph LR
A(Module A)-->B(Resource, include WebService, database,etc...)
B-->C(Module B)
6. 关键决策
6.1. 使用的框架
你可以选择创建自己框架或者复用现有框架,如Prism,MEF及其他。
3.2. 如何组织解决方案
从逻辑角度看,定义每个模块的边界,如需要包含的程序集有哪些等是实现一个模块化架构基础。
从代码角度看,一般情况一个模块一个程序集,对应到代码中则是一个模块一个Project。但是对于大型项目,可能存在10~50个模块,每个模块一个项目会拖慢visual studio。这种情况就需要考虑一个项目是否应该对应多个模块。
3.3. 如何分解模块
可以依据需求将系统分解为不同模块,例如通过功能域分解(设计常用),依据模块供应商分解,依据开发团队分解,依据部署需求分解。你可以从水平功能块分解或者垂直服务分解。水平是指从商业功能方向分解,如一个交易系统包括用户模块,产品模块,订单模块。垂直分解则是从技术角度分解,如拦截业务模块(日志,缓存),商业功能块,UI模块等。
好模块特征:
- 功能单一
- 对其他模块具有最小依赖
- 以接口方式与其他模块交流,该接口定义在共享类库中,或者使用EventAggregator与其他模块交流。
7. 问题
模块化时不同模块的View是否可以重名?