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