正在困扰如何能深一步的学习技术时发现了这篇文章,实在是写的好,让人有茅塞顿开之感web
如何阅读他人的程序代码数据库
文/王建兴设计模式
做者简介: 王建兴,清华大学资讯工程系的博士研究生,研究兴趣包括计算机网络、点对点网络、分布式网络管理、以及行动式代理人,专长则是Internet应用系统的开发。曾参与过的开发项目性质十分普遍并且不一样,从ERP、PC Game到P2P网络电话都在他的涉猎范围以内。网络
1、读懂程序代码,使心法皆为我所用数据结构
程序代码是别人写的,只有原做者才真的了解程序代码的用途及涵义。许多程序人内心都有一种不自觉的恐惧感,深怕被迫去碰触其余人所写的程序代码。可是,与其抗拒接收别人的程序代码,不如完全了解相关的语言和惯例,当成是培养自我实力的基石。多线程
对大多数的程序人来讲,撰写程序代码或许是使人开心的一件事情,但我相信,有更多人视阅读他人所写成的程序代码为畏途。许多人宁肯本身从新写过一遍程序代码,也不肯意接收别人的程序代码,进而修正错误、维护它们、甚至增强功能。架构
这其中的关键究竟在何处呢?如果一语道破,其实也很简单,程序代码是别人写的,只有原做者才真的了解程序代码的用途及涵义。许多程序人内心都有一种不自觉的恐惧感,深怕被迫去碰触其余人所写的程序代码。这是来自于人类心里深处对于陌生事物的原始恐惧。框架
读懂别人写的程序代码,让你收获满满编辑器
不过,基于许多现实的缘由,程序人时常受迫要去接收别人的程序代码。例如,同事离职了,必须接手他遗留下来的工做;也有可能你是刚进部门的菜鸟,而同事经验值够了、升级了,风水轮流转,一代菜鸟换菜鸟。甚至,你的公司所承接的项目,必须接手或是整合客户前一个厂商所遗留下来的系统,大家手上只有那套系统的原始码(运气好时,还有数量不等的文件)。分布式
诸如此类的故事,其实时常在程序人身边或身上持续上演着。许多程序人都将接手他人的程序代码,当作一件悲惨的事情。每一个人都不想接手别人所撰写的程序代码,由于不想花时间去探索,宁肯将生产力花在产生新的程序代码,而不是耗费在了解这些程序代码上。
很遗憾的是,上述的状况对程序人来讲很难避免。咱们老是必须碰触到其余人所写成的程序代码,甚至必须了解它、加以修改。对于这项需求,在现今开放原始码的风气如此盛行的今日,正如以前的「程序设计2.0」文中所提到的,你能够透过开放原始码学习到新的技术、学习到高手的架构设计,大幅提升学习的效率及效果。你甚至能够直接自开放原始码项目中抽取、提炼出本身所需的程序代码,站在巨人的肩膀上,直接由彼端得到所需的生产力。从这个观点来看,读懂别人所写的程序代码,就再也不只是从负面观点的「被迫接收」,而是极具正面价值的「汲取养份」。
先了解系统架构与行为模式,再细读
假若撰写程序代码是程序人的重要技艺之一,那么读懂别人的程序代码、接着加以修改,也势必是另外一个重要的技艺。
若是你不能熟悉这项工做,不只在遭逢你所不肯面对的局面时,没法解决眼前接手他人程序代码的难题,更重要的是,当你看着眼前现成的程序代码,殊不知如何从中撷取本身所需,致使最后只能入宝山空手回,望之兴叹。
接触他人的程序代码,大体上能够分为三种程度:1、了解,2、修改、扩充,3、抽取、提炼。
了解别人的程序代码是最基础的工做,假若不能了解本身要处理的程序代码,就甭论修改或扩充,更不可能去芜存菁,从中萃取出本身所需,回收再利用别人所撰写的程序代码。
虽然说是「阅读」,但程序代码并不像文章或小说同样,透过这种作法,便可以得到必定程度的了解。阅读文章或小说时,几乎都是循序地阅读,你只消翻开第一页,一行行阅读下去便可。可是,有许多程序人在试着阅读其余人的程序代码时,却每每有不知如何读起的困难。
或许找到系统的第一页(也就是程序代码执行的启始点)并不难,可是复杂度高的系统,有时十分庞大,有时千头万绪。
从程序代码的启始点开始读起,一来要循序读完全部的程序代码旷日费时,二来透过这种方式来了解系统,很难在脑中构建出系统的面貌,进而了解到系统真正的行为。因此,阅读程序代码的重点,不在于读完每一行程序代码,而是在于有效率地透过探索及阅读,从而了解系统的架构及行为模式。以便在你须要了解任何片断的细节实做时,可以很快在脑上对映到具体的程序代码位置,直到那一刻,才是细读的时机。
熟悉沟通语言与惯例用语
不论如何,有些基本的准备,是阅读他人程序代码时必需要有的。
首先,你最好得了解程序代码写成的程序语言。想要读懂法文写成的小说,总不能连法文都不懂吧。有些状况则很特殊。咱们虽然不懂该程序代码撰写所用的语言,可是由于现代语言的高阶化,并且流行的程序语言多半都是血统相近,因此即便不那么熟悉,有时也可勉力为之。
除了认识所用语言以外,再来就是要先确认程序代码所用的命名惯例(naming convention)。了解命名惯例很重要,不一样的程序人或开发团队,差别可能很大。
这命名惯例涵盖的范围一般包括了变量的名称、函式的名称、类别(若是是面向对象的话)的名称、原始码档案、甚至是项目建构目录的名称。假若使用了像设计模式之类的方法,这些名称更有一些具体的表述方式。
命名惯例有点像是程序人在程序语言之上,另行建构的一组沟通行话。程序人会透过共通约束、遵照的命名惯例,来表达一些较高阶的概念。例如,有名的匈牙利式命名法,便将变量名称以属性、型别、说明合并在一块儿描述。对程序人来讲,这种方式可以提供更丰富的信息,以了解该变量的做用及性质。
对程序代码阅读来讲,熟悉这个作法之因此重要,是由于当你了解整个系统所采用的惯例时,你便能试着以他们所共同操用的语汇来进行理解。假若,不能了解其所用的惯例,那么这些额外提供的信息,就没法为你所用。像以设计模式写成的程序代码,一样到处充满着模式的名称,诸如:Factory、Facade、Proxy等等。以这些名称指涉的类别,也直接透过名称,表达了它们自身的做用。对于懂得这命名惯例的读者来讲,不须要深刻探索,也能很快捕捉到这些类别的意义。
当你拿到一套必须阅读的程序代码时,最好先取得命名惯例的说明文件。然而,并非每套程序代码都附有此类的说明文件。另外一个方式,就是本身到程序代码中,大略浏览一遍,有经验的程序人能够轻易发掘出该系统所用的命名惯例。
常见的命名方式不脱那几类,这时候经验就很重要,假若你知道的惯例越多,就越能轻易识别他人所用的惯例。若是运气很糟,程序代码所用的惯例是前所未见的,那么你也得花点时间概括,凭本身的力量找出这程序代码命名上的规则。
掌握程序代码撰写者的心态与习惯
大多数的程序代码,基本上都依循一致的命名惯例。不过运气更差的时候,一套系统中可能会充斥着多套命名惯例。这有多是由于开发团队由多组人马所构成,每组人马都有不一样的文化,而在项目开发管理又没有管控得宜所形成。最糟的状况,程序代码彻底没有明显的惯例可言,这时候阅读的难度就更高了。
想要阅读程序代码,得先试着体会程序代码做者的「心」。想要这么作,就得多了解对方所使用的语言,以及惯常运用的语汇。在下一回中,咱们将继续探讨阅读程序代码的相关议题。
2、摸清架构,即可轻松掌握全貌
在本文中,咱们的重点放在:要了解一个系统,最好是采起由上至下的方式。先试着捕捉系统架构性的观念,不要过早钻进细节,由于那一般对于你了解全貌,没有多大的帮助。阅读程序代码不须要从第一行读起,咱们的目的并非在于读遍每一段程序代码。
基于许多缘由,程序人须要阅读其余人所写成的程序代码。而对程序设计2.0时代的程序人来讲,最正面的价值在于,能读懂别人程序的人,才有能力从中萃取本身所需的程序,藉以提升生产力。
阅读程序代码的目的,在于了解全貌而非细节
想要读懂别人程序代码的根本基础,即是了解对方所用的程序语言及命名惯例。有了这个基础以后,才算是具有了基本的阅读能力。正如我以前提到的──想要读懂法文写成的小说,总不能连法文都不懂吧。阅读程序代码和阅读文学做品,都须要了解撰写所用的语言及做者习用的语汇。
但咱们在阅读文学做品一般是采循序的方式,也就是从第一页开始,一行一行地读下去,依循做者为你铺陈的步调,逐渐进到他为你准备好的世界里。
阅读程序代码却大大不一样。咱们不多从第一行开始读起,由于除非它是很简单的单线程程序,不然不多这么作。由于要是这么作,就很难了解整个系统的全貌。
是的,咱们这边提到了一个重点,阅读程序代码的目的在于了解系统的全貌,而不是在于只是为了地毯式的读遍每一段程序代码。
就拿面向对象程序语言所写成的系统来讲,整个系统被拆解、分析成为一个个独立的类别。阅读个别类别的程序代码,或许能够明白每项类别对象个别的行为。但对于各种别对象之间如何交互影响、如何协同工做,又很容易陷入盲人摸象的困境。这是由于各种别的程序代码,只描述个别对象的行为,而片断的阅读就只能造就片面的认识。
由上而下厘清架构后,即可轻易理解组成关系
若是你想要跳脱困境,不想浪费大量时间阅读程序代码,却始终只能捕捉到对系统片断认识,就必须转换到另外一种观点来看待系统。从个别的类别行为着手,是由下至上(Bottom-Up)的方法;在阅读程序代码时,却应该先采由上至下(Top-Down)的方式。对程序代码的阅读来讲,由上至下意谓着,你得先了解整个系统架构。
系统的架构是整个系统的骨干、支柱。它表现出系统最突出的特征。知道系统架构究竟属于那一种类型,一般大大有益于了解系统的个别组成之间的静态及动态关系。
有些系统由于所用的技术或框架的关系,决定了最上层的架构。例如,采用Java Servlet/JSP技术的应用系统,最外层的架构即是以J2EE(或起码J2EE中的Web Container)为根本。
使用Java Servlet/JSP技术时,决定了某些组成之间的关系。例如,Web Container依据web.xml的内容加载全部的Servlets、Listeners、以及Filters。每当Context发生事件(例如初始化)时,它便会通知Listener类别。每当它收到来自客户端的请求时,便会依循设定的全部Filter Chain,让每一个Filter都有机会检查并处理此一请求,最后再将请求导至用来处理该请求的Servlet。
当咱们明白某个系统采用这样的架构时,即可以很容易地知道各个组成之间的关系。即便咱们还不知道究竟有多少Servlets,但咱们会知道,每当收到一个请求时,老是会有个相对应的Servlet来处理它。当想要关注某个请求如何处理时,我应该去找出这个请求对应的Servlet。
了解架构,必需要加上层次感
一样的,以Java写成的Web应用程序中,也许会应用诸如Struts之类的MVC框架,以及像Hibernate这样的数据存取框架。它们均可以视为最主要的架构下的较次级架构。而各个应用系统,甚至有可能在Struts及Hibernate之下,创建自有的更次级的架构。
也就是说,当咱们谈到「架构」这样的观念时,必需要有层次感。而不管是那一层级的架构,都会定义出各自的角色,以及角色间的关系。对阅读者来讲,相较于直接切入最细微的单一角色行为,不如了解某个特定的架构中,究竟存在多少角色,以及这些角色之间的互动模式,比较可以帮助咱们了解整个系统的运做方式。
这是一个很重要的关键,当你试着进到最细节处以前,应该先试着找出参与的角色,及他们之间的关系。例如,对事件驱动式的架构而言,有3个很重要的角色。一个是事件处理的分派器(Event Dispatcher)、一个是事件产生者(Event Generator)、另外一个则是事件处理器(Event Handler)。
事件产生器产生事件,并送至事件分派器,而事件分派器负责找出各事件相对应的事件处理器,而且转交该事件,并命令事件处理器加以处理。像Windows的GUI应用程序,即是采用事件驱动式的架构。
当你知道此类的应用程序皆为事件驱动式的架构时,你即可以进一步得知,在这样的架构下会有3种主要的角色。虽然也许还不清楚整个系统中,究竟会须要处理多少事件的类型,但对你而言,已经创建了对系统全貌最概观的认识。
虽然你还不清楚全部的细节,但诸如确切会有那些事件类型之类的信息,在此刻还不重要──不要忘了,咱们采起的是由上而下的方式,要先摸清楚主建筑结构,至于壁纸的花色怎么处理,那是到了尾声时才会作的事。
探索架构的第一件事:找出系统如何初始化
有经验的程序人,对于时常被运用的架构都很熟悉。经常只须要瞧上几眼,就能明白一个系统所用的架构,天然就可以直接联想到其中会存在的角色,以及角色间的关系。
然而,并非每一个系统所用的架构,都是大众所熟悉,或是一眼可以望穿的。这时候,你须要探索。目标一样要放在界定其中的角色、以及角色间的静态、动态关系。
不论某个系统所采用的架构是否为大部分人所熟知的,在试着探索一个系统的长相时,咱们应该找出来几个答案,了解在它所用的架构下,下列这件事是如何被完成的:1、系统如何初始化,2、与这个系统相接的其余系统(或用户)有那些,而相接的接口又是什么;3、系统如何反应各类事件,4、系统如何处理各类异常及错误。
系统如何初始化是很重要的一件事,由于初始化是为了接下来的全部事物而作的准备。从初始化的方式、内容,能知道系统作了什么准备,对于系统会有什么行为展示,也就能得窥一二了。
之因此要了解与系统相接的其余系统(或用户),为的是要界定出系统的边界。其余的系统可能会提供输入给咱们所探索的系统,也可能接收来自这系统的输出,了解这边界所在,才能肯定系统的外观。
而系统所反应的事件类型、以及如何反应,基本上就表明着系统自己的主要行为模式。最后,咱们必须了解系统处理异常及错误的方式,这一样也是系统的重要行为,但容易被忽略。
以前,咱们提到必须先具有一个系统的语言基础,才可以进一步加以阅读,而在本文中,咱们的重点放在:要了解一个系统,最好是采起由上至下的方式。先试着捕捉系统架构性的观念,不要过早钻进细节,由于那一般对于你了解全貌,没有多大的帮助。
3、优质工具在手,读懂程序非难事
系统的复杂度每每超过人脑的负荷。阅读程序代码的时候,你会须要更多工具提供协助。使用好的集成开发环境(IDE)或文本编辑器,就能提供最基本的帮助。
阅读程序代码的动做,能够是很原始的,利用最简单的文本编辑器,逐一开启原始码,而后凭借着一己的组织能力,在不一样的程序代码间跳跃,拼凑出脑中想要构建的图像。
不过,系统的复杂度每每超过人脑的负荷。阅读程序代码的时候,你会须要更多工具提供协助。使用好的集成开发环境(IDE)或文本编辑器,就能提供最基本的帮助。
善用文本编辑器或IDE,加速解读程序代码
许多文本编辑器提供了常见程序语言的语法及关键词标示功能。这对于阅读来讲,绝对可以起很大的做用。有些文本编辑器(例如我经常使用的EditPlus及偶而使用的Notepad++),甚至可以自动列出某个原始档中全部定义的函式清单,更容许你直接从清单中选择函式,直接跳跃到该函式的定义位置。这对于阅读程序代码的人来讲,就提供了极佳的便利性。
由于在阅读程序代码时,最常作的事,就是随着程序中的某个控制流,将阅读的重心,从某个函式移至它所呼叫的另外一个函式。因此对程序人来讲,阅读程序代码时最常作的事之一就是:找出某个函式位在那一个原始档里,接着找到该函式所在的位置。
好的IDE可以提供的协助就更多了。有些可以自动呈现一些额外的信息,最有用的莫过于函式的原型宣告了。例如,有些IDE支持当光标停留在某函式名称上一段时间后,它会以Tooltip的方式显示该函式的原型宣告。
对阅读程序代码的人来讲,在看到程序代码中呼叫到某个函式时,能够直接利用这样的支持,当即取得和这个函式有关的原型信息,立刻就能知道呼叫该函式所传入的各个自变量的意义,而没必要等到将该函式的定义位置找出后,才能明白这件事。
grep是一个基本而极为有用的工具
除了选用好的文本编辑器或IDE以外,还有一个基本、但却极为有用的工具,它就是grep。熟悉Unix操做系统的程序人,对grep这个公用程序多半都不陌生。Grep最大的用途,在于它容许咱们搜寻某个目录(包括递归进入全部子目录)中全部指定档案,是否有符合指定条件(常数字符串或正规表示式)档案。
假若有的话,则能帮你指出所在的位置。这在阅读程序代码时的做用极大。当咱们随着阅读的脚步,赶上了任何一个不认识、但自认为重要的类别、函式、数据结构定义或变量,咱们就得找出它究竟位在这茫茫程序代码海中的何处,才能将这个图块从未知变为已知。
grep之因此好用,就是在于当咱们发现某个未知的事物时,能够轻易地利用它找出这个未知的事物究竟位在何方。此外,虽然说grep是Unix的标准公用程序之一,可是像Windows这样子的平台,也有各类类型的grep程序。对于在Windows环境工做的程序人来讲,能够自行选用以为称手的工具。
gtags可创建索引,让搜寻更有效率
grep虽然好用,可是仍然有一些不足之处。第一个缺点在于它并不会为所搜寻的原始码档案索引。每当你搜寻时,它都会逐一地找出全部的档案,而且读取其中的全部内容,过滤出知足指定条件的档案。当项目的原始码数量太大时,就会产生搜寻效率不高的问题。
第二个缺点是它只是一个单纯的文本文件搜寻工具,自己并不会剖析原始码所对应的语言语法。当咱们只想针对「函式」名称进行搜寻时,它有可能将批注中含有该名称的原始码,也一并找了出来。
针对grep的缺点,打算阅读他人程序代码的程序人,能够考虑使用像是gtags这样子的工具。gtags是GNU GLOBAL source code tag system,它不仅搜寻文字层次,并且由于具有了各类语言的语法剖析器,因此在搜寻时,能够只针对和语言有关的元素,例如类别名称、函式名称等。
并且,它能针对原始码的内容进行索引,这意谓一旦建好索引以后,每次搜寻的动做,都毋需从新读取全部原始码的内容并逐一搜寻。只须要以现成的索引结构为基础,便可有效率的寻找关键段落。
gtags提供了基于命令行的程序,让你指定原始码所在的目录执行创建索引的动做。它同时也提供程序让你得如同操做grep通常,针对索引结构进行搜寻及检索。它提供了许多有用的检索方式,例如找出项目中定义某个数据结构的档案及定义所在的行号,或者是找出项目中全部引用某数据结构的档案,以及引用处的行号。
这么一来,你就能够轻易地针对阅读程序代码时的需求予以检索。相较于grep所能提供的支持,gtags这样的工具,简直是强大许多。
再搭配htags制做HTML文件,更是如虎添翼
还有一个绝对须要一提的工具。这个叫作htags的工具,可以帮你将已制做完成的索引结构,制做成为一组相互参考的HTML文件。基本上,利用这样的HTML文件阅读程序代码,比起单纯地直接阅读原始码,来得更有结构。缘由是阅读程序代码时,这样的HTML文件,已经为你创建起在各个原始码档案片断间跳跃的链结。例如,图一(略)是针对一个有名的开放原始码项目ffmpeg,由gtags所产生出来的HTML文件首页的一部分。
htags工具首先为你找出全部定义main()函式的档案,而且列出所在的函式。找出main()函式,时常是阅读程序代码的第一步,由于main()函式是程序的主要入口点,全部的动做皆由此启动,它是一切事物的源头。
凭借htags制做的HTML文件,你能够轻易地点击超连接,直接进到main()函式所在的代码段,如图二(略)。
当咱们检视上述原始码时,发现av_register_all()是个陌生、没法了解的事物,而想要搞懂它到底是什么,能够再继续点击这个函式,如图三(略)。这真是太方便了!阅读至此,你会猛然发现,gtags似乎就是为了阅读程序代码而专门量身打造的利器。
4、望文生义,进而推敲组件的做用
先创建系统的架构性认识,而后透过名称及命名惯例,就能够推测出各组件的做用。例如:当Winamp尝试着初始化一个Plug-In时,它会呼叫这个结构中的init函式,以便让每一个Plug-In程序有机会初始化本身。当Winamp打算结束本身或结束某个Plug-In的执行时,便会呼叫quit函式。
在阅读程序代码的细节以前,咱们应先试着捕捉系统的运做情境。在采起由上至下的方式时,系统性的架构是最顶端的层次,而系统的运做情境,则是在它之下的另外一个层次。
好的说明文件难求,拼凑故事的能力很重要
有些系统提供良善的说明文件,也许还利用UML充分描述系统的运做情境。那么对于阅读者来讲,从系统的分析及设计文件着手,即是快速了解系统运做情境的一个途径。
可是,并非每一个软件项目都伴随着良好的系统文件,而许多极具价值的开放原始码项目,也时常不具有此类的文件。对此,阅读者必须尝试自行捕捉,并适度地记录捕捉到的运做情境。
我喜欢将系统的运做情境,比拟成系统会上演的故事情节。在阅读细节性质的程序代码前,先知道系统究竟会发生那些故事,是必备的基本功课。你能够利用熟悉或者本身发明的表示工具,描述你所找到的情境。甚至能够只利用简单的列表,直接将它们列出。只要可以达到记录的目的,对程序代码阅读来讲,都可以提供帮助。或者,你也能够利用UML中的类别图、合做图、循序图之类的表示方法,作出更详细的描述。
当你可以列出系统可能会有的情境,表示你对系统所具有的功能,以及在各类状况下的反应,都具有归纳性的认识。以此为基础,即可在任何须要的时候,钻进细节处深刻了解。
探索架构的第一步──找到程序的入口
在以前,咱们在一个开发项目中,曾经须要将系统所获得的MP3音讯文件,放至iPod这个极受欢迎的播放设备中。
虽然iPod自己也能够作为可移动式的储存设备,但并非单纯地将MP3档案放到iPod中,就可让iPod的播放器认得这个档案,甚至可以加以播放。
这是由于iPod利用一个特殊的档案结构(iTunes DB),记录播放器中可供播放的乐曲、播放列表以及乐曲信息(例如专辑名称、乐曲长度、演唱者等)。为了了解而且试着重复使用既有的程序代码,咱们找到了一个Winamp的iPod插件(Plug-In)。
Winamp是我的计算机上极受欢迎的播放软件,而咱们找到的插件,能让Winamp直接显示链接至计算机的iPod中的歌曲信息,而且容许Winamp直接播放。
咱们追踪与阅读这个插件的思路及步骤以下,首先,咱们要先了解插件的系统架构。很明显的,大概浏览过原始码后,咱们注意到它依循着WinAmp为Plug-In程序所制定的规范,也就是说,它是实做成Windows上的DLL,而且透过一个叫作winampGetMediaLibraryPlugin的DLL函式,提供一个名为winampMediaLibraryPlugin的结构。
当咱们不清楚系统的架构究竟为什么时,咱们会试着探索,而第一步,即是找到程序的入口。如何找到呢?这会依程序的性质不一样而有所差异。
对一个自己就是可独立执行的程序来讲,咱们会找启动程序的主要函式,例如对C/C++来讲就是main(),而对Java来讲,即是static void main()。在找到入口后,再逐一追踪,摸索出系统的架构。
但有时,咱们所欲阅读的程序代码是类别库或函式库,它只是用来提供多个类别或函式供客户端程序(Client Program)使用,自己并不具单一入口,此类的程序代码具备多重的入口──每一个容许客户端程序呼叫的函式或类别,都是它可能的入口。
例如,对WinAmp的iPod Plug-In来讲,它是一个DLL形式的函式库,因此当咱们想了解它的架构时,必需要先找出它对外提供的函式,而对Windows DLL来讲,对外提供的函式,皆会以dllexport这个关键词来修饰。因此,不管是利用grep或gtags之类的工具,咱们能够很快从原始码中,找到它只有一个DLL函式(这对咱们而言,真是一个好消息),而这个函式即是上述的winampGetMediaLibraryPlugin。
系统多会采用相同的架构处理Plug-In程序
若是经验不够的话,也许没法直接猜出这个函式的做用。
不过,若是你是个有经验的程序人,多半能从函式所回传的结构,猜出这个函式实际的用途。而事实上,当你已经知道它是一个Plug-In程序时,就应该要明白,它可能采用的,就是许多系统都采用的相同架构处理Plug-In程序。
当一个系统采用所谓Plug-In形式的架构时,它一般不会知道它的Plug-In究竟会怎么实做、实做什么功能。它只会规范Plug-In程序须要知足某个特定接口。当系统初始化时,全部的Plug-In均可以依循相同的方式,向系统注册,合法宣示本身的存在。
虽然系统并不确切知道Plug-In会有什么行为展示,可是由于它制定了一个标准的接口,因此系统仍然能够预期每一个Plug-In可以处理的动做类型。这些动做具体上怎么执行,对系统来讲并不重要。这也正是面向对象程序设计中的「多型」观念。
随着实务经验,概括常见的架构模式
我想表达的重点,是当你「涉世越深」以后,所接触的架构越多,就越能举一反三。只须要瞧上几眼,就能明白系统所用的架构,天然就可以直接联想到其中可能存在的角色,以及角色间的关系。
像上述的Plug-In程序手法,时常能够在许多容许「外挂」程序代码的系统中看到。因此,有经验的阅读者,多半可以当即反应,知道像Winamp这样的系统,应该是让每一个Plug-In程序,都写成DLL函式库。
而每一个Plug-In的DLL函式库中,都必须提供winampGetMediaLibraryPlugin()这个函式(若是你熟悉Windows的程序设计,你会知道这是利用LoadLibrary()和GetProcAddress()来达成的一种多型手法)。若是你熟悉设计模式,你更会知道这是Simple Factory Method这个设计模式的运用。
winampGetMediaLibraryPlugin()所回传的winampMediaLibraryPlugin结构,正好就描述了每一个Winamp Plug-In的实做内容。
善用名称可加速了解
利用gtags这个工具,咱们当即发现,这个Plug-In它所定义的init、quit、PluginMessageProc这三个名称,都是函式名称。这暗示在多型的做用下,它们都是在某些时间点,会由Winamp核心本体呼叫的函式。
名称及命名惯例是很重要的。看到「init」,咱们会知道它的做用多半是进行初始化的动做,而「quit」大概就是结束时处理函式,而PluginMessageProc多半就是各类讯息的处理程序(Proc一般是procedure的简写,因此PluginMessageProc意指Plugin Message Procedure)了。
「望文生义」很重要,咱们看到函式的名称,就能够猜测到它所表明的做用,例如:当Winamp尝试着初始化一个Plug-In时,它会呼叫这个结构中的init函式,以便让每一个Plug-In程序有机会初始化本身;当Winamp打算结束本身或结束某个Plug-In的执行时,便会呼叫quit函式。当Winamp要和Plug-In程序沟通时,它会发送各类不一样的讯息至Plug-In,而Plug-In程序必须对此作出回应。
咱们甚至不须要检视这几个函式的内容,就能够作出推测,而这样的假设,事实上也是正确的。
5、找到程序入口,再由上而下抽丝剥茧
根据须要决定展开的层数,或展开特定节点,并记录树状结构,而后适度忽略不须要了解的细节─这是一个很重要的态度。由于你不会一次就须要全部的细节,阅读都是有目的的,每次的阅读也许都在探索程序中不一样的区域。
探索系统架构的第一步,就是找到程序的入口点。找到入口点后,多半采起由上而下(Top-Down)的方式,由最外层的结构,一层一层逐渐探索愈来愈多的细节。
咱们的开发团队曾针对Winamp的iPod plug-in进行阅读及探索,不只找到入口点,也找出、并理解它最根本的基础架构。从这个入口点,能够往下再展开一层,分别找到三个重要的组成及其意义:
● init():初始化动做
● quit():终止化动做
● PluginMessageProc():以讯息的方式处理程序所必须处理的各类事件
展开的同时,随手记录树状结构
当咱们从一个入口点找到三个分支后,能够顺着每一个分支再展开一层,因此分别继续阅读init、quit、以及PluginMessageProc的内容,并试着再展开一层。阅读的同时,你能够在文件中试着记录展开的树状结构。
● init():初始化动做
● itunesdb_init_cc():创建存取iTunes database的同步对象
● 初始化数据结构
● 初始化GUI元素
● 加载设定
● 创建log檔
● autoDetectIpod():侦测iPod插入的线程
● quit():终止化动做
● itunesdb_del_cc():终止存取iTunes database的同步对象
● 关闭log檔
● 终止化GUI元素
● PluginMessageProc():以讯息的方式处理程序所必须面临的各类事件
● 执行所链接之iPod的MessageProc()
这部分必需要留意几个重点。首先,应该一边阅读,一边记录文件。由于人的记忆力一般有限,对于陌生的事物更是容易遗忘,所以边阅读边记录,是很好的辅助。
再者,由于咱们采起由上而下的方式,从一个点再分支出去成为多个点,所以,一般也会以树状的方式记录。除此以外,每次只试着往下探索一层。从init()来看你便会明白。如下试着摘要init()的内容:
int init() {
itunesdb_init_cc();
currentiPod=NULL;
iPods = new C_ItemList;
…略
conf_file=(char*)SendMessage(plugin.hwndWinampParent,WM_WA_IPC,0,IPC_GETINIFILE);
m_treeview = GetDlgItem(plugin.hwnd LibraryParent,0x3fd);
//this number is actually magic :)
…略
g_detectAll = GetPrivateProfileInt("ml_ipod", "detectAll",0,conf_file)!=0;
…略
g_log=GetPrivateProfileInt("ml_ipod","log",0,conf_file)!=0;
…略
g_logfile=fopen(g_logfilepath,"a");
…略
autoDetectIpod();
return 0;
}
由于咱们只试着多探索一层,而目的是但愿发掘出下一层的子动做。因此在init()中看到像「itunesdb_init_cc();」这样的函数调用动做时,咱们知道它是在init()之下的一个独立子动做,因此能够直接将它列入。可是当看到以下的程序行:
currentiPod=NULL;
iPods = new C_ItemList;
咱们并不会将它视为init()下的一个独立的子动做。由于好几行程序,才构成一个具备独立抽象意义的子动做。例如以上这两行构成了一个独立的抽象意义,也就是初始化所需的数据结构。
理论上,原来的程序撰写者,有可能撰写一个叫作init_data_structure()的函式,包含这两行程序代码。这样作可读性更高,然而基于种种理由,原做者并无这么作。身为阅读者,必须自行解读,将这几行合并成单一个子动做,并赋予它一个独立的意义──初始化数据结构。
没法望文生义的函式,先试着预看一层
对于某些不明做用的函式叫用,不是望其文便能生其义的。当咱们看到「itunesdb_init_cc()」这个名称时,咱们或许能从「itunesdb_init」的字眼意识到这个函式和iPod所采用的iTunes database的初始化有关,但「cc」却实在使人费解。为了理解这一层某个子动做的真实意义,有时免不了要往前多看一层。
原来它是用来初始化同步化机制用的对象。做用在于这程序必定是用了某个内部的数据结构来储存iTunes database,而这数据结构有可能被多线程存取,因此必须以同步对象(此处是Windows的Critical Section)加以保护。
因此说,当咱们试着以树状的方式,逐一展开每一个动做的子动做时,有时必须多看一层,才能真正了解子动做的意义。由于有了这样的动做,咱们能够在展开树状结构中,为itunesdb_init_cc()附上补充说明:创建存取itunes database的同步对象。这么一来,当咱们在检视本身所写下的树状结构时,就能轻易一目了然的理解每一个子动做的真正做用。
根据须要了解的粒度,决定展开的层数
咱们究竟须要展开多少层呢?这个问题和阅读程序代码时所需的「粒度(Granularity)」有关。若是咱们只是须要归纳性的了解,那么也许展开两层或三层,就可以对程序有基础的认识。假若须要更深刻的了解,就会须要展开更多的层次才行。
有时候,你并非一视同仁地针对每一个动做,都展开到相同深度的层次。也许,你会基于特殊的需求,专门针对特定的动做展开至深层。例如,咱们阅读Winamp iPod plug-in的程序目录,实际上是想从中了解究竟应该如何存取iPod上的iTunes DB,使咱们可以将MP3歌曲或播放列表加至此DB中,并于iPod中播放。
当咱们层层探索与分解以后,找到了parseIpodDb(),从函式名称判断它是咱们想要的。由于它表明的正是parse iPod DB,正是咱们这次阅读的重点,也就达成阅读这程序代码的目的。
咱们强调一种不一样的作法:在阅读程序代码时,多半采起由上而下的方式;而本文建议了一种记录阅读的方式,就是试着记录探索追踪时层层展开的树状结构。你能够视本身须要,了解的深刻程度,再决定要展开的层数。你更能够依据特殊的须要,只展开某个特定的节点,以探索特定的细目。
适度地忽略不须要了解的细节,是一个很重要的态度,由于你不会一次就须要全部的细节,阅读都是有目的的。每次的阅读也许都在探索程序中不一样的区域;而每次探索时,你均可以增补树状结构中的某个子结构。渐渐地,你就会对这个程序更加的了解。
6、阅读的乐趣:透过程序代码认识做者
即使每一个人的写做模式多半受到他人的影响,程序人一般仍是会融合多种风格,而成为本身独有的特点,若是你知道做者程序设计的偏好,阅读他的程序代码就更驾轻就熟。
阅读程序代码时,多半会采起由上而下、抽丝剥茧的方式。透过记录层层展开的树状结构,程序人能够逐步地创建起对系统的架构观,并且能够依照须要的粒度(Granularity),决定展开的层次及精致程度。
创建架构观点的认识是最重要的事情。虽然这一系列的文章前提为「阅读他人的程序代码」,但咱们真正想作的工做,并不在于完全地详读每一行程序代码的细节,而是想要透太重点式的程序代码「摘读」,达到对系统所需程度的了解。每一个人在阅读程序代码的动机不尽相同,须要了解的程度也就有深浅的分别。只有极为少数的状况下,你才会须要细读每一行程序代码。
阅读程序代码是新时代程序人必备的重要技能
这一系列的文章至此已近尾声,回顾曾探讨的主题,咱们首先研究了阅读程序代码的动机。尤为在开放原始码的风气如此之盛的状况下,妥善利用开放原始码所提供的资源,不只可以更快学习到新的技术,同时在原始码版权合适时,还能够直接利用现成的程序代码,大幅地提升开发阶段的生产力。因此,阅读程序代码俨然成为了新时代程序人必备的重要技能之一。
接着,咱们提到了阅读程序代码前的必要准备,包括了对程序语言、命名惯例的了解等等。在此以后,咱们反复提起了「由上而下」的阅读方向的重要性。
由上而下的阅读方式,是由于咱们重视架构更胜于细节。从最外层的架构逐一贯内探索,每往内探索一层,咱们了解系统的粒度就增长了一个等级。当你识别出系统所用的架构时,便可以轻易了解在这个架构下会有的角色,以及它们之间的动态及静态的关系。如此一来,许多信息便不言可喻,毋需额外花费力气,便可以快速理解。
好的名称可以摘要性地点出实体的做用
追踪原始码时,当然能够用原本的方式,利用编辑器开启所需的档案,而后利用编辑器提供的机制阅读,可是假若可以善用工具,阅读程序代码的效率及质量都能大大提高。在本系列文章中,咱们介绍了一些工具,或许你还能够在坊间找到其余更有用的工具。
我在这一系列的文章中,实际带着你们阅读、追踪了一个名为ml_pod的开放原始码项目。它是一个Winamp的iPod plug-in程序。在追踪的过程当中,咱们试着印证这一系列文中所提到的观念及方法。咱们采用逐渐开展的树状结构来记录追踪的过程,并藉以创建起对系统的概观认识。
就原始码的阅读来讲,以前的讨论涉及了工具面及技巧面。但还有一些主题不在这两个范畴以内,例如,善用名称赋予你的提示。名称作为隐喻(Metaphor)的做用很大,好的名称可以摘要性地点出实体的做用,例如咱们看到autoDetectIpod(),天然而然可以想象它的做用在于自动(Auto)侦测(Detect)iPod的存在。
咱们在展开树状结构时,有时候须要预看一层,有时却不须要这么作,即可获得印证。程序人都会有惯用的名称以及组合名称的方法,假若可以从名称上理解,便毋需钻进细节,能够省去至关多的时间。例如,当咱们看到parseIpodDb()时,即可以轻易了解它是剖析(Parse)iPod的数据库(DB),所以便不须要当即钻进parseIpodDb()中查看底细。
尽管如此,可否理解程序人命名的用意,和自身的经验以及是否了解原做者的文化背景,是息息相关的。
命名自己就是一种文化产物。不一样的程序人文化,就会衍生出不一样的命名文化。当你本身的经验丰富,看过及接触过的程序代码也多时,对于名称的感觉及联想的能力天然会有不一样。
这种感觉和联想的能力,究竟应该如何精进,很难具体描述。就我我的的经验,多观察不一样命名体系的差别,而且尝试概括彼此之间的异同,有助于更快地提高对名称的感觉及联想力。
转换立场,理解做者的思考方式
除了工具及技巧以外,「想要阅读程序代码,得先试着阅读写这个程序代码的程序人的心。」这句话说来十分抽象,或许也使人难以理解。
当你在阅读一段程序代码时,或许能够试着转换本身的立场,从旁观者的角度转换成为写做者的心态,揣摩原做者的心理及处境。当你试着设身处地站在他的立场,透过他的思考方式来阅读、追踪他所写下的程序代码,将会感受更加流畅。
许多软件项目,都不是由单一程序人所独力完成。所以,在这样的项目中,便有可能呈现多种不一样的风格。
许多项目会由架构师决定主体的架构及运做,有既定实施的命名惯例,及程序设计须要遵照方针。在多人开发的模式下,越是好的软件项目,越看不出某代码段到底是由谁所写下的。
不过,有些开放原始码的项目,每每又整合了其余开放原始码的项目。有的时候,也很难求风格的统一,便会出现混杂的状况。比如以前提到的ml_pod项目,由于程序代码中混合了不一样的来源,而呈现风格不一致的状况。
我在阅读非本身所写的程序代码时,会观察原做者写做的习惯,藉以对应到脑中所记忆的多种写做模型。在阅读的过程当中,读完几行程序代码,我会试着猜测原做者在写下这段程序代码时的心境。他写下这段程序代码的用意是什么?为何他会采起这样的写法?顺着原做者的思考理路阅读,本身的思考才能更贴近对方写做当时的想法。
当你短暂化身为原做者时,才能更轻易的理解他所写下的程序代码。
若是你能知道原做者的背景,程序设计时的偏好,阅读他的程序代码,就更能驾轻就熟了。
从程序代码着手认识做者独有的风格,进而见贤思齐
我在阅读别人写下的程序代码时,我会试着猜测,原做者到底是属于那一种「流派」呢?每一个人都有本身独特的写做模式,即使每一个人的写做模式多半受到他人的影响──不管是书籍的做者、学习过程当中的指导者,或一同参与项目的同侪,但每一个程序人一般会融合多种风格,而成为本身独有的风格。
面向对象的基本教义派,老是会以他心中以为最优雅的面向对象方式来撰写程序。而阅读惯用、善用设计模式的程序人所写下的程序代码时,不难推想出他会在各类常见的应用情境下,套用哪些模式。
有些时候,在阅读之初,你并不知道原做者的习性跟喜爱,甚至你也不知道他的功力。可是,在阅读以后,你会慢慢地从一个程序人所写下的程序代码,开始认识他。
你或许会在阅读他人的程序代码时,发现使人拍案叫绝的技巧或设计。你也有可能在阅读的同时,发现原做者所留下的缺失或写做时的缺点,而暗自警戒于心。这也算是阅读他人程序代码时的一项乐趣。
当你从视阅读他人的程序代码为畏途,转变成为能够从中获取乐趣的时候,我想,你又进到了另外一个境界。