Jump to Table of Contents Pop Out Sidebar

MFC,祭文

不管怎么说都好,MFC 是本座生涯中首个接触并使用并深入研究的 C++ 框架,花的时间最多,相关的书也看得最多,所以始终对 MFC 都怀有深厚的感情(喜欢或者厌恶)。每次看不懂别人代码的时候,我就会打开 MFC 的源代码来调节心情,马上就恢复信心了。有时候甚至会想,其实 C++ 猿猴大可以认真研究一下 MFC 的源代码,学习也好,研究也罢,借鉴也行,因为无论是优点还是缺点,MFC 的一切表现都很突出,让人无法忽视。毕竟,MFC 始终是第一代成熟稳定的 C++ 工业级框架,目前还有很多应用程序构建于其上。尽管它真的存在很多很多的不足,那又如何?毕竟,在大 C++ 中,能获此殊荣的应用框架屈指可数。经过 MFC 的长期折磨,妈的才发现自己对框架的理解,原来不知不觉中比绝多数猿猴要深刻。

MFC 的最大错误就是出现得太早了,它的一切缺陷都根源于此。那个时代,可用的 C++ 语法特性严重残缺不全,就只是单继承和蹩脚的多继承。就连十几年后的成熟 C++03 以现在的眼光来看都是半残次品,可想而知,MFC 的语法支持困境有多困难。与此同时,当时的静态类型面向对象编程也在探索之中,对其缺点也缺乏深刻的认识。而跨平台的需求就更别提了,可对比几年后更成熟的 VCL 框架。那时,业界对 GUI 的理解,也正好起步。但是,最终,MFC 还是无耻地完成了任务,基本用面向对象封装了 win32 下的繁杂概念,很方便了 win32 api 代码地编写,

而几年后,这套框架还封装了 COM(OLE2)的功能,众所周知,OLE2 原本就是很复杂的模型,要理解都不容易。但是,MFC 竟然又奇迹地完成了这件简直不可能的任务。并且,更有好几家第三方公司也专门做 MFC 的扩展库,好像比较著名的有 BcgControlBar,有 XTreme,其代码写得巨烂,可是其界面效果居然很炫。所有的这些,都证明了一件事情,MFC 的内核确有可取之处。

题外话,更证明了大 C++ 的过人之处,哪怕是像是 MFC 设计得这么多问题百出充满重重缺陷的框架,很多核心代码其实也写得不怎么高明,只要内核有那么一点两点可取之处,就能做出来很好的效果。更别说是设计理念上比它更先进的 stl,qt 等,未来甚至更先进的 C++ 框架,只可惜为时已晚,现在 C++ 的人丁严重不足,而这里面基础好的猿猴更是稀罕,就不说别,不考虑 template,不考虑面向对象,就只是简简单单的一个 class 的撰写,又有几个人能写出来干净的代码。 好吧,言归正传。MFC 的设计者工程师们还是眼光很犀利的,用 class 封装窗口句柄,六大关键技术直面 C++ 面向对象基础性先天不足的痛点。只是局限于对其引申不足,深度高度什么的,都稍嫌不足,导致一连串侵入性设计的呆板表现,通用性灵活性都有待加强。

运行时类型识别或者动态类型信息,作为面向对象框架这是必须的,但是 MFC 的 RTTI 只保存了类名,基类信息,构造函数方法,好像还有版本信息,这就显得太小农意识了。反射这个玩意,必须记录字段信息及其注解,成员函数信息及其注解,其实现的接口信息,还有类本身的注解,还有各种构造函数复制赋值函数析构函数,登记了这些信息的反射档案才算是功能基本合格的运行时类型信息。而且,反射又何必仅局限于 CObject 才有这个待遇,所有的基本类型 int,char,const char* 等,也都要有反射,泛型对象,好比指针,好比模板类,这些也都必须提供反射的支持。有了坚实基础的反射支持,接下来C++的框架设计,复杂庞大的类型关系马上就会简化很多。最起码一点,可以坚持单继承的底线(纯虚基类也即是接口的多继承也必须避免)。这一步必须定住,抵住多继承的诱惑,一旦用上多继承,整个对象的二进制内存布局将不可预测,从此走上一条不归路。 C++ 中面向对象不搞多继承不会影响其表达力,这只是习惯问题,因为还有大把手段弥补多继承的缺席。

序列化,何必只限制于 CObject 的子类才有序列化的待遇,所有的类型,不管是否 CObject 的子类,基本类型,模板类,指针,自定义结构体,都可以享受序列化的服务,不必重写序列化的虚函数。同时,序列化又何必仅仅限制于二进制序列化,json,xml,ini,数据库读写等等,也都是序列化。而且,序列化何须重写重载函数,只要拿到了对象反射信息,通过字段反射信息,就可以自动实现所有序列化的事情,一本万利,而必要时,又可以提供对象高性能的序列化实现。mfc 还不支持多态类型的序列化,比如基类指针到子类的序列化支持,不仅仅 MFC,所有 C++ 序列化库在面对多态类型时,都很尴尬,将对象写进流还好办,可是从流中读出对象,就比较难搞,什么工厂方法以实现虚构造函数的效果,总之,没有统一的方案。无他,一切皆因这些库对反射的支持不到位

消息映射,这原本就是极强有力的抽象手段, 原教旨的面向对象思想就是发送消息,接收消息,处理消息。理论上,对象之间不必知道彼此是啥类型,只需知道对方就是对象,可以接受消息即可,至于对方能否处理消息,一点都不重要。 成员函数调用,从消息上理解就是知道目的对象一定可以响应这条消息,并且这种响应操作是即时进行的。这真是解耦到了极点,听起来很美妙,但是这样一来,对象类型之间的层次关系调用关系就全部都消失了,反而增大了理解系统的困难。消息发送于面向对象就好像 goto 于结构化编程,功能太强大,什么都可以做,反而意味着要避免其使用。老实说,消息发送除了 gui 下,其他地方都不一定是非用不可的,极大多数情况下,接口,非侵入式接口,委托或者闭包,就完全足够了。不管怎么说,消息发送很重要,必须在这一块上大做文章,千万不可草率。让我们将眼光放远一点,何必仅仅支持 win32 的消息。而 win32 下的消息机制,完全就是无类型,参数无类型,返回结果也无类型,而且更恼人的是消息编号的管理,搞不好两个消息 id 就碰撞了,必然人肉管理消息编号,很麻烦。这些都是 C++ 框架下能够发光发热加强完善的地方。

看看 MFC 是怎么做的?通过 CCmdTarget 子类下的消息映射表,表上的每一项都是消息 ID 和成员函数指针。这虽然基本上完成了任务,但是不得不说,这很偷工减料。类的定义一旦完成,消息表就固定下来了,不能增不能减也不能改。然后,因为消息映射表是一个数组,也没有排序,所以表的查找效率有待优化。另外,MFC 的消息处理不支持一对多的机制,好像 C# 的 delegate 那样,也就是一条消息,可以有多个消息监听者。又,MFC 的消息实现,动用了太多的宏。MFC 受人诟病的一大特点就是宏用的太多了,ATL 和 WTL 的宏也多得让人大倒胃口,基本上,涉及消息映射时,都是通过各种各样的宏堆出来。当然,对于 C++ 这样零惩罚的语言来说,要构建动态信息,宏的使用是无法避免的。但是,能节制就节制,宏很丑陋,每多一个宏就要扣一分。在打造应用框架时,架构师还是要有点追求的。

MFC 通过付出极大代价来支持 OLE2,又是内嵌类,又是一大堆宏,一大堆虚函数,每个类的对象,凭空多了很多为了满足 com 需要的字段,就算这些类不需要 com 的功能,当然可以条件编译不包含。这就很违背大 C++ 的第一戒律,不为不需要的特性付出任何一点代价。其实,一眼就看出来,com 通用性很高,完全不属于 MFC 的应用体系,所以 com 的这一套功能显然不能仅仅用于从 mfc 中继承下来的类。com 必须实现出来非侵入式的效果,可以给现有类(不一定是 mfc 的类),哪怕一开始就不考虑 com,也都可以在外面给其添加对 com 的支持。

经过这么一改造,相信 MFC 框架应该会精炼不少。由于我们已经在消息和序列化上花了很大的力气来整改,比如说,已经实现了消息一对多的广播效果,这样子一来,臭名昭著的文档视图低配版的 mvc 框架也没有存在的必要性,虽然这一对难兄难弟有其可取之处,但是重构后的框架,只要稍微花点心思,就可以做出来 mvc 的事情啦。当然,到此为止,只是基本封装了 win32 的界面功能,要做一款 gui 框架,还有很长很长的路要走,比如界面渲染,比如排版,异步等等,C++ 真是适合开发 GUI 框架。话说回来,现在 gui 一切都已经是前端的天下了。别提 C++gui 框架,就连 C++ 本身,也已经越来越没有意义,除了强烈的个人兴趣爱好,因为用 C++ 写 gui,实在充满激情,特别是看到写出来的代码比 mfc,atl,wtl,qt 等之流好很多的时候。

至于 MFC 的 socket 以及数据库封装,都写得巨烂无比。而 MFC 的集合类,跟 stl 的那套相比,不提也罢。写到这里,突然又对 MFC 充满了厌恶,可怜之人必有可恨之处。写到这里,突然对 MFC 大倒胃口,这玩意不建议学习,碰都不要碰,实在是其整盘框架设计的思路,以至于细节代码的处理,都已经过时了。奇怪的是,这么一个破旧东西,竟然运行时很稳定,bug 很少,也确实可以做很多事情。相反,很多时尚高端炫酷的现代化 C++ 模板库,看着漂亮,但是却承担不了重任。

吐槽一句,C++ 的基础库实在太难写,库的重构简直没有止境,往往今天很满意的代码,几天之后,就会被改得面目全非。另外,除非认真写一遍基础库,否则对 C++ 的理解将一直浮于表面。说句大实话,C++ 的很多复杂语法就是用来写库,可惜,基本上过得去的 C++ 库,少之又少,就算是 stl,好了,这次就不提它了,恨铁不成钢啊。