一次关于用MVC改进GUI开发的讨论

昨天临下班前跟猎手讨论了一个技术问题。今天令狐看了,指出这个解决方法治标不治本,属于头痛医头脚痛医脚的解决方案:

但要是你直接取parent的ActiveControl,这个窗体不嵌入其他窗体的时候不是又错了?换句话说,这个窗体跟它的使用环境发生了耦合。有没有比较好的办法来解决这个问题?

我说了三个方法,前两个都不算是通用的办法,就不说了。第三个就是我在《杂而不精》一文里我提到过的MVC模式。在这一点上,我和令狐达成一致。针对猎手这个问题使用MVC,可以把这部分功能从View剥离到Control上去处理。

关于MVC,令狐有一段说明:

MVC的概念我的Blog里有提到。简单的说,就是把界面、界面需要完成的功能、这些功能所要操作到的数据对象全部分离。这样一来,比如你界面上有一个按钮和一个菜单完成同样的功能,就只要调用功能类中同样的方法即可,跟这个按钮、菜单所在的界面就无关了。

VCL中有一个Action的概念,但光有这个,支持MVC开发是远远不够的。这也是我在《杂而不精》中说过,我一直在思考的问题,早就想写一个文章来说的,不过感觉想法还不够成熟。今天既然拿来讨论了,就先简单空谈一下。

RAD让人养成很多坏习惯。比如,即使有Action这样的,做出MVC也会不伦不类,V和C的耦合度会很高的,这很不好。我设想的是RAD只做V的部分,将C部分完全剥离,这样GUI应用就会成为可测试的,用单元测试覆盖C和M。

令狐指出其中可能存在的一个问题:

VCL的问题我以前也想过,我觉得是因为VCL本身对MVC的支持就不够好。因为VCL中,已经将C的东西融合到控件本身去了,这样你再想剥离就很困难。我以前在CSDN上帮人写过一个MVC的演示,就碰到过这个问题。
举个例子,比如TMemo的Lines,存储了Memo中的内容,而这个Lines是跟TMemo本身的操作又是相关的。我那时候是自己用一个List去 记录数据,但是这样一来,就会造成Lines和我自己那份数据的重复,而且有不同步的潜在可能。后来我的解决方法是完全放弃Lines,每次我自己的 list更新之后,将Lines清空重新设置。不过这样对显示效果又有影响。

但我认为还是可以剥离的,这跟VCL的关系不太大。我在最近写的一个BLOG备份程序的GUI部分里就尝试用MVC的思想,感觉还可以。至于令狐上面的说的问题,也是可以解决的。

Memo 的Lines属性是一个TStrings *。在Control里维护一个TString * 成员指向Memo的Lines即可,由Control直接去维护Memo的Lines,这样就不存在一式两份的问题。因为Memo的Lines属性是不可 更改的,所以只能通过Control去操作。不过程序中的其它代码不能直接对Memo的Lines进行操作,而是要通过Control进行。

把我的思路总结一下就是这样:

我把V和C之间的关系简化为三种:Action, State, Contraint
Action代表对界面的操作,调用Control的相应函数处理
State代表界面状态的改变,由Control去执行
Constraint代表约束,即Action和State的关联,比如在某种状态下不能出现某个Action
这个Constriant又有两种,强约束和弱约束
弱约束可以在View中实现
强约束可能在Control中实现,也可能在Model中实现

有必要补充一下:其中关于Constraint的想法,最早来自于去年与Chechy讨论他的一个基于XML的框架时受到的启发。

实际上弱约束的情况很少,它通常不能算作业务逻辑的一部分,为了简单起见才把它放在VIEW这边实现。如令狐所说,弱约束差不多就是指控件Disable或者Invisible这种。不过有些Disable/Invisible可能还是要放到Control里的。

令狐作了一下归纳:

用 户对界面元素进行一个操作,发出一个request,实际触发一个控件事件,这个事件将会调用Control,Control继续调用业务模块,业务模块 完成实际业务逻辑并返回,根据返回值,Control做两种可能的操作:转向另一个页面,或改变当前页面的某些状态(或两者兼具);而页面上的控件由 View类负责,View类接受到状态改变后,对控件实际的表现形式进行对应修改。

整个流程是:
View's Event->Process Control->Business Control->Model->Model return->Business Control return->Process Cotrol's state change OR open new view->View class(change form's control)

基本上就是这样,这样做还有一个很大的好外就是,可以比较方便地更换VIEW层。比如从PC移到PDA,或移到WEB。

令狐补充了一点:

这 样的话,View层由两部分组成:Form和View class,Form负责可视化、用户事件接收,View class负责处理状态的改变。然后,你所谓的强约束,可以几乎肯定是做在Process Control或Business Control层;而弱约束,几乎肯定是做在View Class

如果考虑到View层的更换,是必须加入View Class的,这是我开始没有考虑到的,我原来设想中这部分功能是在control里的。所以还是有很多不够成熟的地方,需要再讨论讨论。最主要的是需要经过实践的考验才行。

令狐又提了两个重要的问题:

1、Windows的控件有的时候是界面相应先于逻辑处理,比如Checkbox,你一点它就直接改变状态了,但是后端的逻辑处理可能又会重复修改它的状态。
2、还有个更严重的问题,对某些控件状态或内容的修改,会触发一些事件,这样会造成一些事件的递归调用或Control的难以控制
在传统的Web程序中,这个问题比较不明显,因为你在View上做的操作都不会产生实际的影响,直到Submit才会去服务器进行MVC的流程,但是客户端就没这么简单了,每个操作都可能会这样跑一圈

事实上,这两个问题在传统RAD开发中都存在,特别是第二个问题尤其严重,并且都很难解决。我起初考虑在GUI应用开发中采用MVC模式,很重要的一个原因就是为了解决如上面的第二个问题。

对 于第一个问题是这样:当check时就会产生Action,它调用到control这边时就可以根据情况处理,如果知道这是一个需要Model做长时间处 理的,就可以先改变界面的状态,比如把光标设置成hourglass,在Model处理完成之后再更新checkbox状态,并恢复光标。当然,最终的状 态要以Model的处理结果而定,由control去更新view的state。

而第二个问题的解决之道就在于所有的Event都会转为Control的Action请求,当第一个请求到达Control时,就会启动相关的Constraint。之后的请求就会根据Constraint发生不同的作用——比如有些就会被屏蔽掉。

令狐又提出第三个技术问题:还有一个问题就是环境上下文如何传递

这个问题在GUI应用中还是比较好解决,但在WEB应用中就麻烦一些。在GUI应用中,上下文可以直接或间接(通过指针指向View中的控件属性,如前面说的那个Lines)记录在Control中。但因为Web应用是无状态的,所以需要额外的持久化机制。

令狐说:持久化机制在服务器端利用数据库、在客户端利用Cookie,都有办法可以做到

但实现方法肯定是与GUI应用不同的,要麻烦一些。

我那个试点程序对上面说的这些没有全部实现,只尝试了一下V和C的分离,所以说我还要再考虑考虑。

我同意令狐说的:看看实现在讨论比较实际,否则是空对空的。试着用上面的理论作个实现出来,如果能提升到框架是最好不过了。就算只是个思想也是好的,不过要实现出来有些问题才好讨论

提升到框架还是有难度的,我的目标是整理出一套切实可行的做法,以改进GUI应用开发。