软件的首要技术使命是管理复杂度(Complexity)。这是<<代码大全>>中的一个标题。软件本质性困难的根源都在于复杂性。Dijkstra指出没有谁的大脑能容得下一计算机程序。正如社会进步催生社会分工一样,软件行业也自然而然地发展出来了模块化方法,将整个系统分解为多个子系统来降低问题的复杂度,分而治之。它有两个主要的目的:
1. 分工 (角色与责任)
2. 信息隐藏 (协作)
分工可以更容易实现并行开发,带来开发效率的提升。分工还可以隔离变化,使得软件应对变化的能力增强。而信息隐藏则降低了对程序员的要求,能够更好地掌握模块内的复杂度。
另一种将模块化视为系列决策组合的看法,只关注到了模块化过程的形式,而忽视了模块化的目的和目标。决策固然是贯穿整个开发过程,但并不是专属于模块化。还是应当强调模块化背后的目的以及使用的方法,关注其产出的结果:模块+接口。
模块是分工的结果,接口则由协作定义。
为了强化模块的协作,上世纪80年代引入了契约式编程(也称为Design by Contract,或Contract Programming)的概念,要求清楚地定义功能的前置条件和输出结果。正像是商业活动中使用合同(contract)来约束和规范商业协作。
凡事过犹不及,模块化的粒度多大才合适?在<<Unix编程艺术>>中第四章有专门一个章节进行探讨,可以作为一个参考。模块化并不能完全解决耦合和复用的问题,所以90年代末又有了将模块中分散着的各个相似的功能集中起来的设计实践,面向方面的编程(AOP, Aspect Oriented Programming)。其实也是为了将分散的逻辑抽取出来,模块化。
这些都是对模块化的完善。
面向对象编程同模块化
面向对象编程其实是一种能更好地支持模块化的编程方法,可以更好的达到了模块化的两个目标。在系统级别的模块划分上,并没引入什么特别的贡献。但在系统模块的内部,却较以前面向过程的开法更方便地实现了逻辑上的模块,更为重要的是它在信息隐藏上的突破。
举例来说,以前在某个模块(组件)内部,以面向过程来写代码时,很容易产生出复杂的函数关系和无法约束数据访问的问题。而面向对象却使用不同的类或接口很好的在内部又建立区不同的逻辑模块来。不同的类对象之间的访问和存取都受到各自实现的约束,真正做到了信息隐藏。
虽然有时面向对象的编程语言也被发展的过于庞大,其本身的复杂度也越来越高,特别是如果过于专注于分层而引入了更多的细节,反而增加了复杂度。语言本身是个工具,如何使用好这个工具最终取决于设计者。它既可以用来雕琢天使,也可以用来创造魔鬼。
模块化的设计方法
如何进行模块化设计?要么从上到下(Top-Down),要么从下向上(Down-Up)。从上到下时常常导致领域问题和技术细节考虑不足,而从下至上的设计方法却有时却让整合出了问题。所以还需要有设计验证的工作和迭代设计的流程。
从上至下,是从整个系统的层面入手,切分子模块。可以按业务功能分(垂直切分),如ERP分为财务、人事、制造/生产管理、采购、销售、资材管理(仓库)等。也可以按逻辑功能分(水平切分),如UI模块、业务逻辑模块、数据持久层、报表管理模块等。
为了实现并行开发,模块间的接口必须被清楚明确的定义出来,包括接口函数和公共数据结构。关于接口的设计,也有很多计论。比如下文:
如何评估模块化的效果呢?Eric.Raymond推荐至少可以从两方面考察:紧凑性和正交性。紧凑性可以用来评估模块化的划分是否合适,而正交性则是关注在模块间的协作水平。为了提高紧凑性,就要减少方案中的特例和边界情况,尽量使用统一明确的规则来组织系统。
WebKit的模块化
模块化的概念绝大多数人都知道,正如前面所说的,结果决定于设计者,而不是工具。向好的设计学习,是掌握模块化的好方法。
后面将对目前使用最广的浏览器内核WebKit的模块化设计进行分析。
Eric.Raymond在<<Unix编程艺术>>中讨论模块化时,多次提到浏览器设计,足可见浏览器设计的代表性。
首先做个简要说明。我们常说的WebKit是浏览器内核,开发者只要在它上面加一个界面层就可以开发出一个浏览器。它的功能包括对网页的加载、解析、显示以及处理用户的交互。随着网页功能越来越复杂多样,浏览器内核的功能也是日益强大,其内部实现也是相当庞大。
根据对Safari使用的WebKit进行统计(使用的是cloc.pl),其中有13354个源文件,共计1645695行代码,不含注释等非代码行的内容。面对这样的代码,还要不断升级来满足各种标准和网页的变化,以及用户的需求变化,如果没有良好的模块化,是根本无法完成的。
面对庞大复杂的浏览器软件,对于浏览器框架首要任务是区隔出核心功能及非核心功能,什么是变化的,什么是相对稳定的。像许多大型的开源项目一样,WebKit的开发也深受Unix的开发文化的影响。于是"只将一件事做好"的格言就决定了WebKit不会去做浏览器,只是浏览器的核心引擎。展示网页内容就是WebKit的核心功能。
关于浏览器设计,有一个简洁的概念模型:
(来源: <<A Reference Architecture for Web Browsers>>,Alan Grosskurth& Michael W. Godfrey)
这个模型展示出来浏览器设计的主要要素。其中用户界面面向的是用户交互,渲染引擎则面向对各种页面标准的支持,而中间的浏览器引擎处理用户的操作,并回馈网页上的交互,是用户界面与渲染引擎交互的桥梁。数据持久层则是用于存储网页会使用的如Cookies和缓存一类的数据。在最下面一层,则是为渲染引擎提供支持的功能模块。
虽然WebKit并没有完全按这样来切分,但其思想是相似的。WebKit将上图中的绿色部分构成了WebKit的主要部分,形成自己的结构:
UI部分可以自由定制,通过WebKit2调用WebCore的功能。WebCore可以通过封装的接口方便地选择JSC或V8做为它的JavaScript解释器。在整个项目中所使用到的公共数据结构和一些与平台无关的辅助性的功能都被放到了WTF中。
我在下面主要探讨WebKit模块化的要点:
1. 封装核心功能,与用户界面隔离。
2. 提取公共的基础代码和数据结构,封装成库,供其它模块使用。
3. 使用插件机制支持扩展需求。
4. 为跨平台提供良好的支持。
5. 持续改进。
应对变化 - 封装核心功能,与用户界面隔离
变化无处不在,但基本上是万变不离其踪。软件中的变化可以归纳为用户需求变化和技术环境变化等。在技术上来说,应用软件中的变化体现在两个层面,一个是外部展现的变化,如界面操作、事件响应等,另一种则是运行策略上的变化,在不同的配置或环境下执行的路径不同。对于浏览器软件还有一项重要的环境引起的变化:标准的变化。比如现在正在发展的HTML5、CSS3等。
WebKit2的应对之道就是分离原则:接口与引擎分离,策略与机制分离。(以下内容参考了WebKit官网的介绍:WebKit2 - High Level Document)
首先在接口与引擎的分离上,WebKit2除了提供一组C的API外,还将用户进程与内核进程分隔开来,如下图所示。上面的UI Process包含了UI和WebKit2的UIProcess层,下面的WebProcess则包括了WebKit内核模块和WebKit2的WebProcess层。这样做可以带来更好的健壮性、安全性和性能。因为如此UI与内核的耦合降低了,提高了正交性,大大降低了系统的复杂度。
(*源自WebKit官网)
为了应对在外部显示的变化及运行策略上的变化,WebKit2提供的是非阻塞式的API,有丰富的回调机制。包括:
- 消息通知式的客户端回调(Notification style client callbacks)。在适当的时机通知客户端但并给它机会去执行什么干预操作,仅仅是通知而已。
- 策略式的客户端回调(Policy style clients callbacks)。允许由客户端决定某个行为。
- 策略配置(Policy settings) 让在客户端就可以预先设定一些策略选项。如一些预设文本之类的内容。
- 可注入的代码(Injected code)。允许在Web Process中加载并执行特定的代码。实现在Web Process中的InjectedBundle模块。
建构基础 - 提取公共的基础代码和数据结构,封装成库
核心业务代码与非核心业务代码的分离,正是面向方面编程(AOP)的主要思想。在不运用AOP框架的情况下,也可以达到一定效果。关键在于识别并抽取出公共的数据结构及操作。
如智能指针和内存分配器。WebKit中提供了丰富的智能指针操作来简化程序员的负担,而它们都被封装到了WTF中。
为了优化内存分配的性能,特别是小内存的分配和跨线程的安全性,WebKit使用了TCMaclloc来作为系统缺省内存分配器的替代者。使用宏定义就可以在它们之间切换。
这些功能无论在WebCore,还是WebKit2都会被使用到。而它们也并不是核心业务,抽取出来,降低了核心业务部分的复杂度,好处不言自明。
灵活健壮 - 支持标准的插件机制
在云计算快速发展的时代,浏览器也正朝着平台化的方向发展,不再只是一个单纯的浏览器。在其上可以构建出许许多多的基于网页的应用,被称为WebApp。通过Chrome的应用商店就可以窥见一斑(国内的360和猎豹因为使用的正是相同的内核,也就可以很容易共享这些应用)。
Netscape为当时的Netscape Navigator定义了一套插件接口 (NAPI) ,现在由Mozilla维护,并被多个主流浏览器所采用。
NAPI实现了在网页中插件一些特别的元素,并可以和其它DOM元素一样通过JavaScript Binding由用户脚本控制。比如常见的Adobe Flash播放插件,和PDF阅读插件。甚至可以和系统应用程序交互。
每个插件由一个特定的MIME Type来指定,由浏览器内核在解析时根据需要加载对应的插件。WebKit实现的模块图如下:
对于插件开发者而言,只要遵循NAPI的规范,就可以实现跨浏览器的支持。Plugin Process同时将插件的独立于内核之外。在需要时调用插件的服务就可以了。
插件在模块化上的作用非常明显,宿主在需要动态调用或组合各个插件就可以实现强大的功能,而且保证他们各自的核心功能不受影响。比如著名的GIMP,它的图像处理函数几乎都是以插件的形式存在的。还有多媒体播放器,解码器也是以插件的形式组织的。插件机制的核心就是接口的定义,而协作决定接口。只有构造出简洁有效的协作方式,才能保证插件的优势。
以下是NAPI相关的参考:
2. Chromium - Plugin Architecture
兼容并蓄 - WebKit的跨平台方案
跨平台是许多大型软件要解决的问题,其前提条件也是提炼出核心业务逻辑,保持它的一致性。而跨平台真正要变的是各种接口,对于浏览器来说主要是网络接口、图形接口、多媒体解等。
WebKit的解决方案主要使用适配器模式。主要思想还是提取公共的业务逻辑封装在一个接口中,再由各个平台视需要实现不同的部分。
以Image类为例,它的一个函数与平台相关,于是类的实现被放在了三个文件中Image.cpp, ImageMac.mm和ImageWin.cpp中。Image.cpp中实现了公共的部分,而ImageMac.mm中实现了Mac OS版本,而ImageWin.cpp则实现了Windows版本。如下图所示:
*WebKit将所有与平台相关的代码,放在platform目录,结构清楚。
还有另一种实现方式,如多媒体元素的播放控件。首先是MediaPlayer提供了一部分公共的逻辑,对于与平台相关实现的部分,定义了一个MediaPlayerPrivateInterface,各平台继承自这个接口实现各自的逻辑。
重构 - 不断追求卓越
好的设计很难一步到位,一定需要不断的演化。WebKit的演化也体现在它的代码上。无论<<程序员修炼之道>>作者推荐的"Don't repeat yourself"和Unix中SPOT原则,都是在强调不要有重复的逻辑实现。
比如前面提到的Plugin Process,最初是放在Web Process中实现的,后来来演化到和UI Process、Web Process使用共同的独立进程的做法。这个过程可能很多的决策,考虑不同的需求、复杂度控制等等同题。
WebKit的开发者也会面临这样的问题,比如下面这段注释就清楚表明了作者将残余的重复列为一个优化项:
// FIXME: There is much code here that is duplicated in WebProcessMainMac.mm,
// we should add a shared base class where we can put everything.
而且提到应当使用一个新的共享基类来完成这个工作,这也是模块化工作的一部分:分工,以降低复杂度。
总结
最后再以<<Unix编程艺术>>中提炼出的KISS原则来做个总结。模块化设计利用模块化在上层得出一个简洁清晰的逻辑关系,使得在每个层级的复杂度都是可控的,将不必要的复杂性隐藏起来。Keep It Simple, Stupid!
参考: 温煜 <<软件架构设计>>
作者:horkychen