现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

从MVC架构到C++的多态实现

2015-03-14 23:20 工业·编程 ⁄ 共 2790字 ⁄ 字号 暂无评论

从MVC架构开始说起吧。这两天系统了解了一下MVC架构的内容,主要参考于文献【1】。

MVC在这几年应该被非常多的人所熟悉了,因为相当多的web框架采用的是这套架构,此外,早在MFC横行的年代,MFC所采用的document/view架构也是MVC架构的变种。包括QT,它的model/view亦是如此。只不过它们都将MVC中的view和controller的功能整合到了一起。

MVC的全称是model-view-controller architecture,最早被用在了smalltalk语言中。MVC最适合用在交互式的应用程序中。

我个人认为,理解MVC架构最重要的是两点:

1. MVC将数据的维护和数据的呈现,与用户的交互割裂了。Model负责的是数据的维护,就好比是DB和文件要保存数据一样,可以认为它是process。而view负责的是数据的呈现,把数据通过某种形式在用户面前展现,把它看做是output。model和view的关系就像下面这幅图一样。

而controller负责的是处理用户的输入。它提供一个窗口或者是控件供用户输入数据,它就是input。所以,一个MVC架构,就是将交互式程序的process,input和output解耦合。

2. 这一点更为重要,就是model与view和controller之间的联系。任何一个MVC框架都必须提供一个“change-propagation mechenism”(变更-传播机制)。而这个变更-传播机制是MVC框架中model和view以及controller之间的唯一的联系(The change-propagation mechanism is the only link between the model and the views and controllers)。比如说,一个用户通过controller改变了model中的数据,那么model会使用变更-传播机制来通知和它有关的view和controller,使得view去刷新数据和重新显示。

有很多人总是对于MVC架构不能够熟练掌握,核心问题就在于他在使用MVC架构的时候是看不到变更-传播系统的,所以对于MVC架构的内在运行机制无法了解。

完整过程如图所示:

1. 用户操作controller,相当于调用controller的handleEvent函数;

2. handleEvent函数实际上调用的是model中的成员函数,对model中的数据进行操作;

3. model中的数据被调用完成后,model会执行自身的notify函数;

4. notify函数会调用和这个model有关的所有view和controller的update函数;

5. update函数会调用各自的display函数和getData函数;

6. 当这一切都完成时,handleEvent才返回;

更多的关于MVC的内容就不在这篇文章中详述了,毕竟俺写这文章不是光为了MVC。有兴趣的可以查看网络文档或者参考文献。

下面的重点在于讨论这个change-propagation mechenism的实现。

其实一个简单MVC架构的变更-传播机制采用observer模式+多态就可以搞定了。model维护一个基类view(和controller)的指针队列,将所有和这个model相关的派生view的指针放在这个队列中。那么model的notify函数就是依次调用队列中的指针的update成员函数。

但是,在实际的C++的MVC系统中,比如MFC,或者QT,都没有采用这种方法来实现变更-传播机制,事实上,他们在实现这个机制的时候都没用到多态。MFC采用的是消息映射的机制,基本概念是建了一个消息查找表,将消息和对应函数的映射关系存储下来。每次处理一个消息的时候,都去表中查找到对应的函数,然后回调。而QT采用的signal-slot机制(具体的实现机制我不清楚,但肯定不是用的多态)。

为什么MFC和QT都不采用多态呢?我相信有很多的原因,比如QT的signal-slot要求是能够跨进程的,这肯定不是用多态能做到的。在这我只讨论一个原因。

讨论之前先说一说C++的多态机制的实现(我更推荐你看参考文献【2】而不是我的这段话,【2】中把这个问题解释得非常清楚)。很多人都知道vtable,这里放着某个类的一个虚函数指针数组,某个类的指针如果要调用虚函数,先会通过vptr找到vtable,然后查找到对应的函数。这个机制本身没有问题。但关键是,C++在vtable中保存的是所有虚函数的指针,也就是说,如果一个基类有1000个虚函数,但它的继承类只改写了其中的5个,那么这个继承类的vtable中仍然有1000项,表中的995项被浪费了。正是由于这个原因,MFC和QT都没有采用C++的多态机制来实现变更-传播机制。因为在MFC和QT中,它的每个基类都有着大量的虚函数,而在实际应用当中,继承类可能只是改写其中的很少的几项,如果采用多态实现,那么会浪费大量的内存空间。

借用文献【2】的一段话,“也正 是因为这个原因,从OWL 到VCL,.. 从MFC到Qt,以至于近几年出现的GUI和游戏开发框架,所有涉及大量事件行为的C++ GUI Framework没有一家使用标准的C++多态技术来构造窗口类层次,而是各自为战,发明出五花八门的技术来绕过这个暗礁。其中比较经典的解决方案有 三,分别以VCL 的动态方法、MFC的全局事件查找表和Qt 的Signal/Slot为代表。而其背后的思想是一致的,用Grady Booch的一句话来总结,就是:“当你发现系统中需要大量相似的小型类的时候,应当用大量相似的小型对象解决之。” 也就是说,将一些本来会导致需要派生新类来解决的问题,用实例化新的对象来解决。这种思路几乎必然导致类似C#中delegate那样的机制成为必需品。 可惜的是,标准C++ 不支持delegate。虽然C++社群里有很多人做了各种努力,应用了诸如template、functor等高级技巧,但是在效果上距离真正的 delegate还有差距。因此,为了保持解决方案的简单,Borland C++Builder扩展了__closure关键字,MFC发明出一大堆怪模怪样的宏,Qt搞了一个moc前处理器,八仙过海,各显神通。”

结语

以上论点其实我并没有十足的把握,因为正如我所说的,实际中不采用多态可能有方方面面的考虑,而我所提到的这个原因也许微不足道。

为了反驳我自己的观点,可以计算一下,当MFC最开始诞生的时候,那个时候计算机的内存很小,所以大家很节约,但现在,计算机的内存非常大,一个vtable就算是上千个函数指针,那也是可以忽略不计的。

参考文献

【1】 面向模式的软件体系结构 卷1:模式系统

【2】 C++多态技术的实现和反思

作者:  historyasamirror

给我留言

留言无头像?