读码破万卷,敲键如有神。
概述###
要成为作家, 需要阅读大量的文学作品;要成为一流的开发者,需要阅读大量的优秀代码。程序设计与开发,可视之为逻辑的武学;解锁优秀代码的能力,是开发者的内功心法之一。
优秀源代码就像一座宝库,里面藏着关于逻辑的奇珍异宝,非常值得一探哦!
- 解读设计思想,理解技术原理和实现,遇到类似问题时,能够应用到解决方案中;
- 积累好的代码,胜于重新造轮子。
怎样才算真正理解了源代码呢? 一般做到如下几点:
- 预分析与反馈总结。自己来设计,思考了哪些要素;优秀的实现,考虑了哪些要素。作个对比,能够有更多领悟和收获;
- 设计思想。蕴含了哪些设计思想,记录下来;
- 核心技术。哪些是核心技术点,是不可替换的 ?
- 细节与发现。有哪些值得关注的细节 ? 有什么重要的发现 ?
写一篇源码阅读笔记,记录其中的设计思想、核心技术和细节,重要或出乎意料的发现。
本文以 Guava.Cache 为例,探讨如何解读优秀源代码。
导图###
流程与步骤###
STEP0. 了解核心使用场景及预分析
可以到官网上去了解所要解读代码的核心使用场景,有哪些优势和不足。
思考一下:如果是自己来设计,会如何来做,考虑哪些因素? 做一定的思考后,再比对别人的实现,对比中可以有更多的收获和领悟。
示例: Guava.Cache 用于创建一个可靠的本地缓存。 如果我来设计,会考虑多线程访问的并发安全性,比如使用 ConcurrentHashMap 。在 Guava 实现里,还考虑了缓存相关的统计,比如命中率,这对于衡量缓存效果是非常重要的;还记录了移除缓存的原因和监听器等。
STEP1. 了解系统的整体设计及概念
与常见业务系统冗长混乱的方法迥异, 优秀的系统通常会有一个优雅的整体设计, 将一组小而简洁的概念串联起来。 先了解系统的整体框架和所基于的基本概念, 会对理解系统的构造与运行有一个总体的引导作用。 同时, 在设计自己的系统时, 也会从中获得很好的启发。
示例: Guava.Cache ,基本概念如下:
- KV : 缓存通常是 KV 结构,基本原理是 Hash 查找;
- CacheBuilderSpec : 缓存的规格,用来设置缓存的一些基本参数,调节缓存的行为;
- CacheLoader : 从 Key 值计算出 Value 值的计算函数;
- CacheBuilder : 根据 CacheBuilderSpec 及 CacheLoader 创建具体的 LocalCache;
- Cache : 缓存对象,存储一定时间期限的KV值;
- ObjectPool : 对象池,缓存通常要使用到池,避免过度膨胀;
- 并发 : 可以通过 Hash 分段表来增大并发吞吐量。
STEP2. 熟悉项目代码组织结构
优秀的开源项目通常会有清晰而有层次化的项目组织结构。
拿到项目源代码, 第一件事:概览项目组织结构, 弄清楚哪些主要模块(包), 各个模块(包) 的主要作用;各个模块(包) 下面有哪些主要类, 其作用如何。 这一步可以采用大胆猜测加API文档说明的方法来完成。
通常, 最顶层包往往包含与客户端使用直接相关的类和方法, 其下层的子包完成某个子功能或特殊模块。 此外, 优秀的项目代码通常会对系统所涉及的每个点都有一个较好的抽象, 使用一个小而简洁的类或接口来表达, 而不是混杂在大的类中。 哪怕只是一些简单的颜色字符串, 也会尽力用枚举来实现它。很多 Javaweb 项目默认采用 Controller - Service - (Dao, RPC) - Model - Utils 或 API - Service - Dependency - Biz - Domain - Common - Config - Deploy 的整体组织模式, 这进一步降低了阅读 Java 项目的难度。
示例: 由于这只是个小的功能,因此都组织在包 com.google.guava.common.cache 下。 如果要看更复杂层次的包结构,可以看 com.google.code.json , junit, Spring 等。
STEP3. 确立目标,缩小范围
在开始阅读源代码之前, 心里要定一个目标去驱动阅读。 比如说, 要探究 proxools 连接池有时获取不到数据库连接的问题, 或者弄清楚 extjs 分页控件 PagingBar & java 线程池 ThreadPoolExecutor 的内部实现。 先攻取一个小分支, 将阅读范围缩小到容易完成的程度。物理学家一上来也不是能一下子弄懂整个宇宙的。
示例:研究 Guava.Cache ,主要是想弄清楚里面的原理和实现。
STEP4. 找到切入点
可以从外部行为切入。 阅读 API 文档, 理解其外部行为,弄清楚设计所针对的需求目标,写个 Demo 单步调试。
还可以根据实际关注点切入。 比如:使用 Proxool 连接池, 我更关心是如何获取数据库连接的, 可以从 ProxoolDataSource.getConnection 方法切入; 使用线程池, 可以从 ThreadPoolExecutor 切入; 可以使用 junit ,从 TestCase 切入。 一般来说,从所使用的客户端类切入是一个不错的选择。
示例:可以编写一个 LocalCache 的 Demo ,然后单步调试进入。
STEP5. 锁定主要和核心的类与方法
任何设计都会隐式或显式地有“关键角色” 与 “支撑角色” 的分工。阅读源代码并不是盲目漫无目的的行为, 而是要先锁定主要和核心的类与方法, 作为阅读的引路灯。在 Guava.Cache 里,核心类就是 LocalCache ,而 CacheLoader, CacheBuilderSpec, CacheBuilder 都是支撑类。
STEP6: 标记主要流程, 绘制协作交互图
跳过各种细节, 主要集中于弄清楚主要流程, 由哪些模块、类以及哪些方法参与, 标记、绘制协作交互图。
示例: 创建 CacheBuilderSpec -> 创建 CacheLoader -> 创建 Cache 实现 -> 使用 Cache
STEP7: 分解细节,各个击破
完成 STEP6 之后, 通常对该框架已经有了一个整体的理解, 虽然还有很多细节不清楚。 没关系! 优秀源码为了追求灵活性和可扩展性,具有更强的缜密度, 相对比业务系统代码更难理解一些。 尤其是细节盘根错节,相互依赖影响。
一行行代码去读,是比较笨拙的;可以将这些细节分离和提炼出多个关注点,理解关注点是如何实现的,在关注点的导引下去阅读这些细节性的代码,会更加高效一点。将多个关注点分解成多个小任务, 各个击破。 这一步需要反复多次地进行, 可能需要查阅很多知识点和接触一些“阴暗之处”, 才能逐渐抵达系统的“真相”, 最终作出对其优缺点的合理的综合评定。
这一步比较困难,需要一些技巧和耐心。单独再来讨论这个话题。
STEP8: 实践,再实践!
由易入难,循序渐进。
- 不含复杂技术点的独立类。比如 Integer , 只要编程基础就能读懂。
- 不含复杂技术点的含有少量交互的框架。比如 JDK Collection,commons-collections, 需要数据结构与算法基础。
- 不含复杂技术点的交互度适中的简易框架。比如 junit ,主要是类与交互的设计。
- 含有并发但不含交互的独立类。比如 AbstractQueuedSynchronizer , 并发工具的基础抽象类。
- 含有并发并只含有少量交互的简单框架。比如 ThreadExecutor , Google.Cache 。
- ???
- 含有大量技术点和交互的实用性框架,比如 Spring , Struts 等。
模式识别与积累###
代码编写和程序设计都有一些模式可以遵循。 熟悉这些代码与设计模式对理解源代码也有很好的帮助。 比如, 面向对象系统中就通常有如下几种基本模式。
独立类
独立类完成其独立的功能,会引用到其它类的方法, 但交互不会复杂。 例如 java.util.Arrays , java.lang.Integer 类;
支撑类
支撑类用来表达一个小而简洁的抽象, 它通常不直接拿来用, 而是被其它类引用来构造更复杂的功能, 比如 java.util.AbstractMap$SimpleEntry;
继承型交互关系
继承型的交互关系遵循"接口-抽象类-具体类" 的模式: 接口规定行为规范, 抽象类完成用于定制的骨架实现, 具体类则实现具体完整的功能,可以直接拿来用。 例如经典“三段式” : Map -> AbstractMap -> HashMap 。
继承性交互关系,要体会接口是怎么设计的,为什么需要定义这些方法;要体会抽象类是怎么设计的,如何将通用流程和钩子方法提炼出来。
委托型交互关系
委托型交互遵循“封装或代理”模式,将一部分或全部的功能实现委托转发给其它类的实现, 可能会做一点封装或代理操作。比如 ForwardingLoadingCache , 将缓存操作全部转发给另一个缓存。
组合与混合
实际应用中, 通常是多种模式混合而成。比如 :
- class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> 是“接口-抽象类-具体实现”的继承性交互关系;
- static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> implements LoadingCache<K, V> 是“接口-实现类-子类”的继承性交互关系;
- public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> 是 “接口-接口”的继承关系;
- LocalLoadingCache 的缓存操作实现,是委托给 LocalCache 来实现的,是“实现类-实现类”的代理关系;
- ReentrantLock 委托 Sync 实现,而 Sync 继承自 AbstractQueuedSynchronizer。
在解决具体设计问题时, 会应用到一些常见的设计模式, 比如单例模式、观察者模式、装饰器模式、代理模式、责任链模式等, 熟悉这些设计模式也是必要的, 详见《设计模式-可复用面向对象软件的基础》 一书,在 “软件设计要素初探:基础设计模式概览”一文中亦有简短的总结。
技巧与手段###
-
边阅读边注释。对读过的地方做些必要的注释, 主要突出其用途,必要的实现细节; 可以边读边做些笔记。
-
搭建环境, 运行, 调试。搭建好源码环境,写个单测,运行起来, 然后设置断点调试, 观察结果, 很好的跟踪方式;
-
从简单着手, 善于分解小任务。对 Spring , Tomcat 的源代码无从下手? 从 JDK, Junit 看起; 看不懂 Extjs Grid 的代码? 从 Label, ComboBox 着手; 一个庞大的类看的吃力? 不妨将其分解成多个小任务, 各个击破。
-
找点其它的事情做做。阅读源代码可不是这世界最美妙的事情。 如果起初不是很适应的话, 可以先读若干函数, 然后做点其它事, 比如活动活动, 听听歌看看视频, 然后再回来阅读, 反复如此, 来逐渐增强这种耐心和适应感。
心理锻炼###
神秘有难度
通常会认为优秀源代码很牛逼,反而敬而远之,不敢入宝山而探之。 其实,优秀源代码很平常,与日常所写的代码,是同样的原材料和材质。不同的是: 1. 组织条理性更强; 2. 逻辑更缜密。 这不正是所需要学习的地方吗?
此外,总会遇到看不懂,想不通的地方; 这时, 可能需要弥补下基础知识(数据结构与算法、设计模式、并发、网络协议、系统原理等), 也可能学习到某种高级技巧(函数式、位运算、字节码等), 一定不要放过这种学习机会。
耐心与毅力
阅读源码很好滴锻炼耐心和毅力,而耐心和毅力均是可贵的品质。 阅读源码,起初是有些艰难,但是,一旦攻下,就为后面的前进打下很好的铺垫了。
强化练习
有两种模式:
- 初期,可以固定时段,比如每天早上或晚上半小时阅读源代码,建立反射弧;
- 中期,可以集中时段,强化阅读大量源代码。 有时候 “自虐”一下,会有更大的成长。
实际障碍###
英语能力
emmm... 忘了说关键的一点了,阅读源代码需要一定的英语能力。 怎么办呢 ? 学呀,备个英文词典呗!
没有时间
想做一定会挤出时间。欲望要足够强烈。
小结###
就像任何一门技艺一样, 阅读源代码的技能也要从基础一点一点的训练, 直到娴熟、炉火纯青的境界。