转-如何读懂源码(下)



阅读他人的程式码( 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专案,因为程式码中混合了不同的来源,而呈现风格不一致的情况。 

我在阅读非自己所写的程式码时,会观察原作者写作的习惯,借以对应到脑中所记忆的多种写作模型。在阅读的过程中,读完几行程式码,我会试着猜想原作者在写下这段程式码时的心境。他写下这段程式码的用意是什么?为什么他会采取这样的写法?顺着原作者的思考理路阅读,自己的思考才能更贴近对方写作当时的想法。 

当你短暂化身为原作者时,才能更轻易的理解他所写下的程式码。 
如果你能知道原作者的背景,程式设计时的偏好,阅读他的程式码,就更能得心应手了。 

从程式码着手认识作者独有的风格,进而见贤思齐 
我在阅读别人写下的程式码时,我会试着猜想,原作者究竟是属于那一种“流派”呢?每个人都有自己独特的写作模式,即便每个人的写作模式多半受到他人的影响─ ─不论是书籍的作者,学习过程中的指导者,或一同参与专案的同侪,但每个程式人通常会融合多种风格,而成为自己独有的风格。 

物件导向的基本教义派,总是会以他心中觉得最优雅的物件导向方式来撰写程式。而阅读惯用,善用设计模式的程式人所写下的程式码时,不难推想出他会在各种常见的应用情境下,套用哪些模式。 

有些时候,在阅读之初,你并不知道原作者的习性跟喜好,甚至你也不知道他的功力。但是,在阅读之后,你会慢慢地从一个程式人所写下的程式码,开始认识他。 

你或许会在阅读他人的程式码时,发现令人拍案叫绝的技巧或设计。你也有可能在阅读的同时,发现原作者所留下的缺失或写作时的缺点,而暗自警惕于心。这也算是阅读他人程式码时的一项乐趣。 

当你从视阅读他人的程式码为畏途,转变成为可以从中获取乐趣的时候,我想,你又进到了另一个境界。

你可能感兴趣的:(转-如何读懂源码(下))