扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
模块化 —— Keep it clean, keep it simple
程序员所面对的复杂性日益增大,而划分代码的方法也有一个自然的发展过程。一开始,软件不过是一大块机器代码。最早的过程化语言带来了“依据子例程划分代码”的观念,接下来我们发明了程序库,为不同程序提供公共服务。再下来我们发明了独立地址空间和进程间通信。如今,我们已经惯常于把程序系统分布在彼此相隔万里的互联主机上。
Unix的早期开发者同时也是软件模块化思想的先锋。在他们之前,模块化只是计算机科学的思想,还不是工程实践。其弘旨是:要开发可正确工作的复杂软件的唯一途径,就是用定义良好的界面把诸多简单模块连结起来,形成整个系统。只有如此,大部分局部问题才可能在局部得到修正或者优化,而不至于破坏整体。
今天所有的程序员都被教育要在子程序的层次上进行模块化。有些幸运者掌握了在抽象数据类型(ADT)层次上的模块化能力,就已经被认为是好的设计者了。如今的设计模式运动,就是希望把这个层次再提高一步,发现那些有助于对程序大型结构进行良好组织的成功的设计抽象。
封装和最佳模块体积
模块之间的API有双重身份。在实现层次,API函数扼守模块之间的连接点,阻止内部信息外泄。在设计层次,API实际上定义了你的系统架构。
模块分解越细致,模块越小,API的定义越重要。整体的复杂度、错误的可能性也随之降低。
然而,如果分解过度,导致过小的模块,会得到意想不到的情况。下面的图来自Hatton 1997年的统计数据。可见到图形是U形的。
Hatton的数据是在不同语言和不同平台上经过广泛统计的道德,故具有可信性。可见,代码量在200到400逻辑行之间的模块效果最佳。
紧凑性和正交性
代码并不是唯一具有所谓“最佳单块体积”的软件要素。语言和APIs同样受到人类认识能力的限制而逃不出Hatton的U曲线。
因此,Unix程序员在设计APIs、命令集、协议以及其他东西的艰苦思索过程中发现了模块化的两个要素:紧密性和正交性。
紧凑性
紧凑性是指设计对于人脑而言“易于理解和接受的程度”。
紧凑的软件工具跟顺手的日常手工工具一样拥有很多优点。它让人们乐于使用,用起来方便自然,大大提高你的效率,而且安全可靠,不像那些复杂的工具,动不动就弄伤你。
紧凑并不意味着功能弱,如果一个设计建筑在容易理解的抽象基础之上,那么它可以非常强大和灵活,同时又保持紧凑。紧凑也不意味着容易学习,你可能必须先理解抽象之下精致的概念模型,之后才能感到容易。
软件很少有绝对紧凑的,但是很多软件是相对紧凑的,它们有一个紧凑的工作集,一个功能子集,可以满足80%甚至更多的专家级用户的日常需求。
举例来说,Unix系统调用API就是紧凑的,而C标准库则不是。Unix工具中make(1)是紧凑的,autoconf(1)和automake(1)则不是。标记语言中,HTML是紧凑的,而DocBook不是。Man(7)是紧凑的,troff(1)不是。通用语言中,C和Python是紧凑的,Perl, Java, Emacs Lisp和Shell不是。C++则是“反紧凑”的——该语言的设计者自己都承认,他不指望有人能够完全理解C++。
不过也不是说不紧凑的设计就是邪恶的或者糟糕的。有些问题域太复杂,紧凑的设计无法实现。这里强调紧凑性,其目的并不是希望读者把它看成是一个绝对要求,而是像Unix程序员那样,合理对待,尽力实践,决不轻易放弃。
正交性
正交性有助于你将复杂设计紧凑化,在这一点上,它的重要性非常突出。在纯正交的设计中,操作没有副作用,每一个行动只改变一件事情,不影响其它东西。对于系统中的每一个属性,有且只有一条途径去改变它。
电脑监视器是正交性的良好范例,你可以调明暗而不影响饱和度,色彩平衡的控制也彼此独立。如果不是如此,想象一下你会遇到多大麻烦!
软件中的非正交性设计太多了。举个例子,格式转换函数的作者经常会不假思索地要一个源文件的路径名作为参数,可是输入经常来自一个打开的文件句柄,如果设计成正交的,则该函数不应该有“打开文件”的副作用,则将来这个函数可以处理来自各种源头的数据流。
Doug McIloy的名言“只做好一件事”经常被认为是关于简单性的箴言,而其实这句话里对于正交性的强调至少是同样分量的。
非正交性的主要问题是副作用扰乱了程序员和用户的思维模型,而且经常被遗忘,带来程度不同的灾难。
Unix API整体上是一个很好的正交设计范例,正因为如此,在其他平台上的C库都尽力模仿它。所以就算你不是Unix程序员,也值得研究它。
SPOT法则
The Pragmatic Programmer 一书中指出了一类特别重要的正交性,“Don’t Repeat Yourself”法则:任何知识点应当是唯一的,无歧义的,在系统中以确定无疑的方式存在的。在本书里,我遵循Brain Kernighan的建议,把这个法则称为Single Point Of Truth,或简称SPOT法则。
重复导致不一致,对代码构成潜在的危害。因为如果你要改变重复信息中的一个,就必须记得改变它所有的化身。这体现出你根本没有清晰地组织你的代码。
软件是多层的
宽泛地说,当你在设计函数或者对象层次结构(hierarchy)时,有两个方向可供选择,而你的选择对于代码的分层(layering)将有重大的影响。
自顶向下,自底向上
一个方向是自下而上,从问题域中一定会用到具体的操作出发向上——从具体到抽象。举个例子,如果你要为磁盘驱动器开发一个固件(firmware),则在低层可以有一些操作原语如“磁头移至某物理块”,“读物理快”,“写物理快”,“切换LED”等。
另一个方向是自上而下,从抽象到具体,从最顶层的程序或者逻辑整体描述规范出发向下到个别的操作。比如某人设计一个可以控制不同介质的海量存储器控制器,可以从抽象的操作出发,比如“寻址逻辑块”,“读逻辑块”,“写逻辑块”,“切换指示设备”。这上面所说的硬件层次的操作很不相同,
一个大一些的例子是Web浏览器。自顶向下的设计从一个规范说明出发——能接受哪些URL类型,能显示哪些图像,对Java和JavaScript支持如何,等等。与这个自顶向下的视图相对应的实现层是应用的主事件循环。
同时Web浏览器必须要调用大量的专用元操作(primitives)。比如建立网络连接,发送数据,接受响应,比如GUI相关的操作,比如HTML解析操作。
从哪端开始,这事关重大,因为你的起点很可能对你的终点构成了限制。如果你完全的自顶向下,到一定时候你可能会尴尬地发现,逻辑上所需要的元操作实际上不能完全实现。如果你完全的自低向上,你会发现自己做了大量与程序无关的事情。
从1960年代起,初级程序员们就被教导说,写程序应该“自顶向下,逐渐细分”。自顶向下在下面三个条件成立的时候,是很好的经验:a. 你可以事先经确定义程序的需求,b. 在实现过程中,该规范不大可能变化,c. 在最底层,你有充分的自由来选择完成工作的方式。
程序层次越高,这些条件越容易被满足。然而,即使在最高层次的应用程序开发中,这些条件仍然经常不成立。
出入自我保护,程序员试图双管齐下。一方面以自顶向下的应用逻辑表达抽象规范,另一方面用函数和库来归纳领域内的元操作,在高层设计发生变化时可以复用之。
Unix程序员主要做系统程序设计,所以倾向于自底向上的开发方式。
一般来说,自底向上的开发更有吸引力,它使你以一种探索的方式开发,给你相对充裕的时间去细化含糊的规范,也更加符合程序员天生的懒惰——一旦出错,报废的代码通常要少得多。
不过实际的代码一般是自顶向下和自底向上向结合的。两者经常在一个项目中运用,这直接导致了胶合层(glue layer)的出现。
胶合层
当自顶向下和自底向上的汽车撞在一起的时候,情形通常是一片混乱。顶层的应用逻辑和底层的元操作必须由胶合层来阻隔。
几十年来,Unix程序员明白了一个道理,胶合层是令人厌恶的东西,应该让粘结层越薄越好,此乃性命攸关之大事!胶合层应该用来把东西粘在一起,而不是用来掩盖层与层之间的冲突和裂痕。
拿上面那个浏览器的例子来说,粘结层包括:把由HTML解析而来的文档对象应设为显示缓冲区里的位图。这部分代码是声名狼藉的难写,错误百出。HTML解析和GUI库的错误和缺陷都会在这层里表现出来。
浏览器的胶合层不仅要在规范和元操作之间充当中介者,还要在若干不同的外部规范中间充当中介者——HTTP网络协议的行为,HTML文档结构,不同的图形和多媒体格式,以及来自GUI的用户行为。
一层胶合层已经很容易出错了,但这还不是最糟糕的。如果一个设计者意识到胶合层的存在,并且试图去用自己的一套数据结构或者对象把这个胶合层组织到一个中间层中,那么结果就会是多出两个胶合层——一个在那个中间层之上,一个在其下。那些聪明但却欠缺历练的程序员经常积极地跳到这个陷阱里去。他们把基本的类(应用逻辑,中层和元操作)做得像课本上的例子那样漂亮,最后却为了把这些漂亮的代码粘合到一起而在很多个越来越厚的胶合层中忙得团团转,直到困死。
C语言本身被认为是薄胶合层的良好范例。
Unix和面向对象语言
自1980年代中期开始,新的语言纷纷宣称自己对面向对象编程提供直接支持。
OO设计的概念首先在图形系统,GUI系统和仿真系统里被证明是很有价值的。然而历史证明,在这些领域之外,OO并没有带来明显的益处,这令很多人感到吃惊,感到幻灭。应该试图去理解其中的道理,这将会是很有意义的事情。
在Unix传统的模块化技术与围绕OO语言发展起来的模式之间,存在着一些冲突和张力。Unix程序员较之其他人对于OO抱有更大的怀疑态度。原因之一是多样性法则。OO被说成是软件复杂性问题唯一正确的解决之道,这未免令人生疑。不过,还有更深层的原因。
我们刚才提到,Unix的模块化传统中,薄胶合层是一个重要原则,从顶层程序对象到下层硬件之间的抽象层越少越好。
这部分是因为C的影响。在C中间模拟真正的对象是件很费力的工作。因此,叠置一大堆抽象层简直是要人老命的事情。因此,在C中的对象层次倾向于平坦和透明。长此以往,Unix程序员使用其他语言也习惯于薄粘接/浅层次。
OO语言使得抽象变得容易了——也许是太容易了。它鼓励整个架构具有厚厚的、精致的胶合层。如果问题域确实复杂,确实需要大量的抽象,这可能是好事。但是这也是很糟糕的事,因为程序员最后会把很简单的事情用很复杂的办法来做,仅仅因为他们可以这么做。
所有的OO语言都有有一些倾向,吸引程序员跳进“过度分层”的陷阱里。对象框架和对象浏览器并不能取代好的设计和文档,但是却经常被看成一回事。太多的层次破坏了透明性——我们很难看穿下面的东西,很难在思想上对于代码的功能建立清晰的模型。简单性、明晰性和透明性一口气全被破坏了,结果代码充满了晦涩的错误,带来严重的维护性问题。
这种情况还在继续恶化,很多培训班把厚厚的软件分层当成好东西传授——你拥有那一大堆类被认为是数据中所潜藏的知识的体现。问题在于,在胶合层中的“smart data”经常与程序所操作的自然实体无关,而仅仅只是胶合本身。(一个确切的标志就是抽象子类的不断增值,以及所谓的“minxins”。)
Unix程序员对这些问题有本能的直觉。Unix中OO语言没有能够替代非OO语言如C,Perl(虽然支持OO,但很少有人用到),Shell等,这大概是原因之一。Unix世界里对于OO的批评比别的领域中要尖刻得多。Unix程序员知道什么时候不应该用OO,就算是要用OO,他们也尽可能的保持对象设计的简洁。正如Michael Padlipsky所说:“如果你知道你在干什么,三层足够;如果你不知道你在干什么,十七层也没用。”
OO在GUI、仿真和图形领域里取得成功的原因,可能是因为在这些领域中,相对而言,比较容易解决“类型存在与否”的问题。例如,在GUI和图形系统中,类和可视对象之间存在着自然的映射关系。如果你发现自己所增加的类并不直接映射可视对象,则你也可能就会发现胶合层已经变得很厚。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者