【讨论】从吉日的一段话说起+寻找WinForm架构的最佳实践

时间:2022-09-11 13:57:43

这两天园子里最火的莫过于吉日的白话反射,导致包子的批判,然后引来了老赵的两篇文章,然后又有若干人等一堆反射技术文章出世。可谓百花齐放,百家争鸣啊。喜欢这种氛围,呵呵。

今天我不谈反射,但和反射有关

不谈吉日,但话题是从这里开始

吉日的《白话反射》里说到:

"我们在开发大型软件项目时经常会遇到,系统很庞大了有几百M的代码了,主程序启动时,总不能把这些都引用了吧?全部加载在内存里?那程序的启动速度,不知道会不会慢如老牛推车了?这时候也会用一些反射技术等,用到哪个窗体,就动态加载哪个那个窗体,总感觉比较清爽一些。"

表面一看,确实。主模块引用这么多模块,启动的时候那不是很慢,那必须用反射来“制造”一个延迟加载的机制。

实际上,CLR的加载过程是什么样子的呢?(不说的那么详细)

很多书里都介绍了,JIT的编译单元是方法。当执行一个方法之前,会先JIT(即时编译)这个方法,然后发现,这个方法里引用了在别的程序集里定义的类型,那么CLR Loader就会加载这个程序集(更详细的过程可以参见《.Net本质论》或我之前的两篇文章CLR LoaderAssembly Loader)。那这么看来,吉日说的就是错误的,几百M的代码不错,难道这几百M的代码在启动的时候就会全部执行么?如果不执行也就不会JIT,不会JIT也就不会加载这些模块。但是有一个例外:你这几百M代码,全部放在一个模块里(我觉得吉日肯定不会那么干)。

根据上面的讨论,所以吉日说的这种场景不需要用反射,不会影响主程序的启动速度,只需要合理的划分子功能模块就可以了,不要弄得铁板一块。

真的是这样的么?如果光说教我会很快的陷入太虚之中,所以我准备了一个小实验:

【讨论】从吉日的一段话说起+寻找WinForm架构的最佳实践

这个一个典型的WinForm应用的解决方案。Main是主启动项目,而FunctionModule1-FunctionModule4是具体的子功能模块(实际的项目中,肯定不止四个)。

我们看到这里Main项目中引用了四个子功能模块。然后下面是主界面:

【讨论】从吉日的一段话说起+寻找WinForm架构的最佳实践

Function主菜单下有四个子菜单,每个子菜单调用一个子功能模块,菜单的事件代码如下:

   1: private void function1ToolStripMenuItem_Click(object sender, EventArgs e)
   2: {
   3:     Function1 function = new Function1();
   4:     MessageBox.Show(function.ToString());
   5: }
   6: private void function2ToolStripMenuItem_Click(object sender, EventArgs e)
   7: {
   8:     Function2 function = new Function2();
   9:     MessageBox.Show(function.ToString());
  10: }
  11: private void function3ToolStripMenuItem_Click(object sender, EventArgs e)
  12: {
  13:     Function3 function = new Function3();
  14:     MessageBox.Show(function.ToString());
  15: }
  16: private void function4ToolStripMenuItem_Click(object sender, EventArgs e)
  17: {
  18:     Function4 function = new Function4();
  19:     MessageBox.Show(function.ToString());
  20: }

现在问:这里的Main项目启动时,会主动加载这四个子功能模块么?

我们使用Visual Studio调试时输出窗口的功能看看:

【讨论】从吉日的一段话说起+寻找WinForm架构的最佳实践

窗口中,前部分输出的是加载的框架的模块,最后一个是加载Main程序集。但是FunctionModule1-FunctionModule4呢?我仔仔细细寻找了好几遍没发现。那么说明,Main项目启动时,不会主动加载子功能模块的,所以吉日的说法是错误的,除非他把所有子功能模块全部写在主功能模块中。那什么时候加载这些子功能模块?来,我们尝试点一下菜单:

【讨论】从吉日的一段话说起+寻找WinForm架构的最佳实践

子菜单的功能弹出来了,从输出窗口里也发现这个时候加载了FunctionModule1.dll。

以下是对WinForm应用架构设计的讨论

-----------------------------------------------------------------------------------------------------------

下面,我们来讨论一下WinForm应用的架构问题。

我不知道园子里有多少人在做.Net WinForm应用,只是看到园子里大部分人都是在搞Web或WPF。苦恼的我还在搞WinForm,不过也其乐无穷。

上面的实例实际上也给出了大部分WinFrom项目的一个雏形,很多的菜单,每个菜单对应一个功能,打开一个功能的时候要么是Mdi的模式,弹出一个窗口,要么就是一个TabPage。大的工程肯定是一个人完成不了的,需要多个人来做这个事情。但是又需要统一规划。

所以必定是这样的:由架构师来规划好底层支撑的框架,比如这里的Shell,还有一些引擎级别的服务(比如LanguageService“多语言”,PropertyService“持久化软件里某些设置”等)。这个框架必须稳定,项目组其他人员,每个人负责一个或多个功能,每个人只需要关注自己的业务就可以了,把功能做好,然后调用框架里提供的一些服务,把自己给挂接到主框架中。

那么如果按照上面示例的这种设计,我们的程序主框架就必须引用所有的子功能模块,项目开始的时候,可能只有一两个模块,随着项目的前进,引用也在不断的增加,而主界面上的菜单也在不断的增加,这可以说主界面是稳定的么?

那我们必须寻找一种机制。子功能对自己负责,子功能负责自己将自己注册到系统中去。这个时候我们可能采用这样的方式:

一个Modules文件夹,在这个文件夹下又放着很多子文件夹,每个子文件夹里放着一个功能,当系统启动的时候,由框架搜寻Modules子文件夹,在里面查找一个后缀名为addin(或者其他方式)的xml文件,文件里面的内容可能如下:

<MenuItem Site=”File” Text=”Edit” Icon=”Edit.png” CommandType=”MultiLibrariesApp.EditCommand” />

主模块读取这个之后就会生成一个菜单项,当点击这个菜单项的时候,根据CommandType,利用反射实例化一个EditCommand类型,所有的Command可能都实现一个ICommand接口,而这个接口里有一个Run方法:

   1: public interface ICommand
   2: {
   3:     void Run();
   4: }
   5:  
   6: public class EditCommand : ICommand
   7: {
   8:     public void Run()
   9:     {
  10:         //code here...
  11:     }
  12: }
  13: public class MenuItem
  14: {
  15:     public string Text{get;set;}
  16:     //主框架的代码里
  17:     ICommand command = null;
  18:     private void MenuItem_Click(object sender, EventArgs e)
  19:     {
  20:             //延迟加载
  21:             if(command == null)
  22:                 command = //通过反射实例化具体的Command类型,这里就是EditCommand
  23:            command.Run();
  24:     }
  25:     public ToolStripMenuItem CreateMenu()
  26:     {
  27:         ToolStripMenuItem menuItem = new ToolStripMenuItem();
  28:         menuItem.Text = this.Text;
  29:         menuItem.Click += MenuItem_Click;
  30:     }
  31: }

这样,就能保证主框架是稳定的了,子功能负责自己的菜单管理,只需要写一个文件就可以了。

当然,实际的框架实现过程中,肯定会碰到各种各样的问题,这只是一个思路。对于WinForm程序而言,你还可以参考一下开源项目:

SharpDevelop 一个开源的IDE

MonoDevelop 从SharpDevelop发展而来,但是现在大变样了

Mono.AddIn由MonoDevelop的插件机制发展出来的一个小插件系统

Composite Application Block 微软模式&实践小组的

还有很多其他,我就没有研究过了。

以上只是我的一个思路,由于WinForm开发的看到不多,我在网上搜WinForm Best Practice也没找到多少资料,所以希望能够在这方面有所讨论。也许能碰撞出一些火花出来。

后话

刚才出去了一趟,在路上又思考了一些问题。

通过上面对模块加载和WinForm架构设计的讨论,总结一下:吉日文中说的这种应用反射的情况并不是因为基于启动效率的问题,而是设计的考量。这个地方跟性能一点关系都没有(在这里对我在老赵博客里开始错误的评论表示道歉),用不用反射启动效率都是这样。

再看看吉日另外一个应用反射的场景:两个类循环引用。我不知道为什么有这样一个设计,如果是遗留代码那你首先应该考虑重构一下,如果实在不能重构,就必须这样,那只有用反射了。所以这个问题也是设计上的问题,跟反射也没啥关系。

还有吉日说的,配置多数据库的场景。老赵说了,这里推崇ORM。即使你不用ORM,我也觉得这是没有必要的。我不知道有多少情况一个正在运行的系统要突然更换不同类型的数据库?即使有这种情况,那么这也属于重大变更,对于这种变更,你完全可以修改代码。还有,针对这种情况,微软已经给出了Best Practice:提供者模式。虽然提供者模式最终还是反射。