[Prism]Composite Application Guidance for WPF(7)——模块
周银辉
既然是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()
{
return new 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
更多的,可以参考这里 Gotcha! Visual Studio Pre/Post-Build Events
OK,非常感谢大家对本系列随笔的关注:)