如何理解和阅读理解开源项目
5. 理解开源项目
在这一章中,只打算讨论以命令式编程范型为主的语言,因为其他的编程范型的开源项目,笔者接触太少了(期待各类达人多多补充)。
5.1. 静态理解
阅读一个开源项目的源代码,通常都很容易。大多数开源项目的托管网站,都提供了无需下载,直接阅读源代码的功能,比较有趣的是,大家可以比较一下 sourceforge、google code以及github的查看源代码的功能。这分别代表了老、中、青三代开源托管平台,对于查看代码的重视程度。
5.1.1. 目录结构
好的开源项目,通常会选择合理的目录结构,来组织自己的代码。而所谓合理,通常意味着遵循最常见的约定俗成。比如:
目录名 | 含义 |
conf/configure | 各种配置文件 |
src/source | 项目的源代码 |
doc/document | 项目文档 |
test/unittest | 单元测试 |
tools | 相关工具 |
lib | 库文件 |
app | 应用相关的文件(在web项目中经常出现) |
controllers | 控制器,在遵循MVC模式的Web项目中,经常出现 |
models | 模型,在遵循MVC模式的Web项目中,经常出现 |
views | 视图,在遵循MVC模式的Web项目中,经常出现 |
db | 数据库相关文件 |
demo/example | 相关示例代码 |
misc | 其他杂项 |
include | 头文件所在目录,c/c++项目中常见 |
out/build | 编译结果输出目录 |
5.1.2. 包名与文件名
在软件体系中,包(Package)是一个很重要的概念,与模块(Module)类似,但是又有所区别。一个项目,在初始设计时,就需要做模块划 分,每一个大小合适的模块,往往就可以作为一个开发工作单元,分配给某个开发者完成。当然,对于那种大型的、复杂的项目,还需对模块做进一步的细分,比 如:子模块(Sub Module)。而包(Package),则往往具有一定的可重用性。我们可以认为,一个模块,开源出去未必会有人来用。而一个设计良好的包,本身就可以 作为一个开源项目,放出去给被人使用。
因此,从更加有利于软件开发的协作的角度来说,合理的包命名,就变得非常重要。
越是现代的开源项目,越是懂得不必一切从零开始搭建,所以,我们常常会发现,一个开源项目,他们自己会开发一组Package,同时也依赖一批别人开发的Package。在静态理解项目是,了解一个项目项目有哪些包,以及依赖哪些包,就非常重要。
举例之一:rails是一个著名的ruby开源项目。我们访问它的github主页 ,就可以看到这个项目的源代码结构。
- 首先需要选择查看某一个稳定版本,比如3.2版
https://github.com/rails/rails/tree/3-2-stable - 然后阅读install.rb文件
https://github.com/rails/rails/blob/3-2-stable/install.rb - 我们可以看到的内容,主要包含两大部分:
- 编译rails的依赖包:activesupport、activemodel、activerecord、activeresource、actionpack、actionmailer、railties。这些都是rails项目自己开发的包
- 然后再编译rails本身
- rails本身的包描述文件是:rails.gemspec
https://github.com/rails/rails/blob/3-2-stable/rails.gemspec - 通过阅读rails.gemspec,我们可以了解到,rails这个项目,依赖的包还包括:bundler、sprockets-rails。
- 事实上,通过阅读activesupport等一系列包的.gemspec文件,我们还会发现更多的外部依赖包。
不同的项目,描述包文件,以及包依赖关系,有各种不同的格式。需要一一分别学习。这里就不再详细解说了。
在一个开源项目中,代码当然是由一个一个的源代码文件组成的。通过查看文件名,往往可以了解一个文件的大概内容。例如:
- errors.rb,通常会与出错处理有关
- i18n.rb,通常会与国际化有关
- logger.rb,通常会与日志有关
- json目录下的两个文件decoding.rb和encoding.rb,自然是JSON格式的编解码相关代码
通常,要迅速的辨认出一个文件名的含义,与领域知识大有关系。例如:http.rb,通常会是处理http协议相关。而request和response,则通常是网络协议中的请求与响应相关的处理代码。对于这些单词的熟悉程度,决定了我们阅读与理解代码的迅捷程度。
5.1.3. 类名、函数名与变量名
java是一门很讲究规范的语言,所以他的每一个类,就会对应一个同名的.java文件(内部类除外)。这使得我们寻找类所在的源文件,变得非常简单。当然,这样会造成源文件数量的增加,也许会有人不喜欢。
不同的语言,对于命名有其自己的规范,我们可以做一个列表,来简单列出这些规范。
语言 | 包/命名空间命名 | 类命名 | 函数/方法命名 | 常量命名 | 变量命名 |
Java | domainname.package | ThisClassName | theMethodName | THE_VALUE | theValue |
全部都是小写的单词,以.区隔 | 每个单词都以大写字母开头 | 第一个单词以小写字母开头 | 全部大写,单词以下划线分隔 | 与函数名一致 | |
C# | DomainName.Package | ThisClassName | TheMethondName | THE_VALUE | TheValue |
每个单词都以大写字母开头,以.区隔 | 每个单词都以大写字母开头 | 与类名一致 | 全部大写,单词以下划线分隔 | 与类名一致 | |
PHP | domainname.package | ThisClassName | theMethodName | $THE_VALUE | $theValue |
全部都是小写的单词,以.区隔 | 每个单词都以大写字母开头 | 第一个单词以小写字母开头 | 全部大写,单词以下划线分隔 | 与函数名一致 | |
C/C++ | std::hex | CThisClassName | TheMethodName | nMAX_VALUE | nTheValue |
全部小写,以::区隔 | 以大写C开头,后续是大写字母开头的单词 | 每个单词以大写字母开头 | 特别会引入前缀的概念,例如:n代表整形、b代表布尔型、c代表字符型等等 | ||
Delphi | myunit.unit2 | TThisClassName | TheMethodName | castMaxValue | iTheValue |
全部小写,以.区隔 | 以大写T开头,后续是大写字母开头的单词 | 每个单词以大写字母开头 | 特别会引入前缀的概念,例如:i代表整形、b代表布尔型、c代表字符型等等,cast代表常量 | ||
Ruby | Module::SubModule | ThisClassName | the_method_name | MAX_VALUE | the_value |
每个单词以大写开头,以::区隔 | 每个单词都以大写字母开头 | 全小写单词,以下划线分隔,!?有特定的含义 | 全大写单词,以下划线分隔 | 全小写单词,以下划线分隔 | |
Python | mod_submod | ThisClassName | the_method_name/theMethodName | MAX_VALUE | the_value |
全部小写,以_区隔 | 每个单词都以大写字母开头 | 全小写单词,以下划线分隔,也可以类似Java的命名,私有函数以双下划线开头 | 全大写,以下划线分隔 | 全小写,以下划线分隔 | |
JavaScript | 无 | ThisClassName | thePrivateMethod/ThePublicMethod | MAX_VALUE | theValue |
无 | 每个单词都以大写字母开头 | 私有函数小写字母开头,公有函数以大写字母开头 | 全大写,下划线分隔 | 小写字母开头,私有变量,加下划线 |
这是一个非常粗略,挂一漏万的表格,详细的命名规范,请参考各种具体语言的命名规范文档。
5.1.4. 注释与Readme
很多时候,开源项目代码里的注释,会给你带来误导。或者会让人不知所云,或者只是写给自己看到TODO,或者代码改了,注释忘记改。种种原因,因此我强烈的不建议过于重视注释。
但是,在业内有一种流派,非常重视注释,而且信奉从源代码的注释,就可以直接生成项目的开发文档。比如JavaDoc这样的东西,在java的开 源项目里,简直用到泛滥,也造成了java的很多项目,注释数量比代码的数量还要多。而且,格式规范,千篇一律(为了生成Document),真正有意义 的注释内容,少之又少。纯粹是干扰阅读。
很多时候,我都建议阅读代码,就真的去读代码。然后试着从类名、方法名、变量名中,大致“猜出”代码的意义。然后再实际的将代码运行起来,看看执行过程中,这些代码是如何工作的。
总之,不到万不得已,不要先看注释。
虽然我个人对于注释,相当的不重视,但是却非常认同Readme的价值。令我倍感欣慰的是,Github的创立者,也完全赞成这一点。他们将一个 项目的首页,直接规定为“代码展示+Readme”,也就强迫所有在Github上安家的开源项目,将更多的精力,投注到Readme的撰写中去。这样形 成的良性循环,使得我们可以乐观的预期:越来越多的开源软件,将会越来越重视项目根目录下的Readme文件的价值,将一个项目最为重要的内容,以最为精 炼的方式,在Readme中,以结构良好的方式,展现出来。
因此,首先阅读Readme,对于了解一个开源项目,是一个非常好的选择。
5.1.5. UML图
UML是一种软件建模语言,全称为:统一建模语言(UML,Unified Modeling Language)。非常巧合的是,在打算写这一小节的今天,@蔡学庸发了两条微博:我非常期待,他能够就这个问题,谈到更多的心得。在我看来,在阅读源代码的时候,不断记录,在脑海里形成整个项目的全景图像,是非常有帮助的。
关于UML的定义,可以参考维基百科: UML 以下引用一段:
统一建模语言(UML,Unified Modeling Language)是非专利的第三代建模和规约语言。UML是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的 制品的开放方法。UML展现了一系列最佳工程实践,这些最佳实践在对大规模,复杂系统进行建模方面,特别是在软件架构层次已经被验证有效。
UML集成了Booch,OMT和面向对象软件工程的概念,将这些方法融合为单一的,通用的,并且可以广泛使用的建模语言。UML打算成为可以对并发和分布式系统的标准建模语言。
UML 并不是一个工业标准,但在Object Management Group的主持和资助下,UML正在逐渐成为工业标准。OMG 之前曾经呼吁业界向其提供有关对象导向的理论及实现的方法,以便制作一个严谨的软件建模语言(Software Modeling Language)。有很多业界的领袖亦真诚地回应OMG,帮助她建立一个业界标准。
在UML系统开发中有三个主要的模型:
- 功能模型: 从用户的角度展示系统的功能,包括用例图。
- 对象模型: 采用对象,属性,操作,关联等概念展示系统的结构和基础,包括类图。
- 动态模型: 展现系统的内部行为。包括序列图,活动图,状态图。
在学习开源软件时如何使用UML,有以下一些经验和忠告:
- 不用使用工具,自动化的生成UML。自己手绘或者用Umbrello这样的开源工具自己绘制,将大大提高阅读并理解代码的能力。
- 首先建立对象的静态模型,也就是先画“类图”。
- 在UML规范之外,可以加一些辅助自己记忆的符号,这个没有一定的规矩,方便好记就行。
- 其次在动态理解的过程中,对关键的执行路径,画出时序图(Sequence Diagram),将有助于深入理解项目的执行过程。
- 对于非面向对象的软件项目,可以参照类图与组件图的模式,画出模块图。也有助于加深理解。
- UML本身有越来越复杂,越来越学术化的倾向,要适可而止。
5.1.6. 外部文档
开源项目的文档水平,有如下几个层次:
- 没有文档(比这个更糟糕的,是有一些错漏的,长时间没有更新的垃圾文档)
- 有一个根据代码注释自动生成的XXDoc,通常这样的文档,价值很低。还不如直接去看代码。
- 有一个简单的综述性质的文档,至少告诉你一个项目的大概。
- 有完整的项目文档,这样的项目已经非常罕见了。
- 有各国志愿者帮助翻译的多语言文档。这样的项目,通常已经是世界一流的项目了。
- 有专门的文档、博客、甚至图书,《XX项目源码解读》之类。一般只有Linux、MySQL这样的项目,才有这样的待遇吧。
一般来说,外部文档的可信度并不高,而且往往过时。不到走投无路,我不建议找外部文档来帮助理解。当然,一些太复杂的项目,作为入门导引,看看也无妨。
5.2. 动态理解
所谓动态理解,就是让项目运行起来,在运行项目的过程中,理解一个项目是如何运行的。(这个有点像绕口令了。。。)
5.2.1. 输出日志
所谓日志,在软件领域,通常是指程序运行的一种记录。开发者与维护人员,可以通过分析日志,了解程序的运行状况。日志输出的数量多寡,可以分为以下几种:
- 完全没有输出(这不是一种好的做法)
- 有出错与崩溃时的输出日志(主要用于排除故障)
- 打开某个参数配置的开关,例如将日志级别修改为debug,将会输出更多的日志信息(主要用于调试程序)
- 为了理解特定的片段,直接修改代码,增加更多的日志输出,甚至将代码执行过程中的所有相关变量,全都输出出来。(用于查找疑难杂症,深入理解源代码等目的。)
一个比较完善的开源项目,通常会输出一些日志,如果你搜索整个项目的源代码,都找不到(log,logger,logging)这样的关键字,那就比较糟糕了。
通过修改源代码,以增加更多的日志输出,是我们常用的一种手段,最好是能够找到项目中可供参考的输出日志的办法,照着那个例子来改写。如果实在找 不到,或者调用存在一些陷阱,也可以自己纯粹手工的添加日志输出代码。简单的举一个ruby的例子,下面的这一行代码,就可以输出一段内容到日志文件里去 了。
File.open("temp.log","a") { |f| f.puts("log info") }
输出日志,能够解决大多数情况下的理解需求,唯一可能会有陷阱的,则是*多线程程序输出的日志*,因为每次输出的内容可能次序不一致,因此需要特别小心。
5.2.2. 设置断点与单步跟踪
IDE有一个重要的好处,就是可以帮助程序员调试程序。在一个图形化界面里,跟踪调试程序,有着纯文字界面难以比拟的便利性。
Netbeans IDE 7.0 调试PHP的程序片段
在一个宽屏的显示器里,同时显示源代码树、对象结构、当前执行到的代码行、当前的各种变量值、调用序列、各个线程、输出内容等等等等,还是很爽的。
当然,要使得IDE能够调试一个开源项目,还是有很多琐碎的事情需要处理。例如:- 如何在IDE中打开一个项目
- 如何在IDE中配置一个项目的依赖项
- 如何在IDE中编译并运行一个项目
- 如何在IDE中设置断点
这些如何,因语言、平台、IDE、版本、具体项目的不同,而有所区别。这里没法给出一个周到全面的解决方案,但是可以给一些搜索方面的建议:
- 假设要在Netbeans 7中打开一个开源的Java项目,可以搜索“how to open java project in netbeans 7”,然后我们可以找到一些文档:
当然,一定会有各种让人挠头的问题,各位多多尝试吧。另外推荐一本书,是张银奎写的《软件调试》,大部头。但是的确是一本好书!
http://book.douban.com/subject/3088353/
5.2.3. 抛出异常
begin1/0rescue => exceptionputs exception.backtraceend
java.lang.Exceptionat org.jruby.lexer.yacc.LexerSource.getSource(LexerSource.java:147)at org.jruby.parser.Parser.parse(Parser.java:122)at org.jruby.Ruby.parseFile(Ruby.java:1965)at org.jruby.Ruby.parseFile(Ruby.java:1969)at org.jruby.Ruby.parseFromMain(Ruby.java:364)at org.jruby.Ruby.runFromMain(Ruby.java:327)at org.jruby.Main.run(Main.java:214)at org.jruby.Main.run(Main.java:100)at org.jruby.Main.main(Main.java:84)
5.2.4. 修改代码,破坏性尝试
5.2.5. 工具
5.3. 主线与支线
5.3.1. 寻找入口
5.3.2. 跟踪关键流程
5.3.3. 寻找挂接点
5.4. 外围代码
5.4.1. 必须存在的外围功能
5.4.2. demo/example
5.4.3. 单元测试
5.5. 知其所以然
有一句俗语叫做:“知其然,更要知其所以然”。用在任何学习科目上,几乎都是恰当的。本章叫做《理解开源项目》,而之前的4个小节,可以说都是属于“知其然”的功夫。如何才能知其所以然呢?
所以然包括哪些内容?
往大了说,整个这份文档,希望帮助读者达到的,就是能够对于开源软件“知其所以然”。这样才算是真正提高了软件开发的能力。因此,我们可以将“架构决策”、“代码风格”、“领域知识”、“编程技巧”等等内容,都算作是所以然的一部分。
架构决策 通过深入阅读和分析源代码,理解整个项目,为何像这样,而不是那样做架构设计。其间蕴含着项目作者的经验和智慧,理解了这个,将是一种巨大的收获。
代码风格
每一种语言、每一个社区、每一个开发者群体,甚至每一个开源项目,都有其独特的代码风格,这种风格,有其背后的合理性,也有很多是来源于某种开发哲学的思
考。理解一种代码风格,就是理解一种思考的模式,一种思想的体系。能够多了解一些不同种类的代码风格,对于提高软件开发能力,将有很大的帮助。
领域知识
有些代码不容易看懂,很重要的一个原因,是这个项目所涉及的领域,我们没有什么深入的了解。多年的程序员经验告诉我,要做好某一个行业的软件,一定要成为
某一个行业的内行。甚至要比那个本行业的业内人士,更加精通。因此,一个优秀的程序员,通常是能够跟你聊多个不同行业的话题的。强大到你几乎无法分别他的
是不是业内人士。因此,通过理解开源项目,进而理解相关的领域知识,会有很多收获。
编程技巧 阅读优秀的开源项目的代码,有时候很像是看一本好书。细细品味,慢慢的体会。我们会发现一点一滴的“妙处”。这些妙处凝聚了程序员的巧思妙想,能够体会得越多,对我们的帮助也就越大。
所以然还包括的一些内容:
个人偏好
开源作者也是普通人,他们有很多观点和取舍,未必能够说服他人,只能算是他们自己的偏好。而他们将自己的偏好表达在代码里,有些时候,我们能够很容易理解
(因为我们也是这样想的)。有些时候,我们就会感觉很不解,而且,常常会发生的一类故事就是:某某大牛写了一个开源项目,另一个大牛有感觉不爽的地方。提
了意见建议,人家又不肯改。结果,这另外一个大牛,就一怒之下,另起炉灶,写了一个新的开源项目。
历史原因 有一篇很有意思的文章,解释了《为什么Vim使用HJKL键作为方向键 》,其实原因很简单。当Bill Joy创建Vi文本编辑器时,他使用的机器机器是ADM-3A终端机,这机器就是把HJKL键作为方向键的。
如何搞清楚这些所以然呢?
思考 当然,这种思考应该伴随我们“通过开源项目,学习软件开发”的始终。但是,从方法论来说,可以尝试从以下一些角度出发来思考:
- 如果我来做一个,会如何去做?
- 如果能够对这个项目做减法,我可以去掉那些模块和代码?真的能够去掉吗?
- 通过阅读单元测试,理解开发者的设计思路。
- 尝试做一些破坏或者修改,来理解项目中的那些做法。这个在下一章会更详细的讨论。
讨论 到开源社区去,发起一些讨论。当然,前提是你必须经过足够深入的思考。不要尽是问一些傻问题
向作者提问 与上面的讨论类似,前提是要先思考。当然,还有一个讨巧的办法,你可以提出:翻译这个开源项目的英文文档,然后,对方当然会很高兴——国际化了嘛。然后,你可以在翻译的过程中,提出各种各样的问题。。。
阅读指南
有些著名的开源项目,本身也非常复杂,所以会有一些文档与书籍:linux内核代码分析、MySQL源码解读、PHP源码分析等等。如果有心学习这样的大
型开源项目,这种入门指南,也是很有价值的。但是,这毕竟是别人嚼过的饭,肯定不如自己去啃来的香。所以,还是回到那句老话,源代码才是最有营养的。
参考文档
- 看源代码那些事
- Tomcat7调试运行环境搭建与源代码分析入门
- Redis代码阅读
- 通过修改JRuby,给我的ruby代码加密
- 转自:http://sbbs.me/view_article/5061b20608158e116a000066