如何有效地阅读他人的程式码

<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:Verdana; panose-1:2 11 6 4 3 5 4 4 2 4; mso-font-charset:0; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:536871559 0 0 0 415 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:595.3pt 841.9pt; margin:1.0cm 1.0cm 1.0cm 1.0cm; mso-header-margin:0cm; mso-footer-margin:0cm; mso-paper-source:0; layout-grid:15.6pt;} div.Section1 {page:Section1;} -->

阅读他人的程式码( 1 --- 读懂程式码,使心法皆为我所用  

程式码是别人写的,只有原作者才真的了解程式码的用途及涵义。许多程式人心里都有一种不自觉的恐惧感,深怕被迫去碰触其他人所写的程式码。但是,与其抗拒接收别人的程式码,不如彻底了解相关的语言和惯例,当成是培养自我实力的基石。对大多数的程式人来说,撰写程式码或许是令人开心的一件事情,但我相信,有更多人视阅读他人所写成的程式码为畏途。许多人宁可自己重新写过一遍程式码,也不愿意接收别人的程式码,进而修正错误,维护它们,甚至加强功能。   这其中的关键究竟在何处呢?若是一语道破,其实也很简单,程式码是别人写的,只有原作者才真的了解程式码的用途及涵义。许多程式人心里都有一种不自觉的恐惧感,深怕被迫去碰触其他人所写的程式码。这是来自于人类内心深处对于陌生事物的原始恐惧。  
读懂别人写的程式码,让你收获满满  
   
不过,基于许多现实的原因,程式人时常受迫要去接收别人的程式码。例如,同事离职了,必须接手他遗留下来的工作,也有可能你是刚进部门的菜鸟,而同事经验值够了,升级了,风水轮流转,一代 菜鸟换菜鸟。甚至,你的公司所承接的专案,必须接手或是整合客户前一个厂商所遗留下来的系统,你们手上只有那套系统的原始码(运气好时,还有数量不等的文 件)   诸如此类的故事,其实时常在程式人身边或身上持续上演着。许多程式人都将接手他人的程式码,当做一件悲惨的事情。每个人都不想接手别人所撰写的程式码,因为不想花时间去探索,宁可将生产力花在产生新的程式码,而不是耗费在了解这些程式码上。   很遗憾的是,上述的情况对程式人来 说很难避免。我们总是必须碰触到其他人所写成的程式码,甚至必须了解它,加以修改。对于这项需求,在现今开放原始码的风气如此盛行的今日,正如之前的 式设计 2.0 ” 文中所提到的,你可以透过开放原始码学习到新的技术,学习到高手的架构设计,大幅提高学习的效率及效果。你甚至可以直接自开放原始码专案中抽取,提炼出 自己所需的程式码,站在巨人的肩膀上,直接由彼端获得所需的生产力。从这个观点来看,读懂别人所写的程式码,就不再只是从负面观点的 被迫接收 ,而是极具正面价值的 汲取养份。 ” 
先了解系统架构与行为模式,再细读  
   
倘若撰写程式码是程式人的重要技艺之一,那么读懂别人的程式码,接着加以修改,也势必是另一个重要的技艺。   如果你不能熟悉这项工作,不仅在遭逢你所不愿面对的局面时,无法解决眼前接手他人程式码的难题,更重要的是,当你看着眼前现成的程式码,却不知如何从中撷取自己所需,导致最后只能入宝山空手回,望之兴叹。接触他人的程式码,大致上可以分为 三种程度:一,了解,二,修改,扩充,三,抽取,提炼。了解别人的程式码是最基础的工作,倘若不能了解自己要处理的程式码,就甭论修改或扩充,更不可能去 芜存菁,从中萃取出自己所需,回收再利用别人所撰写的程式码。虽说是 阅读 ,但程式码并不像文章或小说一样,透过这种做法,便能够获得一定程度的了解。阅读文章或小说时,几乎都是循序地阅读,你只消翻开第一页,一行行阅读下去即 可。但是,有许多程式人在试着阅读其他人的程式码时,却往往有不知如何读起的困难。   或许找到系统的第一页(也就是程式码执行的启始点)并不难,但是复杂度高的系统,有时十分庞大,有时千头万绪。从程式码的启始点开始读起,一来要 循序读完所有的程式码旷日费时,二来透过这种方式来了解系统,很难在脑中构建出系统的面貌,进而了解到系统真正的行为。所以,阅读程式码的重点,不在于读 完每一行程式码,而是在于有效率地透过探索及阅读,从而了解系统的架构及行为模式。以便在你需要了解任何片段的细节实作时,能够很快在脑上对映到具体的程 式码位置,直到那一刻,才是细读的时机。  
熟悉沟通语言与惯例用语  
   
不论如何,有些基本的准备,是阅读他人程式码时必须要有的。   首先,你最好得了解程式码写成的程式语言。想要读懂法文写成的小说,总不能连法文都不懂吧。有些情况则很特殊。我们虽然不懂该程式码撰写所用的语言,但是因为现代语言的高阶化,而且流行的程式语言多半都是血统相近,所以即使不那么熟悉,有时也可勉力为之。   除了认识所用语言之外,再来就是要先确认程式码所用的命名惯例(命名惯例)。了解命名惯例很重要,不同的程式人或开发团队,差异可能很大。   这命名惯例涵盖的范围通常包括了变数的名称,函式的名称,类别(如果是物件导向的话)的名称,原始码档案,甚至是专案建构目录的名称。倘若使用了像设计模式之类的方法,这些名称更有一些具体的表述方式。   命名惯例有点像是程式人在程式语言 之上,另行建构的一组沟通行话。程式人会透过共通约束,遵守的命名惯例,来表达一些较高阶的概念。例如,有名的匈牙利式命名法,便将变数名称以属性,型 别,说明合并在一起描述。对程式人来说,这种方式能够提供更丰富的资讯,以了解该变数的作用及性质。   程式码阅读来说,熟悉这个做法之 所以重要,是因为当你了解整个系统所采用的惯例时,你便能试着以他们所共同操用的语汇来进行理解。倘若,不能了解其所用的惯例,那么这些额外提供的资讯, 就无法为你所用。像以设计模式写成的程式码,同样处处充满着模式的名称,诸如:工厂,门面,代理等等。以这些名称指涉的类别,也直接透过名称,表达了它们 自身的作用。对于懂得这命名惯例的读者来说,不需要深入探索,也能很快捕捉到这些类别的意义。   当你拿到一套必须阅读的程式码时,最好先取得命名惯例的说明文件。然而,并不是每套程式码都附有此类的说明文件。另一个方式,就是自己到程式码中,大略浏览一遍,有经验的程式人可以轻易发掘出该系统所用的命名惯例。   常见的命名方式不脱那几类,这时候经验就很重要,倘若你知道的惯例越多,就越能轻易识别他人所用的惯例。如果运气很糟,程式码所用的惯例是前所未见的,那么你也得花点时间归纳,凭自己的力量找出这程式码命名上的规则。  
掌握程式码撰写者的心态与习惯  
   
大多数的程式码,基本上都依循一致 的命名惯例。不过运气更差的时候,一套系统中可能会充斥着多套命名惯例。这有可能是因为开发团队由多组人马所构成,每组人马都有不同的文化,而在专案开发 管理又没有管控得宜所造成。最糟的情况,程式码完全没有明显的惯例可言,这时候阅读的难度就更高了。   想要阅读程式码,得先试着体会程式码作者的 。想要这么做,就得多了解对方所使用的语言,以及惯常运用的语汇。在下一回中,我们将继续探讨阅读程式码的相关议题。  

阅读他人的程式码( 2 - 摸清架构,便可轻松掌握全貌

在本文中,我们的重点放在:要了解一个系统,最好是采取由上至下的方式。先试着捕捉系统架构性的观念,不要过早钻进细节,因为那通常对于你了解全貌,没有多大的帮助。阅读程式码不需要从第一行读起,我们的目的并不是在于读遍每一段程式码。基于许多原因,程式人需要阅读其他人所写成的程式码。而对程式设计 2.0 时代的程式人来说,最正面的价值在于,能读懂别人程式的人,才有能力从中萃取自己所需的程式,借以提高生产力。  
阅读程式码的目的,在于了解全貌而非细节  
   
想要读懂别人程式码的根本基础,便是了解对方所用的程式语言及命名惯例。有了这个基础之后,才算是具备了基本的阅读能力。正如我之前提到的─ ─想要读懂法文写成的小说,总不能连法文都不懂吧。阅读程式码和阅读文学作品,都需要了解撰写所用的语言及作者习用的语汇。但我们在阅读文学作品通常是采循序 的方式,也就是从第一页开始,一行一行地读下去,依循作者为你铺陈的步调,逐渐进到他为你准备好的世界里。阅读程式码却大大不同。我们很少从第一行开始读 起,因为除非它是很简单的单执行绪程式,否则很少这么做。因为要是这么做,就很难了解整个系统的全貌。是的,我们这边提到了一个重点,阅读程式码的目的在 于了解系统的全貌,而不是在于只是为了地毯式的读遍每一段程式码。   就拿物件导向程式语言所写成的系统 来说,整个系统被拆解,分析成为一个个独立的类别。阅读个别类别的程式码,或许可以明白每项类别物件个别的行为。但对于各类别物件之间如何交互影响,如何 协同工作,又很容易陷入盲人摸象的困境。这是因为各类别的程式码,只描述个别物件的行为,而片段的阅读就只能造就片面的认识。  
由上而下厘清架构后,便可轻易理解组成关系  
   
如果你想要跳脱困境,不想浪费大量 时间阅读程式码,却始终只能捕捉到对系统片段认识,就必须转换到另一种观点来看待系统。从个别的类别行为着手,是由下至上(自下而上)的方法 ; 在阅读程式 码时,却应该先采由上至下(自上而下)的方式。对程式码的阅读来说,由上至下意谓着,你得先了解整个系统架构。   系统的架构是整个系统的骨干,支 柱。它表现出系统最突出的特征。知道系统架构究竟属于那一种类型,通常大大有益于了解系统的个别组成之间的静态及动态关系。有些系统因为所用的技术或框架 的关系,决定了最上层的架构。例如,采用的 Java Servlet / JSP 的技术的应用系统,最外层的架构便是以 J2EE 的(或起码的 J2EE 中的 Web 容器)为根本。使用的 Java Servlet / JSP 的技术时,决定了某些组成之间的关系。例如, Web 容器依据 web.xml 中的内容载入所有的 Servlets ,听众,以及过滤器。每当语境发生事件(例如初始化)时,它便会通知监听类别。每当它收到来自客户端的请求时,便会依循设定的所有过滤器链,让每个过滤器 都有机会检查并处理此一请求,最后再将请求导至用来处理该请求的 Servlet 的。当我们明白某个系统采用这样的架构时,便可以很容易地知道各个组成之间的关系。即使我们还不知道究竟有多少 Servlets ,但我们会知道,每当收到一个请求时,总是会有个相对应的服务器来处理它。当想要关注某个请求如何处理时,我应该去找出这个请求对应的服务器。  
了解架构,必须要加上层次感  
   
同样的,以爪哇写成的网页应用程式中,也许会应用诸如 Struts 的之类的的 MVC 框架,以及像 Hibernate 的这样的资料存取框架。它们都可以视为最主要的架构下的较次级架构。而各个应用系统,甚至有可能在 Struts 的及休眠之下,建立自有的更次级的架构。   也就是说,当我们谈到 架构 这样 的观念时,必须要有层次感。而不论是那一层级的架构,都会定义出各自的角色,以及角色间的关系。对阅读者来说,相较于直接切入最细微的单一角色行为,不如 了解某个特定的架构中,究竟存在多少角色,以及这些角色之间的互动模式,比较能够帮助我们了解整个系统的运作方式。   这是一个很重要的关键,当你试着进到最细节处之前,应该先试着找出参与的角色,及他们之间的关系。例如,对事件驱动式的架构而言,有 3 个很重要的角色。一个是事件处理的分派器(事件调度) ,一个是事件产生者(事件发生器) ,另一个则是事件处理器(事件处理程序)。   事件产生器产生事件,并送至事件分派器,而事件分派器负责找出各事件相对应的事件处理器,并且转交该事件,并命令事件处理器加以处理。像的图形用户界面的 Windows 应用程式,便是采用事件驱动式的架构。当你知道此类的应用程式皆为事件驱动式的架构时,你便可以进一步得知,在这样的架构下会有 3 种主要的角色。虽然也许还不清楚整个系统中,究竟会需要处理多少事件的类型,但对你而言,已经建立了对系统全貌最概观的认识。   虽然你还不清楚所有的细节,但诸如确切会有那些事件类型之类的资讯,在此刻还不重要─ ─不要忘了,我们采取的是由上而下的方式,要先摸清楚主建筑结构,至于壁纸的花色怎么处理,那是到了尾声时才会做的事。  
探索架构的第一件事:找出系统如何初始化  
   
有经验的程式人,对于时常被运用的 架构都很熟悉。常常只需要瞧上几眼,就能明白一个系统所用的架构,自然就能够直接联想到其中会存在的角色,以及角色间的关系。然而,并不是每个系统所用的 架构,都是大众所熟悉,或是一眼能够望穿的。这时候,你需要探索。目标同样要放在界定其中的角色,以及角色间的静态,动态关系。不论某个系统所采用的架构是否为大 部分人所熟知的,在试着探索一个系统的长相时,我们应该找出来几个答案,了解在它所用的架构下,下列这件事是如何被完成的:一,系统如何初始化,二,与这 个系统相接的其他系统(或使用者)有那些,而相接的介面又是什么 ; 三,系统如何反应各种事件,四,系统如何处理各种异常及错误。   系统如何初始化是很重要的一件事, 因为初始化是为了接下来的所有事物而做的准备。从初始化的方式,内容,能知道系统做了什么准备,对于系统会有什么行为展现,也就能得窥一二了。之所以要了 解与系统相接的其他系统(或使用者) ,为的是要界定出系统的边界。其他的系统可能会提供输入给我们所探索的系统,也可能接收来自这系统的输出,了解这边界所在,才能确定系统的外观。而系统所反应的事件类型,以及如何 反应,基本上就代表着系统本身的主要行为模式。最后,我们必须了解系统处理异常及错误的方式,这同样也是系统的重要行为,但容易被忽略。之前,我们提到必 须先具备一个系统的语言基础,才能够进一步加以阅读,而在本文中,我们的重点放在:要了解一个系统,最好是采取由上至下的方式。先试着捕捉系统架构性的观 念,不要过早钻进细节,因为那通常对于你了解全貌,没有多大的帮助。

 

阅读他人的程式码( 3 - 优质工具在手,读懂程式非难事

系统的复杂度往往超过人脑的负荷。阅读程式码的时候,你会需要更多工具提供协助。使用好的整合式开发环境( IDE )的或文字编辑器,就能提供最基本的帮助。阅读程式码的动作,可以是很原始的,利用最简单的文字编辑器,逐一开启原始码,然后凭借着一己的组织能力,在不同的程式码间跳跃,拼凑出脑中想要构建的图像。   不过,系统的复杂度往往超过人脑的负荷。阅读程式码的时候,你会需要更多工具提供协助。使用好的整合式开发环境( IDE )的或文字编辑器,就能提供最基本的帮助。  
善用文字编辑器或 IDE 中,加速解读程式码  
   
许多文字编辑器提供了常见程式语言 的语法及关键字标示功能。这对于阅读来说,绝对能够起很大的作用。有些文字编辑器(例如我常用的编辑器及偶而使用的记事本 + + ,甚至能够自动列出某个原始档中所有定义的函式清单,更允许你直接从清单中选择函式,直接跳跃到该函式的定义位置。这对于阅读程式码的人来说,就提供了极 佳的便利性。   因为在阅读程式码时,最常做的事,就是随着程式中的某个控制流,将阅读的重心,从某个函式移至它所呼叫的另一个函式。所以对程式人来说,阅读程式码时最常做的事之一就是:找出某个函式位在那一个原始档里,接着找到该函式所在的位置。   好的的 IDE 能够提供的协助就更多了。有些能够自动呈现一些额外的资讯,最有用的莫过于函式的原型宣告了。例如,有些的 IDE 支援当游标停留在某函式名称上一段时间后,它会以提示的方式显示该函式的原型宣告。   对阅读程式码的人来说,在看到程式码中呼叫到某个函式时,可以直接利用这样的支援,立即取得和这个函式有关的原型资讯,马上就能知道呼叫该函式所传入的各个引数的意义,而不必等到将该函式的定义位置找出后,才能明白这件事。  
grep
按(读者:推荐来源透视)是一个基本而极为有用的工具  
   
除了选用好的文字编辑器或的 IDE 之外,还有一个基本,但却极为有用的工具,它就是 grep 按。熟悉的 Unix 作业系统的程式人,对 grep 按这个公用程式多半都不陌生。 grep 按最大的用途,在于它允许我们搜寻某个目录(包括递回进入所有子目录)中所有指定档案,是否有符合指定条件(常数字串或正规表示式)档案。   倘若有的话,则能帮你指出所在的位置。这在阅读程式码时的作用极大。当我们随着阅读的脚步,遇上了任何一个不认识,但自认为重要的类别,函式,资料结构定义或变数,我们就得找出它究竟位在这茫茫程式码海中的何处,才能将这个图块从未知变为已知。 grep 按之所以好用,就是在于当 我们发现某个未知的事物时,可以轻易地利用它找出这个未知的事物究竟位在何方。此外,虽说 grep 按是 Unix 系统的标准公用程式之一,但是像视窗这样子 的平台,也有各种类型的 grep 按程式。对于在视窗环境工作的程式人来说,可以自行选用觉得称手的工具。  
gtags
可建立索引,让搜寻更有效率  
    grep
按虽然好用,但是仍然有一些不足之处。第一个缺点在于它并不会为所搜寻的原始码档案索引。每当你搜寻时,它都会逐一地找出所有的档案,并且读取其中的所有内容,过滤出满足指定条件的档案。当专案的原始码数量太大时,就会产生搜寻效率不高的问题。   第二个缺点是它只是一个单纯的文字档搜寻工具,本身并不会剖析原始码所对应的语言语法。当我们只想针对 函式 名称进行搜寻时,它有可能将注解中含有该名称的原始码,也一并找了出来。   针对 grep 按的缺点,打算阅读他人程式码的程式人,可以考虑使用像是 gtags 这样子的工具。 gtags 是源代码的 GNU 全局标记系统,它不只搜寻文字层次,而且因为具备了各种语言的语法剖析器,所以在搜寻时,可以只针对和语言有关的元素,例如类别名称,函式名称等。   而且,它能针对原始码的内容进行索引,这意谓一旦建好索引之后,每次搜寻的动作,都毋需重新读取所有原始码的内容并逐一搜寻。只需要以现成的索引结构为基础,即可有效率的寻找关键段落。  gtags 提供了基于命令列的程 式,让你指定原始码所在的目录执行建立索引的动作。它同时也提供程式让你得如同操作 grep 按一般,针对索引结构进行搜寻及检索。它提供了许多有用的检索 方式,例如找出专案中定义某个资料结构的档案及定义所在的行号,或者是找出专案中所有引用某资料结构的档案,以及引用处的行号。这么一来,你就可以轻易地针对阅读程式码时的需求予以检索。相较于 grep 按所能提供的支援, gtags 这样的工具,简直是强大许多。  
再搭配 htags 制作的 HTML 文件,更是如虎添翼  
   
还有一个绝对需要一提的工具。这个 叫做 htags 的工具,能够帮你将已制作完成的索引结构,制作成为一组相互参考的的 HTML 文件。基本上,利用这样的的 HTML 文件阅读程式码,比起单纯 地直接阅读原始码,来得更有结构。原因是阅读程式码时,这样的的 HTML 文件,已经为你建立起在各个原始码档案片段间跳跃的链结。例如,图一是针对一个有名的开放原始码专案 ffmpeg ,由 gtags 所产生出来的的 HTML 文件首页的一部分。  
1  
htags
工具首先为你找出所有定义的 Main )函式的档案,并且列出所在的函式。找出的 Main )函式,时常是阅读程式码的第一步,因为主要( )函式是程式的主要入口点,所有的动作皆由此启动,它是一切事物的源头。   凭借 htags 制作的的 HTML 文件,你可以轻易地点击超连结,直接进到的 Main )函式所在的程式码片段,如图二。  
2   3
当我们检视上述原始码时,发现 av_register_all )是个陌生,无法了解的事物,而想要搞懂它究竟是什么,可以再继续点击这个函式,如图三。这真是太方便了!阅读至此,你会猛然发现, gtags 仿佛就是为了阅读程式码而专门量身打造的利器。  

阅读他人的程式码( 4 - 望文生义,进而推敲组件的作用

先建立系统的架构性认识,然后透过名称及命名惯例,就可以推测出各组件的作用。例如:当 AOL Winamp 尝试着初始化一个插件时,它会呼叫这个结构中的 初始化函式,以便让每个插件程式有机会初始化自己。当 AOL Winamp 打算结束自己或结束某个插件的执行时,便会呼叫退出函式。在阅读程式码的细节之前,我们应先试着捕捉系统的运作情境。在采取由上至下的方式时,系统性的架构是最顶端的层次,而系统的运作情境,则是在它之下的另一个层次。
好的说明文件难求,拼凑故事的能力很重要  
   
有些系统提供良善的说明文件,也许还利用 UML 的充分描述系统的运作情境。那么对于阅读者来说,从系统的分析及设计文件着手,便是快速了解系统运作情境的一个途径。但是,并不是每个软体专案都伴随着良好的系统文件,而许多极具价值的开放原始码专案,也时常不具备此类的文件。对此,阅读者必须尝试自行捕捉,并适度地记录捕捉到的运作情境。   我喜欢将系统的运作情境,比拟成系 统会上演的故事情节。在阅读细节性质的程式码前,先知道系统究竟会发生那些故事,是必备的基本功课。你可以利用熟悉或者自己发明的表示工具,描述你所找到 的情境。甚至可以只利用简单的列表,直接将它们列出。只要能够达到记录的目的,对程式码阅读来说,都能够提供帮助。或者,你也可以利用基于 UML 中的类别 图,合作图,循序图之类的表示方法,做出更详细的描述。当你能够列出系统可能会有的情境,表示你对系统所具备的功能,以及在各种情况下的反应,都具备概括性的认识。以此为基础,便可在任何需要的时候,钻进细节处深入了解。  
探索架构的第一步─ ─找到程式的入口  
   
在之前,我们在一个开发专案中,曾经需要将系统所得到的的 MP3 音讯档,放至 iPod 的这个极受欢迎的播放设备中。   虽然 iPod 的本身也可以做为可移动式的储存设备,但并不是单纯地将 MP3 播放档案放到中的 iPod ,就可以让苹果的播放器认得这个档案,甚至能够加以播放。   这是因为苹果利用一个特殊的档案结构( iTunes 的数据库) ,记录播放器中可供播放的乐曲,播放清单以及乐曲资讯(例如专辑名称,乐曲长度,演唱者等) 。为了了解并且试着重复使用既有的程式码,我们找到了一个 AOL Winamp iPod 的外挂程式(插件)  AOL Winamp 是个人电脑上极受欢迎的播放软体,而我们找到的外挂程式,能让的软件直接显示连接至电脑的的 iPod 中的歌曲资讯,并且允许的软件直接播放。   我们追踪与阅读这个外挂程式的思路 及步骤如下,首先,我们要先了解外挂程式的系统架构。很明显的,大概浏览过原始码后,我们注意到它依循着 AOL Winamp 为插件程式所制定的规范,也 就是说,它是实作成的 Windows 上的 DLL 的,并且透过一个叫做 winampGetMediaLibraryPlugin DLL 的函式,提供一个名 winampMediaLibraryPlugin 的结构。   当我们不清楚系统的架构究竟为何时,我们会试着探索,而第一步,便是找到程式的入口。如何找到呢?这会依程式的性质不同而有所差别。   对一个本身就是可独立执行的程式来说,我们会找启动程式的主要函式,例如对的 C / C + + 来说就是主要( ,而对爪哇来说,便是静无效的 main 。在找到入口后,再逐一追踪,摸索出系统的架构。   但有时,我们所欲阅读的程式码是类别库或函式库,它只是用来提供多个类别或函式供用户端程式(客户程序)使用,本身并不具单一入口,此类的程式码具有多重的入口─ ─每个允许用户端程式呼叫的函式或类别,都是它可能的入口。   例如,对 AOL Winamp iPod 的插件来说,它是一个动态链接库形式的函式库,所以当我们想了解它的架构时,必须要先找出它对外提供的函式,而对的 Windows DLL 来说, 对外提供的函式,皆会以 dllexport 这个关键字来修饰。所以,不论是利用 grep 按或 gtags 之类的工具,我们可以很快从原始码中,找到它只有一 DLL 的函式(这对我们而言,真是一个好消息) ,而这个函式便是上述的 winampGetMediaLibraryPlugin  
系统多会采用相同的架构处理插件程式  
   
如果经验不够的话,也许无法直接猜出这个函式的作用。   不过,如果你是个有经验的程式人,多半能从函式所回传的结构,猜出这个函式实际的用途。而事实上,当你已经知道它是一个插件程式时,就应该要明白,它可能采用的,就是许多系统都采用的相同架构处理插件程式。   当一个系统采用所谓插件形式的架构时,它通常不会知道它的插件究竟会怎么实作,实作什么功能。它只会规范插件程式需要满足某个特定介面。当系统初始化时,所有的插件都可以依循相同的方式,向系统注册,合法宣示自己的存在。   虽然系统并不确切知道插件会有什么行为展现,但是因为它制定了一个标准的介面,所以系统仍然可以预期每个插件能够处理的动作类型。这些动作具体上怎么执行,对系统来说并不重要。这也正是物件导向程式设计中的 多型 观念。  
随着实务 经验,归纳常见的架构模式  
   
我想表达的重点,是当你 涉世越深 之后,所接触的架构越多,就越能触类旁通。只需要瞧上几眼,就能明白系统所用的架构,自然就能够直接联想到其中可能存在的角色,以及角色间的关系。像上述的插件程式手法,时常可以在许多允许 外挂 程式码的系统中看到。所以,有经验的阅读者,多半能够立即反应,知道像这样的系统的软件,应该是让每个插件程式,都写成 DLL 的函式库。   而每个插件的 DLL 的函式库中,都 必须提供 winampGetMediaLibraryPlugin )这个函式(如果你熟悉的 Windows 的程式设计,你会知道这是利用加载( )和 GetProcAddress )来达成的一种多型手法) 。如果你熟悉设计模式,你更会知道这是简单工厂方法这个设计模式的运用。  winampGetMediaLibraryPlugin )所回传的 winampMediaLibraryPlugin 结构,正好就描述了每个 AOL Winamp 插件的实作内容。  
善用名称可加速了解  
   
利用 gtags 这个工具,我们立即发现,这个插件它所定义的初始化,退出, PluginMessageProc 这三个名称,都是函式名称。这暗示在多型的作用下,它们都是在某些时间点,会由 AOL Winamp 核心本体呼叫的函式。   名称及命名惯例是很重要的。看到 初始化 ,我们会知道它的作用多半是进行初始化的动作,而 退出 大概就是结束时处理函式,而 PluginMessageProc 多半就是各种讯息的处理常式(过 程通常是程序的简写,所以 PluginMessageProc 意指插件讯息程序)了。  “ 望文生义 很重要,我们看到函式 的名称,就可以猜想到它所代表的作用,例如:当 AOL Winamp 尝试着初始化一个插件时,它会呼叫这个结构中的初始化函式,以便让每个插件程式有机会 初始化自己 ; AOL Winamp 打算结束自己或结束某个插件的执行时,便会呼叫退出函式。当 AOL Winamp 要和插件程式沟通时,它会发送各种不 同的讯息至插件,而插件程式必须对此做出回应。   我们甚至不需要检视这几个函式的内容,就可以做出推测,而这样的假设,事实上也是正确的。

阅读他人的程式码( 5 - 找到程式入口,再由上而下抽丝剥茧

根据需要决定展开的层数,或展开特定节点,并记录树状结构,然后适度忽略不需要了解的细节─这是一个很重要的态度。因为你不会一次就需要所有的细节,阅读都是有目的的,每次的阅读也许都在探索程式中不同的区域。

探索系统架构的第一步,就是找到程式的入口点。找到入口点后,多半采取由上而下(自上而下)的方式,由最外层的结构,一层一层逐渐探索越来越多的细节。   我们的开发团队曾针对 AOL Winamp iPod 的插件进行阅读及探索,不仅找到入口点,也找出,并理解它最根本的基础架构。从这个入口点,可以往下再展开一层,分别找到三个重要的组成及其意义:  
  
init 初始化动作       ● 退出( 终止化动作     ● PluginMessageProc 以讯息的方式处理程式所必须处理的各种事件
展开的同时,随手记录树状结构  
   
当我们从一个入口点找到三个分支后,可以顺着每个分支再展开一层,所以分别继续阅读的 init ,退出,以及 PluginMessageProc 的内容,并试着再展开一层。阅读的同时,你可以在文件中试着记录展开的树状结构。  
 
init 初始化动作       ● itunesdb_init_cc 建立存取 iTunes 的数据库的同步物件  
     
初始化资料结构        初始化的 GUI 元素        载入设定        建立日志档       ● autoDetectIpod 侦测的 iPod 插入的执行绪  
     ●
退出( 终止化动作       ● itunesdb_del_cc 终止存取 iTunes 的数据库的同步物件        关闭日志档  
     
终止化图形用户界面元素       ● PluginMessageProc 以讯息的方式处理程式所必须面临的各种事件
     
执行所连接之苹果的 MessageProc   这部分必须要留意几个重点。首先,应该一边阅读,一边记录文件。因为人的记忆力通常有限,对于陌生的事物更是容易遗忘,因此边阅读边记录,是很好的辅助。   再者,因为我们采取由上而下的方式,从一个点再分支出去成为多个点,因此,通常也会以树状的方式记录。除此之外,每次只试着往下探索一层。从的 init )来看你便会明白。   以下试着摘要的 init )的内容:  
诠释的 init  
itunesdb_init_cc
; currentiPod = 苹果 = C_ItemList ; ...  
conf_file =
(字符 * SendMessage 
plugin.hwndWinampParent WM_WA_IPC 0 IPC_GETINIFILE
m_treeview = GetDlgItem
plugin.hwnd LibraryParent 0x3fd
/ /
这个数字实际上是魔术:  ...  
g_detectAll = GetPrivateProfileInt
“ ml_ipod ” “ detectAll ” 0 conf_file = 0 ; ...  
g_log = GetPrivateProfileInt
“ ml_ipod ” 日志 0 conf_file = 0 ; ...  
g_logfile =
打开( g_logfilepath ,有 “ A ” ; ...  
autoDetectIpod
返回 0 ;   
因为我们只试着多探索一层,而目的 是希望发掘出下一层的子动作。所以在的 init )中看到像 “ itunesdb_init_cc ; ” 这样的函式呼叫动作时,我们知道它是在初始化( )之下的一个独立子动作,所以可以直接将它列入。但是当看到如下的程式行:  currentiPod = 苹果 = C_ItemList ;  我们并不会将它视为的 init )下的一个独立的子动作。因为好几行程式,才构成一个具有独立抽象意义的子动作。例如以上这两行构成了一个独立的抽象意义,也就是初始化所需的资料结构。   理论上,原来的程式撰写者,有可能 撰写一个叫做 init_data_structure )的函式,包含这两行程式码。这样做可读性更高,然而基于种种理由,原作者并没有这么做。身为阅读者,必须自行解读,将这几行合并成单一个子动作,并赋予 它一个独立的意义─ ─初始化资料结构。  
无法望文生义的函式,先试着预看一层  
   
对于某些不明作用的函式叫用,不是 望其文便能生其义的。当我们看到 “ itunesdb_init_cc 这个名称时,我们或许能从 “ itunesdb_init ” 的字眼意识到这个函式和苹果所采用的的 iTunes 数据库的初始化有关,但 循环 却实在令人费解。为了理解这一层某个子动作的真实意义,有时免不了要 往前多看一层。   原来它是用来初始化同步化机制用的物件。作用在于这程式一定是用了某个内部的资料结构来储存的 iTunes 数据库,而这资料结构有可能被多执行绪存取,所以必须以同步物件(此处是视窗的临界区)加以保护。   所以说,当我们试着以树状的方式, 逐一展开每个动作的子动作时,有时必须多看一层,才能真正了解子动作的意义。因为有了这样的动作,我们可以在展开树状结构中,为 itunesdb_init_cc )附上补充说明:建立存取 iTunes 的数据库的同步物件。这么一来,当我们在检视自己所写下的树状结构时,就能轻易一目了然的理解每个子动作的真正作用。  
根据需要了解的粒度,决定展开的层数  
   
我们究竟需要展开多少层呢?这个问题和阅读程式码时所需的 粒度(粒度) 有关。如果我们只是需要概括性的了解,那么也许展开两层或三层,就能够对程式有基础的认识。倘若需要更深入的了解,就会需要展开更多的层次才行。   有时候,你并不是一视同仁地针对每 个动作,都展开到相同深度的层次。也许,你会基于特殊的需求,专门针对特定的动作展开至深层。例如,我们阅读 AOL Winamp iPod 插件的程式目 录,其实是想从中了解究竟应该如何存取的 iPod 上的 iTunes 的数据库,使我们能够将 MP3 播放歌曲或播放清单加至此数据库中,并于的 iPod 中播 放。   当我们层层探索与分解之后,找到了 parseIpodDb ,从函式名称判断它是我们想要的。因为它代表的正是剖析 iPod 的数据库,正是我们此次阅读的重点,也就达成阅读这程式码的目的。   我们强调一种不同的做法:在阅读程式码时,多半采取由上而下的方式,而本文建议了一种记录阅读的方式,就是试着记录探索追踪时层层展开的树状结构。你可以视自己需要,了解的深入程度,再决定要展开的层数。你更可以依据特殊的需要,只展开某个特定的节点,以探索特定的细目。   适度地忽略不需要了解的细节,是一个很重要的态度,因为你不会一次就需要所有的细节,阅读都是有目的的。每次的阅读也许都在探索程式中不同的区域 ; 而每次探索时,你都可以增补树状结构中的某个子结构。渐渐地,你就会对这个程式更加的了解。

阅读他人的程式码( 6 - 阅读的乐趣:透过程式码认识作者

即便每个人的写作模式多半受到他人的影响,程式人通常还是会融合多种风格,而成为自己独有的特色,如果你知道作者程式设计的偏好,阅读他的程式码就更得心应手。阅读程式码时,多半会采取由上而下,抽丝剥茧的方式。透过记录层层展开的树状结构,程式人可以逐步地建立起对系统的架构观,而且可以依照需要的粒度(粒度) ,决定展开的层次及精致程度。   建立架构观点的认识是最重要的事 情。虽然这一系列的文章前提为 阅读他人的程式码 ,但我们真正想做的工作,并不在于彻底地详读每一行程式码的细节,而是想要透过重点式的程式码 摘读 ,达到对系统所需程度的了解。每个人在阅读程式码的动机不尽相同,需要了解的程度也就有深浅的分别。只有极为少数的情况下,你才会需要细读每一行程式码。  
阅读程式码是新时代程式人必备的重要技能  
   
这一系列的文章至此已近尾声,回顾 曾探讨的主题,我们首先研究了阅读程式码的动机。尤其在开放原始码的风气如此之盛的情况下,妥善利用开放原始码所提供的资源,不仅能够更快学习到新的技 术,同时在原始码版权合适时,还可以直接利用现成的程式码,大幅地提高开发阶段的生产力。所以,阅读程式码俨然成为了新时代程式人必备的重要技能之一。   接着,我们提到了阅读程式码前的必要准备,包括了对程式语言,命名惯例的了解等等。在此之后,我们反覆提起了 由上而下 的阅读方向的重要性。   由上而下的阅读方式,是因为我们重 视架构更胜于细节。从最外层的架构逐一向内探索,每往内探索一层,我们了解系统的粒度就增加了一个等级。当你识别出系统所用的架构时,便能够轻易了解在这 个架构下会有的角色,以及它们之间的动态及静态的关系。如此一来,许多资讯便不言可喻,毋需额外花费力气,便能够快速理解。  
好的名称能够摘要性地点出实体的作用  
   
追踪原始码时,固然可以用本来的方式,利用编辑器开启所需的档案,然后利用编辑器提供的机制阅读,但是倘若能够善用工具,阅读程式码的效率及品质都能大大提升。在本系列文章中,我们介绍了一些工具,或许你还可以在坊间找到其他更有用的工具。   我在这一系列的文章中,实际带着大 家阅读,追踪了一个名为 ml_pod 的开放原始码专案。它是一个 AOL Winamp iPod 的外挂程式。在追踪的过程中,我们试着印证这一系列文中所 提到的观念及方法。我们采用逐渐开展的树状结构来记录追踪的过程,并借以建立起对系统的概观认识。   就原始码的阅读来说,之前的讨论涉 及了工具面及技巧面。但还有一些主题不在这两个范畴之内,例如,善用名称赋予你的提示。名称做为隐喻(隐喻)的作用很大,好的名称能够摘要性地点出实体的 作用,例如我们看到 autoDetectIpod ,自然而然能够想像它的作用在于自动(自动)侦测(检测)的 iPod 的存在。   我们在展开树状结构时,有时候需要 预看一层,有时却不需要这么做,便可得到印证。程式人都会有惯用的名称以及组合名称的方法,倘若能够从名称上理解,便毋需钻进细节,可以省去相当多的时 间。例如,当我们看到 parseIpodDb )时,便可以轻易了解它是剖析(解析)的 iPod 的资料库( DB )的,因此便不需要立即钻进 parseIpodDb )中查看底细。   尽管如此,能否理解程式人命名的用意,和自身的经验以及是否了解原作者的文化背景,是息息相关的。   命名本身就是一种文化产物。不同的程式人文化,就会衍生出不同的命名文化。当你自己的经验丰富,看过及接触过的程式码也多时,对于名称的感受及联想的能力自然会有不同。   这种感受和联想的能力,究竟应该如何精进,很难具体描述。就我个人的经验,多观察不同命名体系的差异,并且尝试归纳彼此之间的异同,有助于更快地提升对名称的感受及联想力。  
转换立场,理解作者的思考方式  
   
除了工具及技巧之外, 想要阅读程式码,得先试着阅读写这个程式码的程式人的心。 这句话说来十分抽象,或许也令人难以理解。   当你在阅读一段程式码时,或许可以试着转换自己的立场,从旁观者的角度转换成为写作者的心态,揣摩原作者的心理及处境。当你试着设身处地站在他的立场,透过他的思考方式来阅读,追踪他所写下的程式码,将会感觉更加流畅。   许多软体专案,都不是由单一程式人所独力完成。因此,在这样的专案中,便有可能呈现多种不同的风格。   许多专案会由架构师决定主体的架构及运作,有既定实施的命名惯例,及程式设计需要遵守方针。在多人开发的模式下,越是好的软体专案,越看不出某程式码片段究竟是由谁所写下的。   不过,有些开放原始码的专案,往往又整合了其他开放原始码的专案。有的时候,也很难求风格的统一,便会出现混杂的情况。好比之前提到的 ml_pod 专案,因为程式码中混合了不同的来源,而呈现风格不一致的情况。   我在阅读非自己所写的程式码时,会 观察原作者写作的习惯,借以对应到脑中所记忆的多种写作模型。在阅读的过程中,读完几行程式码,我会试着猜想原作者在写下这段程式码时的心境。他写下这段 程式码的用意是什么?为什么他会采取这样的写法?顺着原作者的思考理路阅读,自己的思考才能更贴近对方写作当时的想法。   当你短暂化身为原作者时,才能更轻易的理解他所写下的程式码。  
如果你能知道原作者的背景,程式设计时的偏好,阅读他的程式码,就更能得心应手了。  
从程式码着手认识作者独有的风格,进而见贤思齐  
   
我在阅读别人写下的 程式码时,我会试着猜想,原作者究竟是属于那一种 流派 呢?每个人都有自己独特的写作模式,即便每个人的写作模式多半受到他人的影响─ ─不论是书籍的作者,学习过程中的指导者,或一同参与专案的同侪,但每个程式人通常会融合多种风格,而成为自己独有的风格。   物件导向的基本教义派,总是会以他心中觉得最优雅的物件导向方式来撰写程式。而阅读惯用,善用设计模式的程式人所写下的程式码时,不难推想出他会在各种常见的应用情境下,套用哪些模式。   有些时候,在阅读之初,你并不知道原作者的习性跟喜好,甚至你也不知道他的功力。但是,在阅读之后,你会慢慢地从一个程式人所写下的程式码,开始认识他。你或许会在阅读他人的程式码时,发现令人拍案叫绝的技巧或设计。你也有可能在阅读的同时,发现原作者所留下的缺失或写作时的缺点,而暗自警惕于心。这也算是阅读他人程式码时的一项乐趣。   当你从视阅读他人的程式码为畏途,转变成为可以从中获取乐趣的时候,我想,你又进到了另一个境界。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值