代码整洁之道.pdf
http://www.100md.com
2019年12月18日
![]() |
| 第1页 |
![]() |
| 第10页 |
![]() |
| 第18页 |
![]() |
| 第26页 |
![]() |
| 第50页 |
![]() |
| 第131页 |
参见附件(10033KB,782页)。
作为一个开发师,写一手整洁的代码是非常的有必要的,不仅自己看着舒服,让同事或者后维护项目的人都能一眼看懂,那么这本代码整洁之道非常适合你学习。

代码整洁之道介绍
软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。《代码整洁之道》提出一种观念:代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。作为编程领域的佼佼者,《代码整洁之道》作者给出了一系列行之有效的整洁代码操作实践。这些实践在《代码整洁之道》中体现为一条条规则(或称“启示”),并辅以来自现实项目的正、反两面的范例。只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。
《代码整洁之道》阅读对象为一切有志于改善代码质量的程序员及技术经理。书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。
代码整洁之道作者解决
Robert C. Martin,是软件工程领域的大师级人物,是《敏捷软件开发:原则、模式与实践》、《敏捷软件开发:原则、模式与实践(C#版)》(邮电)、《极限编程实践》(邮电)等国内引进的畅销书的作者,其中原著荣获美国《软件开发》第13届震憾(Jolt)大奖,Martin的敏捷系列书是软件工程界书籍。本书是他的又一力作。
Martin在书中对代码具有革命性的解读
阐述了整洁代码的佳敏捷实践的方法
书中介绍规则均来自Martin多年的经验,拥有很高的借鉴价值
韩磊,互联网产品与运营专家,技术书籍著译者。曾在全球的IT中文社区CSDN及《程序员》杂志任副总经理、总编辑等职。现居广州。译著有《梦断代码》和《C#编程风格》。与刘韧合著《网络媒体教程》,与戴飞合译《BeginningC#Objects中文版:概念到代码》。
代码整洁之道部分目录
第1章 整洁代码
第2章 有意义的命名
第3章 函数
第4章 注释
第5章 格式
第6章 对象和数据结构
第7章 错误处理
第8章 边界
第9章 单元测试
代码整洁之道内容预览
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。代码清单3-1显然想做好几件事。它创建缓冲区、获取页面、搜索继承下来的页面、渲染路径、添加神秘的字符串、生成HTML,如此等等。代码清单3-1手忙脚乱。而代码清单3-3则只做一件简单的事。它将设置和拆解包纳到测试页面中。
过去30年以来,以下建议以不同形式一再出现:函数应该做一件事。做好这件事。只做这一件事。
问题在于很难知道那件该做的事是什么。
代码清单3.3只做了一件事,对吧?其实也很容易看作是三件事:(1)判断是否为测试页面;(2)如果是,则容纳进设置和分拆步骤;(3)渲染成HTML。如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。
编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
代码清单3.1明显包括了处于多个不同抽象层级的步骤。显然,它所做的不止一件事。即便是代码清单3-2也有两个抽象层,这已被我们将其缩短的能力所证明。然而,很难再将代码清单3.3做有意义的缩短。可以将if语句拆出来做一个名为include Setup And Teardonws Ifrestpage的函数,但那只是重新诠释代码,并未改变抽象层级。
所以,要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。
代码整洁之道截图


目 录
封面
扉页
版权
内容提要
序
关于封面
代码猴子与童子军军规
前言
第1章 整洁代码
1.1 要有代码
1.2 糟糕的代码
1.3 混乱的代价
1.3.1 华丽新设计
1.3.2 态度
1.3.3 迷题
1.3.4 整洁代码的艺术
1.3.5 什么是整洁代码
1.4 思想流派
1.5 我们是作者
1.6 童子军军规
1.7 前传与原则
1.8 小结
1.9 文献
第2章 有意义的命名
2.1 介绍
2.2 名副其实2.3 避免误导
2.4 做有意义的区分
2.5 使用读得出来的名称
2.6 使用可搜索的名称
2.7 避免使用编码
2.7.1匈牙利语标记法
2.7.2 成员前缀
2.7.3 接口和实现
2.8 避免思维映射
2.9 类名
2.10 方法名
2.11 别扮可爱
2.12 每个概念对应一个词
2.13 别用双关语
2.14 使用解决方案领域名称
2.15 使用源自所涉问题领域的名称
2.16 添加有意义的语境
2.17 不要添加没用的语境
2.18 最后的话
第3章 函数
3.1 短小
3.2 只做一件事
3.3 每个函数一个抽象层级
3.4 switch语句
3.5 使用描述性的名称
3.6 函数参数
3.6.1 一元函数的普遍形式
3.6.2 标识参数3.6.3 二元函数
3.6.4 三元函数
3.6.5 参数对象
3.6.6 参数列表
3.6.7 动词与关键字
3.7 无副作用
3.8 分隔指令与询问
3.9 使用异常替代返回错误码
3.9.1 抽离TryCatch代码块
3.9.2 错误处理就是一件事
3.9.3 Error.java依赖磁铁
3.10 别重复自己
3.11 结构化编程
3.12 如何写出这样的函数
3.13 小结
3.14 SetupTeardownIncluder程序
3.15 文献
第4章 注释
4.1 注释不能美化糟糕的代码
4.2 用代码来阐述
4.3 好注释
4.3.1 法律信息
4.3.2 提供信息的注释
4.3.3 对意图的解释
4.3.4 阐释
4.3.5 警示
4.3.6 TODO注释
4.3.7 放大4.3.8 公共API中的Javadoc
4.4 坏注释
4.4.1 喃喃自语
4.4.2 多余的注释
4.4.3 误导性注释
4.4.4 循规式注释
4.4.5 日志式注释
4.4.6 废话注释
4.4.7 可怕的废话
4.4.8 能用函数或变量时就别用注释
4.4.9 位置标记
4.4.10 括号后面的注释
4.4.11 归属与署名
4.4.12 注释掉的代码
4.4.13 HTML注释
4.4.14 非本地信息
4.4.15 信息过多
4.4.16 不明显的联系
4.4.17 函数头
4.4.18 非公共代码中的Javadoc
4.4.19 范例
4.5 文献
第5章 格式
5.1 格式的目的
5.2 垂直格式
5.2.1 向报纸学习
5.2.2 概念间垂直方向上的区隔
5.2.3 垂直方向上的靠近5.2.4 垂直距离
5.2.5 垂直顺序
5.3 横向格式
5.3.1 水平方向上的区隔与靠近
5.3.2 水平对齐
5.3.3 缩进
5.3.4 空范围
5.4 团队规则
5.5 鲍勃大叔的格式规则
第6章 对象和数据结构
6.1 数据抽象
6.2 数据、对象的反对称性
6.3 得墨忒耳律
6.3.1 火车失事
6.3.2 混杂
6.3.3 隐藏结构
6.4 数据传送对象
6.5 小结
6.6 文献
第7章 错误处理
7.1 使用异常而非返回码
7.2 先写Try-Catch-Finally语句
7.3 使用不可控异常
7.4 给出异常发生的环境说明
7.5 依调用者需要定义异常类
7.6 定义常规流程
7.7 别返回null值
7.8 别传递null值7.9 小结
7.10 文献
第8章 边界
8.1 使用第三方代码
8.2 浏览和学习边界
8.3 学习log4j
8.4 学习性测试的好处不只是免费
8.5 使用尚不存在的代码
8.6 整洁的边界
8.7 文献
第9章 单元测试
9.1 TDD三定律
9.2 保持测试整洁
9.3 整洁的测试
9.3.1 面向特定领域的测试语言
9.3.2 双重标准
9.4 每个测试一个断言
9.5 F.I.R.S.T.
9.6 小结
9.7 文献
第10章 类
10.1 类的组织
10.2 类应该短小
10.2.1 单一权责原则
10.2.2 内聚
10.2.3 保持内聚性就会得到许多短小的类
10.3 为了修改而组织
10.4 文献第11章 系统
11.1 如何建造一个城市
11.2 将系统的构造与使用分开
11.2.1 分解main
11.2.2 工厂
11.2.3 依赖注入
11.3 扩容
11.4 Java代理
11.5 纯Java AOP框架
11.6 AspectJ的方面
11.7 测试驱动系统架构
11.8 优化决策
11.9 明智使用添加了可论证价值的标准
11.10 系统需要领域特定语言
11.11 小结
11.12 文献
第12章 迭进
12.1 通过迭进设计达到整洁目的
12.2 简单设计规则1:运行所有测试
12.3 简单设计规则2~4:重构
12.4 不可重复
12.5 表达力
12.6 尽可能少的类和方法
12.7 小结
12.8 文献
第13章 并发编程
13.1 为什么要并发
13.2 挑战13.3 并发防御原则
13.3.1 单一权责原则
13.3.2 推论:限制数据作用域
13.3.3 推论:使用数据复本
13.3.4 推论:线程应尽可能地独立
13.4 了解Java库
13.5 了解执行模型
13.5.1 生产者-消费者模型
13.5.2 读者-作者模型
13.5.3 宴席哲学家
13.6 警惕同步方法之间的依赖
13.7 保持同步区域微小
13.8 很难编写正确的关闭代码
13.9 测试线程代码
13.9.1 将伪失败看作可能的线程问题
13.9.2 先使非线程代码可工作
13.9.3 编写可插拔的线程代码
13.9.4 编写可调整的线程代码
13.9.5 运行多于处理器数量的线程
13.9.6 在不同平台上运行
13.9.7 装置试错代码
13.9.8 硬编码
13.9.9 自动化
13.10 小结
13.11 文献
第14章 逐步改进
14.1 Args的实现
14.2 Args:草稿14.2.1 所以我暂停了
14.2.2 渐进
14.3 字符串参数
14.4 小结
第15章 JUnit内幕
15.1 JUnit框架
15.2 小结
第16章 重构SerialDate
16.1 首先,让它能工作
16.2 让它做对
16.3 小结
16.4 文献
第17章 味道与启发
17.1 注释
17.2 环境
17.3 函数
17.4 一般性问题
17.5 Java
17.6 名称
17.7 测试
17.8 小结
17.9 文献
附录A 并发编程II
A.1 客户端服务器的例子
A.1.1 服务器
A.1.2 添加线程代码
A.1.3 观察服务器端
A.1.4 小结A.2 执行的可能路径
A.2.1 路径数量
A.2.2 深入挖掘
A.2.3 小结
A.3 了解类库
A.3.1 Executor框架
A.3.2 非锁定的解决方案
A.3.3 非线程安全类
A.4 方法之间的依赖可能破坏并发代码
A.4.1 容忍错误
A.4.2 基于客户代码的锁定
A.4.3 基于服务端的锁定
A.5 提升吞吐量
A.5.1 单线程条件下的吞吐量
A.5.2 多线程条件下的吞吐量
A.6 死锁
A.6.1 互斥
A.6.2 上锁及等待
A.6.3 无抢先机制
A.6.4 循环等待
A.6.5 不互斥
A.6.6 不上锁及等待
A.6.7 满足抢先机制
A.6.8 不做循环等待
A.7 测试多线程代码
A.8 测试线程代码的工具支持
A.9 小结
A.10 教程:完整代码范例A.10.1 客户端服务器非线程代码
A.10.2 使用线程的客户端服务器代码
附录B org.jfree.date.SerialDate
结束语代码整洁之道
[美]Robert C.Martin 著
韩磊 译
人民邮电出版社
北京图书在版编目(CIP)数据
代码整洁之道(美)马丁(Martin,R.C)著;韩磊译.--北京:人民
邮电出版社,2010.1
ISBN 978-7-115-21687-8
Ⅰ.①代… Ⅱ.①马…②韩… Ⅲ.①软件开发 Ⅳ.①TP311.52
中国版本图书馆CIP数据核字(2009)第202911号
版权声明
Authorized translation from the English language edition,entitled Clean
Code:A Handbook of Agile Software Craftsmanship,9780132350884 by
Robert C.Martin,published by Pearson Education,Inc,publishing as Prentice
Hall,Copyright?2009 Pearson Education,Inc.
All rights reserved.No part of this book may be reproduced or
transmitted in any form or by any means,electronic or mechanical,including
photocopying,recording or by any information storage retrieval
system,without permission from Pearson Education,Inc.CHINESE
SIMPLIFIED language edition published by PEARSON EDUCATION
ASIA LTD.,and POSTS TELECOMMUNICATIONS PRESS
Copyright?2009.
本书封面贴有Pearson Education(培生教育出版集团)激光防伪
标签。无标签者不得销售。
代码整洁之道
◆著 [美]Robert C.Martin
译 韩磊
责任编辑 刘映欣
◆人民邮电出版社出版发行 北京市崇文区夕照寺街14号
邮编 100061 电子函件 315@ptpress.com.cn
网址 http:www.ptpress.com.cn三河市潮河印业有限公司印刷
◆开本:800×1000 116
印张:25.5
字数:554千字 2010年1月第1版
印数:1-5000册 2010年1月河北第1次印刷
著作权合同登记号 图字:01-2008-5467号
ISBN 978-7-115-21687-8
定价:59.00元
读者服务热线:(010)67132705 印装质量热线:(010)67129223
反盗版热线:(010)67171154内容提要
软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相
关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。
本书提出一种观念:代码质量与其整洁度成正比。干净的代码,既
在质量上较为可靠,也为后期维护、升级奠定了良好基础。作为编程领
域的佼佼者,本书作者给出了一系列行之有效的整洁代码操作实践。这
些实践在本书中体现为一条条规则(或称“启示”),并辅以来自现实项
目的正、反两面的范例。只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。
本书阅读对象为一切有志于改善代码质量的程序员及技术经理。书
中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编
程方面,虽为一“家”之言,然诚有可资借鉴的价值。序
乐嚼(Ga-Jol)是在丹麦最受欢迎的糖果品种之一,它浓郁的甘草
味道,完美地弥补了此地潮湿且时常寒冷的天气。对于我们这些丹麦
人,乐嚼的妙处还在于包装盒顶上印制的哲言慧语。今早我买了一包两
件装,在其包装盒上发现这句丹麦谚语:
rlighed i sm? ting er ikke nogen lille ting.
“小处诚实非小事。”这句话正好是我想在这里说的。以小见大。本
书写到了一些价值殊胜的小主题。
神在细节之中,建筑师Ludwig mies van der Rohe(路德维希·密斯·
范·德·罗)[1]如是说。这句话引发了有关软件开发、特别是敏捷软件开
发中架构所处地位的若干争论。鲍勃(Bob)[2]和我时常发现自己沉湎
于此类对话中。没错,Ludwig mies van der Rohe的确专注于效用和基于
宏伟架构之上的永恒建筑形式。然而,他也为自己设计的每所房屋挑选
每个门把手。为什么?因为小处见大。
就 TDD[3]话题展开目前仍在继续的“辩论”时,鲍勃和我认识到,我们均同意软件架构在开发中占据重要地位,但就其确切意义而言,我
们之间还有分歧。然而,这种矛与盾孰利的讨论相对而言并不重要,因
为在项目开始之时,我们理所当然应该让专业人士投入些许时间去思考
及规划。20世纪90年代末期有关仅以测试和代码驱动设计的概念已一去
不返。相对于任何宏伟愿景,对细节的关注甚至是更为关键的专业性基
础。首先,开发者通过小型实践获得可用于大型实践的技能和信用度。
其次,宏大建筑中最细小的部分,比如关不紧的门、有点儿没铺平的地板,甚至是凌乱的桌面,都会将整个大局的魅力毁灭殆尽。这就是整洁
代码之所系。
架构只是软件开发用到的借喻之一,主要用在那种等同于建筑师交
付毛坯房一般交付初始软件产品的场合。在Scrum和敏捷(Agile)的日
子里,人们关注的是快速将产品推向市场。我们要求工厂全速运转、生
产软件。这就是人类工厂:懂思考、会感受的编码人,他们由产品备忘
或用户故事开始创造产品。来自制造业的借喻在这种场合大行其道。例
如,Scrum 就从装配线式的日本汽车生产方式中获益良多。
即便是在汽车工业里,大量工作也并不在于生产而在于维护——或
避免维护。对于软件而言,百分之八十或更多的工作量集中在我们美其
名曰“维护”的事情上:其实就是修修补补。与其接受西方关于制造好软
件的传统看法,不如将其看作建筑工业中的房屋修理工,或者汽车领域
的汽修工。日本式管理对于这种事怎么说的呢?
大约在1951年,一种名为“全员生产维护”(Total Productive
Maintenance,TPM)的质量保证手段在日本出现。它关注维护甚于关
注生产。TPM的主要支柱之一是所谓的5S原则体系。5S是一套规程,用“规程”这个词,是为了读者便于理解。5S原则其实是精益(Lean)
——西方视野中的一个时髦词,也是在软件领域渐领风骚的时髦词——
的基石所在。正如鲍勃大叔(Uncle Bob)在前言中写到的,良好的软
件实践遵循这些规程:专注、镇定和思考。这并非总只有关实作,有关
推动工厂设备以最高速度运转。5S哲学包括以下概念:
整理(Seiri)[4],或谓组织(想想英语中的sort(分类、排序)一
词)。搞清楚事物之所在——通过恰当地命名之类的手段——至关重
要。觉得命名标识无关紧要?读读后面的章节吧。
整顿(Seiton),或谓整齐(想想英文中的systematize(系统化)
一词)。有句美国老话说:物皆有其位,而后物尽归其位(A place for
everything, and everything in its place)。每段代码都该在你希望它所在的地方——如果不在那里,就需要重构了。
清楚(Seiso),或谓清洁(想想英文中的shine(锃亮)一词)。
清理工作地的拉线、油污和边角废料。对于那种四处遗弃的带注释的代
码及反映过往或期望的无注释代码,本书作者怎么说的来着?除之而后
快。
清洁(Seiketsu),或谓标准化。有关如何保持工作地清洁的组内
共识。本书有没有提到在开发组内使用一贯的代码风格和实践手段?这
些标准从哪里来?读读看。
身美(Shitsuke)[5],或谓纪律(自律)。在实践中贯彻规程,并
时时体现于个人工作上,而且要乐于改进。
如果你接受挑战——没错,就是挑战,阅读并应用本书,你就会理
解和赞赏上述最后一条。我们最终是在驶向一种负责任的专业精神之根
源所在,这种专业性隶属于一个关注产品生命周期的专业领域。在我们
遵循 TPM 来维护机动车和其他机械时,停机维护——等待缺陷显现出
来——并不常见。我们更上一层楼:每天检查机械,在磨损机件停止工
作之前就换掉它,或者按常例每1000英里(约1609.3km)就更换润滑
油、防止磨损和开裂。对于代码,应无情地做重构。还可以更进一步,就像TPM运动在50多年前的创新:一开始就打造更易维护的机械。写出
可读的代码,重要程度不亚于写出可执行的代码。1960年左右,围绕
TPM引入的终极实践(ultimate practice),关注用全新机械替代旧机
械。诚如Fred Brooks所言,我们或许应该每7年就重做一次软件的主要
模块,清理缓慢陈腐的代码。也许我们该把重构周期从以年计缩短到以
周、以天甚至以小时计。那便是细节所在了。
细节中自有天地,而在生活中应用此类手段时也有微言大义,就像
我们一成不变地对那些源自日本的做法寄予厚望一般。这并非只是东方
的生活观;英美民间也遍是这类警句。上引“整顿”(Seiton)二字就曾
出现在某位俄亥俄州牧师的笔下,他把齐整看作是“荡涤种种罪恶之良方”。“清楚”(Seiso)又如何呢?整洁近乎虔诚(Cleanliness is next to
godliness)。一张脏乱的桌子足以夺去一所丽宅的光彩。老话怎么
说“身美”(Shitsuke)的?守小节者不亏大节(He who is faithful in little
is faithful in much)。对于时时准备在恰当时机做重构,为未来
的“大”决定夯实基础,而不是置诸脑后,有什么说法吗?及时一针省九
针(A stitch in time saves nine)。早起的鸟儿有虫吃(The early bird
catches the worm)。日事日毕(Don’t put off until tomorrow what you can
do today)。在精益实践落入软件咨询师之手前,这就是其所谓“最后时
机”的本义所在。摆正单项工作在整体中的位置呢?巨木生于树籽
(Mighty oaks from little acorns grow)。如何在日常生活中做好简单的
防备性工作呢?防病好过治病(An ounce of prevention is worth a pound
of cure)。一天一苹果,医生远离我(An apple a day keeps the doctor
away)。整洁代码以其对细节的关注,荣耀了深埋于我们现有、或曾
有、或该有的壮丽文化之下的智慧根源。
即便是在宏伟的建筑作品中,我们也听到关注细节的回响。想想
Ludwig mies van der Rohe的门把手吧。那正是整理(seiri)。认真对待
每个变量名。你当用为自己第一个孩子命名般的谨慎来给变量命名。
正如每位房主所知,此类照料和修葺永无休止。建筑师Christopher
Alexander——模式与模式语言之父——把每个设计动作看作是较小的局
部修复动作。他认为,设计良好结构才是建筑师的本职所在,而更大的
建筑形态则当留给模式及居住者搬进的家私来完成。设计始终在持续进
行,不只是在新建一个房间时,也在我们重新粉刷墙面、更换旧地毯或
者换厨房水槽时。大多数艺术门类也持类似主张。在寻找其他推崇细节
的人时,我们发现,19世纪法国作家Gustav Flaubert(古斯塔夫·福楼
拜)名列其中。法国诗人Paul Valery(保尔·瓦雷里)认为,每首诗歌都
无写完之时,得持续重写,直至放弃为止。全心倾注于细节,屡见于追
求卓越的行为之中。虽然这无甚新意,但阅读本书对读者仍是一种挑战,你要重拾久已弃置脑后的良好规则,自发自主,“响应改变”。
不幸的是,我们往往见不到人们把对细节的关注当作编程艺术的基
础要件。我们过早地放弃了在代码上的工作,并不是因为它业已完成,而是因为我们的价值体系关注外在表现甚于关注要交付之物的本质。疏
忽最终结出了恶果:坏东西一再出现。无论是在行业里还是学术领域,研究者都很重视代码的整洁问题。供职于贝尔软件生产研究实验室
(Bell Labs Software Production Research)——没错,就是生产!——
时,我们有些不太严密的发现,认为前后一致的缩进风格明显标志了较
低的缺陷率。我们原指望将质量归因于架构、编程语言或者其他高级概
念;我们的专业能力归功于对工具的掌握和各种高高在上的设计方法,至于那些安置于厂区的机器,那些编码者,他们居然通过简单地保持一
致缩进风格创造了价值,这简直是一种侮辱。我在17年前就在书中写
过,这种风格远不止是一种单纯的能力那么简单。日本式的世界观深知
日常工作者的价值,而且,还深知工作者简单的日常行为所锻造的开发
系统的价值。质量是上百万次全心投入的结果——而非仅归功于任何来
自天堂的伟大方法。这些行为简单却不简陋,也不意味着简易。相反,它们是人力所能达的不仅伟大而且美丽的造物。忽略它们,就不成其为
完整的人。
当然,我仍然提倡放宽思路,也推崇根植于深厚领域知识和软件可
用性的各种架构手法的价值。但本书与此无关——至少,没有明显关
系。本书精妙之处,其意义之深远,不该无人赏识。它正与Peter
Sommerlad、Kevlin Henny及Giovanni Asproni等真正写代码的人现今所
持的观念相吻合。他们鼓吹“代码即设计”和“简单代码”。我们要谨记,界面就是程序,而且其结构也极大地反映出程序结构,但也理应始终谦
逊地承认设计存在于代码中,这至关紧要。制造上的返工导致成本上
升,但重做设计却创造出价值。我们应当视代码为设计——作为过程而
非终点的设计——这种高尚行为的漂亮体现。耦合与内聚的架构韵律在代码中脉动。Larry Constantine以代码的形式——而不是用UML那种高
高在上的抽象概念——来描述耦合与内聚。Richard Garbriel
在“Abstraction Descant”(抽象刍议)一文中告诉我们,抽象即恶。代码
除恶,而整洁的代码则大抵是圣洁的。
回到我那个小小的乐嚼包装盒,我想要重点提一下,那句丹麦谚语
不只是教我们重视小处,更教我们小处要诚实。这意味着对代码诚实、对同僚坦承代码现状,最重要的是在代码问题上不自欺。是否已尽全
力“把露营地清理得比来时还干净”?签入代码前是否已做重构?这可不
是皮毛小事,它正高卧于敏捷价值的正中位置。Scrum 有一种建议的实
践,主张重构是“完成”(Done)概念的一部分。无论是架构还是代码都
不强求完美,只求竭诚尽力而已。人孰无过,神亦容之(To err is
human; to forgive, divine)。在Scrum中,我们使一切可见。我们晾出脏
衣服。我们坦承代码状态,因为它永不完美。我们日渐成为完整的人,配得起神的眷顾,也越来越接近细节中的伟大之处。
在自己的专业领域中,我们亟需能得到的一切帮助。假使干净的地
板能减少事故发生,假使归置到位的工具能提升生产力,我也会倾力做
到。至于本书,在我看过的有关将精益原则应用于软件的印刷品中,是
最具实用性的。那班求索者多年来并肩奋斗,不但是为求一己之进步,更将他们的知识通过和你手上正在做的事一般的工作贡献给这个行业。
看过鲍勃大叔寄来的原稿之后,我发现,世界竟略有改善了。
对高瞻远瞩的练习业已结束,我要去清理自己的书桌了。
James O.Coplien于丹麦默尔鲁普
[1].译注:20世纪中期著名现代建筑大师,秉承“少即是多”的建筑设计
哲学,缔造了玻璃幕墙等现代建筑结构。
[2].译注:本书主要作者Robert C. Martin绰号Uncle Bob,这里的“鲍
勃”及后文的“鲍勃大叔”就是指Robert C. Martin。[3].译注:Test Driven Development,测试驱动开发。
[4].译注:这些概念最初出现于日本,5个概念的日文罗马字拼音首字母
正好都是S,所以这里也保留了日文罗马字拼音写法。中译本以日文汉
字直接译出,读者留意,不可直接对应其中文意思。
[5].译注:中文意为“素养、教养”。关于封面
封面的图片是M104:草帽星系(The Sombrero Galaxy)。M104坐
落于处女座(Virgo),距地球仅3000万光年。其核心是一个质量超大
的黑洞,有100万个太阳那么重。
这幅图是否让你想起了Klingon星球(克林贡)[1]的卫星Praxis(普
拉西斯)爆炸的事?我清楚地记得,在《星舰迷航 VI》中,大爆炸之
后碎片四溅,飞舞出一个赤道光环的场景。至此,光环就成为科幻电影
中爆炸场景的必然产物了。甚至就在《星舰迷航》系列电影的后续情节
中,Alderaan(阿尔德然)的爆炸也有类似场景出现。
环绕M104的光环是什么造成的?它为何会有如此巨大的膨胀率和
如此明亮而微小的内核?在我看来,仿佛那位于中心位置的黑洞勃然大
怒,向星系的中心扔出了一个3万光年大的洞一般。在这场宇宙大崩塌
所及范围之内的居民全都大难临头了。
超大质量的黑洞以星体为食,将星体的相当部分质量转换为能量。
方程式E = MC2已经足够体现杠杆作用了,但当 M 有一颗星体那么大的
质量时,看吧!在那巨兽酒足饭饱之前,有多少星体会一头撞进它的胃
里?核心部分空洞的大小,是否说明了些什么呢?
封面上的M104图片,是用来自于哈勃望远镜的那幅著名的可见光
相片(上图)和Spitzer(斯比泽)轨道探测器最新的红外影像(下图)
组合而成。
在红外影像中,光环中的热粒子闪耀着穿过了中心膨胀体。这两幅影像组合起来,显现出我们从未见过的景象,展示了久远之前曾熊熊燃
烧的火海。
封面图片:来自斯比泽太空望远镜
[1].系列剧《星舰迷航》(Star Trek)中的故事情节,Praxis星爆炸,由
此导致联邦和Klingon达成首次和平协议。代码猴子与童子军军规
2007年3月,我在SD West 2007技术大会上聆听了Robert C.
Martin(鲍勃大叔)的主题演讲“Craftsmanship and the Problem of
Productivity: Secrets for Going Fast without Making a Mess”。一身休闲打
扮的鲍勃大叔,以一曲嘲笑低水平编码者的Code Monkey(代码猴子)
开场。
是的,我们就是一群代码猴子,上蹿下跳,自以为领略了编程的真
谛。可惜,当我们抓着几个酸桃子,得意洋洋坐到树枝上,却对自己造
成的混乱熟视无睹。那堆“可以运行”的乱麻程序,就在我们的眼皮底下
慢慢腐坏。
从听到那场以TDD为主题的演讲之后,我就一直关注鲍勃大叔,还
有他在TDD和整洁代码方面的言论。去年,人民邮电出版社计算机分社
拿一本书给我看,封面上赫然写着Robert C. Martin的大名。看完原书序
和前言,我已经按捺不住,接下了翻译此书的任务。这本书名为Clean
Code,乃是Object Mentor(鲍勃大叔开办的技术咨询和培训公司)一干
大牛在编程方面的经验累积。按鲍勃大叔的话来说,就是“Object
Mentor整洁代码派”的说明。
正如 Coplien 在序中所言,宏大建筑中最细小的部分,比如关不紧
的门、有点儿没铺平的地板,甚至是凌乱的桌面,都会将整个大局的魅
力毁灭殆尽。这就是整洁代码之所系。Coplien列举了许多谚语,证明整
洁的价值,中国也有修身齐家治国平天下之语。整洁代码的重要性毋庸
置疑,问题是如何写出真正整洁的代码。本书既是整洁代码的定义,亦是如何写出整洁代码的指南。鲍勃大
叔认为,“写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的‘整洁
感’。这种‘代码感’就是关键所在……它不仅让我们看到代码的优劣,还
予我们以借戒规之力化劣为优的攻略。”作者阐述了在命名、函数、注
释、代码格式、对象和数据结构、错误处理、边界问题、单元测试、类、系统、并发编程等方面如何做到整洁的经验与最佳实践。长期遵照
这些经验编写代码,所谓“代码感”也就自然而然滋生出来。更有价值的
部分是鲍勃大叔本人对3个Java项目的剖析与改进过程的实操记录。通
过这多达3章的重构记录,鲍勃大叔充分地证明了童子军军规在编程领
域同样适用:离开时要比发现时更整洁。为了向读者呈现代码的原始状
态,这部分代码及本书其他部分的绝大多数代码注释都不做翻译。如果
读者有任何疑问,可通过邮件与我沟通(cleancode.cn@gmail.com)。
接触开发技术十多年以来,特别是从事IT技术媒体工作六年以来,我见过许多对于代码整洁性缺乏足够重视的开发者。不算过分地说,这
是职业素养与基本功的双重缺陷。我翻译The Elements of C Style(中
译版《C编程风格》)和本书,实在也是希望在这方面看到开发者重视
度和实际应用的提升。
在本书的结束语中,鲍勃大叔提到别人给他的一条腕带,上面的字
样是Test Obsessed(沉迷测试)。鲍勃大叔“发现自己无法取下腕带。不
仅是因为腕带很紧,而且那也是条精神上的紧箍咒。……它一直提醒
我,我做了写出整洁代码的承诺。”有了这条腕带,代码猴子成了模范
童子军。我想,每位开发者都需要这样一条腕带吧?
韩磊
2009年11月前言
衡量代码质量的唯一有效标准:WTFmin
承Thom Holwerda惠允,自http:www.osnews.comstory19266WTFs_m
再制你的代码在哪道门后面?你的团队或公司在哪道门后面?为什么会
在那里?只是一次普通的代码复查,还是产品面世后才发现一连串严重
问题?我们是否在战战兢兢地调试自己之前错以为没问题的代码?客户
是否在流失?经理们是否把我们盯得如芒刺在背?当事态变得严重起
来,如何保证我们在那道正确的门后做补救工作?答案是:技艺
(craftsmanship)。
习艺之要有二:知和行。你应当习得有关原则、模式和实践的知
识,穷尽应知之事,并且要对其了如指掌,通过刻苦实践掌握它。
我可以教你骑自行车的物理学原理。实际上,经典数学的表达方式
相对而言确实简洁明了。重力、摩擦力、角动量、质心等,用一页写满
方程式的纸就能说明白。有了这些方程式,我可以为你证明出骑车完全
可行,而且还可以告诉你骑车所需的全部知识。即便如此,你在初次骑
车时还是会跌倒在地。
编码亦同此理。我们可以写下整洁代码的所有“感觉良好”的原则,放手让你去干(换言之,让你从自行车上摔下来)。那样的话,我们算
是哪门子老师?而你又会成为怎样的学生呢?
不!本书可不会这么做。
学写整洁代码很难。它可不止于要求你掌握原则和模式。你得在这
上面花工夫。你须自行实践,且体验自己的失败。你须观察他人的实践
与失败。你须看看别人是怎样蹒跚学步,再转头研究他们的路数。你须
看看别人是如何绞尽脑汁做出决策,又是如何为错误决策付出代价。
阅读本书要多用心思。这可不是那种降落前就能读完的“感觉不
错”的飞机书。本书要让你用功,而且是非常用功。如何用功?阅读代
码——大量代码。而且你要去琢磨某段代码好在什么地方、坏在什么地
方。在我们分解,而后组合模块时,你得亦步亦趋地跟上。这得花些工
夫,不过值得一试。
本书大致可分为3个部分。前几章介绍编写整洁代码的原则、模式和实践。这部分有相当多的示例代码,读起来颇具挑战性。读完这几
章,就为阅读第2部分做好了准备。如果你就此止步,只能祝你好运
啦!
第2部分最需要花工夫。这部分包括几个复杂性不断增加的案例研
究。每个案例都清理一些代码——把有问题的代码转化为问题少一些的
代码。这部分极为详细。你的思维要在讲解和代码段之间跳来跳去。你
得分析和理解那些代码,琢磨每次修改的来龙去脉。
你付出的劳动将在第3部分得到回报。这部分只有一章,列出从上
述案例研究中得到的启示和灵感。在遍览和清理案例中的代码时,我们
把每个操作理由记录为一种启示或灵感。我们尝试去理解自己对阅读和
修改代码的反应,尽力了解为什么会有这样的感受、为什么会如此行
事。结果得到了一套描述在编写、阅读、清理代码时思维方式的知识
库。
如果你在阅读第2部分的案例研究时没有好好用功,那么这套知识
库对你来说可能所值无几。在这些案例研究中,每次修改都仔细注明了
相关启示的标号。这些标号用方括号标出,如:[H22]。由此你可以看
到这些启示在何种环境下被应用和编写。启示本身不值钱,启示与案例
研究中清理代码的具体决策之间的关系才有价值。
如果你跳过案例研究部分,只阅读了第1部分和第3部分,那就不过
是又看了一本关于写出好软件的“感觉不错”的书。但如果你肯花时间琢
磨那些案例,亦步亦趋——站在作者的角度,迫使自己以作者的思维路
径考虑问题,就能更深刻地理解这些原则、模式、实践和启示。这样的
话,就像一个熟练地掌握了骑车的技术后,自行车就如同其身体的延伸
部分那样;对你来说,本书所介绍的整洁代码的原则、模式、实践和启
示就成为了本身具有的技艺,而不再是“感觉不错”的知识。
致谢
插图感谢两位艺术家Jennifer Kohnke和Angela Brooks。Jennifer绘制了每
章起始处创意新颖、效果惊人的插图,以及Kent Beck、Ward
Cunningham、Bjarne Stroustrup、Ron Jeffries、Grady Booch、Dave
Thomas、Michael Feathers和我本人的肖像。
Angela 绘制了文中那些精致的插图。这些年她为我画了一些画,包
括 Agile Software Development: Principles, Patterns, and Practices(中译版
《敏捷软件开发:原则、模式与实践》)一书中的大量插图。她是我的
长女,常给我带来极大的愉悦。第1章 整洁代码阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好
的程序员。很好。我们需要更好的程序员。
这是本有关编写好程序的书。它充斥着代码。我们要从各个方向来
考察这些代码。从顶向下,从底往上,从里而外。读完后,就能知道许
多关于代码的事了。而且,我们还能说出好代码和糟糕的代码之间的差
异。我们将了解到如何写出好代码。我们也会知道,如何将糟糕的代码
改成好代码。1.1 要有代码
有人也许会以为,关于代码的书有点儿落后于时代——代码不再是
问题;我们应当关注模型和需求。确实,有人说过我们正在临近代码的
终结点。很快,代码就会自动产生出来,不需要再人工编写。程序员完
全没用了,因为商务人士可以从规约直接生成程序。
扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些
层面上,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器
可以执行的细节程度,就是编程要做的事。而这种规约正是代码。
我期望语言的抽象程度继续提升。我也期望领域特定语言的数量继
续增加。那会是好事一桩。但那终结不了代码。实际上,在较高层次上
用领域特定语言撰写的规约也将是代码!它也得严谨、精确、规范和详
细,好让机器理解和执行。
那帮以为代码终将消失的伙计,就像是巴望着发现一种无规范数学
的数学家们一般。他们巴望着,总有一天能创造出某种机器,我们只要
想想、嘴都不用张就能叫它依计行事。那机器要能透彻理解我们,只有
这样,它才能把含糊不清的需求翻译为可完美执行的程序,精确满足需
求。
这种事永远不会发生。即便是人类,倾其全部的直觉和创造力,也
造不出满足客户模糊感觉的成功系统来。如果说需求规约原则教给了我
们什么,那就是归置良好的需求就像代码一样正式,也能作为代码的可
执行测试来使用。
记住,代码确然是我们最终用来表达需求的那种语言。我们可以创
造各种与需求接近的语言。我们可以创造帮助把需求解析和汇整为正式结构的各种工具。然而,我们永远无法抛弃必要的精确性——所以代码
永存。1.2 糟糕的代码
最近我在读Kent Beck著Implementation Patterns(中译版《实现模
式》)[1]一书的序言。他这样写道:“……本书基于一种不太牢靠的前
提:好代码的确重要……”这前提不牢靠?我反对!我认为这是该领域
最强固、最受支持、最被强调的前提了(我想Kent也知道)。我们知道
好代码重要,是因为其短缺实在困扰了我们太久。
20 世纪 80 年代末,有家公司写了个很流行的杀手应用,许多专业
人士都买来用。然后,发布周期开始拉长。缺陷总是不能修复。装载时
间越来越久,崩溃的几率也越来越大。至今我还记得自己在某天沮丧地
关掉那个程序,从此再不用它。在那之后不久,该公司就关门大吉了。20年后,我见到那家公司的一位早期雇员,问他当年发生了什么
事。他的回答叫我愈发恐惧起来。原来,当时他们赶着推出产品,代码
写得乱七八糟。特性越加越多,代码也越来越烂,最后再也没法管理这
些代码了。是糟糕的代码毁了这家公司。
你是否曾为糟糕的代码所深深困扰?如果你是位有点儿经验的程序
员,定然多次遇到过这类困境。我们有专用来形容这事的词:沼泽
(wading)。我们趟过代码的水域。我们穿过灌木密布、瀑布暗藏的沼
泽地。我们拼命想找到出路,期望有点什么线索能启发我们到底发生了
什么事;但目光所及,只是越来越多死气沉沉的代码。你当然曾为糟糕的代码所困扰过。那么——为什么要写糟糕的代码
呢?
是想快点完成吗?是要赶时间吗?有可能。或许你觉得自己要干好
所需的时间不够;假使花时间清理代码,老板就会大发雷霆。或许你只
是不耐烦再搞这套程序,期望早点结束。或许你看了看自己承诺要做的
其他事,意识到得赶紧弄完手上的东西,好接着做下一件工作。这种事
我们都干过。
我们都曾经瞟一眼自己亲手造成的混乱,决定弃之而不顾,走向新
一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂
程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不
(Later equals never)。1.3 混乱的代价
只要你干过两三年编程,就有可能曾被某人的糟糕的代码绊倒过。
如果你编程不止两三年,也有可能被这种代码拖过后腿。进度延缓的程
度会很严重。有些团队在项目初期进展迅速,但有那么一两年的时间却
慢如蜗行。对代码的每次修改都影响到其他两三处代码。修改无小事。
每次添加或修改代码,都得对那堆扭纹柴了然于心,这样才能往上扔更
多的扭纹柴。这团乱麻越来越大,再也无法理清,最后束手无策。
随着混乱的增加,团队生产力也持续下降,趋向于零。当生产力下
降时,管理层就只有一件事可做了:增加更多人手到项目中,期望提升
生产力。可是新人并不熟悉系统的设计。他们搞不清楚什么样的修改符
合设计意图,什么样的修改违背设计意图。而且,他们以及团队中的其
他人都背负着提升生产力的可怕压力。于是,他们制造更多的混乱,驱
动生产力向零那端不断下降。如图1-1所示。
图1-1 生产力vs.时间
1.3.1 华丽新设计最后,开发团队造反了,他们告诉管理层,再也无法在这令人生厌
的代码基础上做开发。他们要求做全新的设计。管理层不愿意投入资源
完全重启炉灶,但他们也不能否认生产力低得可怕。他们只好同意开发
者的要求,授权去做一套看上去很美的华丽新设计。
于是就组建了一支新军。谁都想加入这个团队,因为它是张白纸。
他们可以重新来过,搞出点真正漂亮的东西来。但只有最优秀、最聪明
的家伙被选中。其余人等则继续维护现有系统。
现在有两支队伍在竞赛了。新团队必须搭建一套新系统,要能实现
旧系统的所有功能。另外,还得跟上对旧系统的持续改动。在新系统功
能足以抗衡旧系统之前,管理层不会替换掉旧系统。
竞赛可能会持续极长时间。我就见过延续了十年之久的。到了完成
的时候,新团队的老成员早已不知去向,而现有成员则要求重新设计一
套新系统,因为这套系统太烂了。
假使你经历过哪怕是一小段我谈到的这种事,那么你一定知道,花
时间保持代码整洁不但有关效率,还有关生存。
1.3.2 态度
你是否遇到过某种严重到要花数个星期来做本来只需数小时即可完
成的事的混乱状况?你是否见过本来只需做一行修改,结果却涉及上百
个模块的情况?这种事太常见了。
怎么会发生这种事?为什么好代码会这么快就变质成糟糕的代码?
理由多得很。我们抱怨需求变化背离了初期设计。我们哀叹进度太紧
张,没法干好活。我们把问题归咎于那些愚蠢的经理、苛求的用户、没
用的营销方式和那些电话消毒剂。不过,亲爱的呆伯特(Dilbert)[2],我们是自作自受[3]。我们太不专业了。
这话可不太中听。怎么会是自作自受呢?难道不关需求的事?难道不关进度的事?难道不关那些蠢经理和没用的营销手段的事?难道他们
就不该负点责吗?
不。经理和营销人员指望从我们这里得到必须的信息,然后才能做
出承诺和保证;即便他们没开口问,我们也不该羞于告知自己的想法。
用户指望我们验证需求是否都在系统中实现了。项目经理指望我们遵守
进度。我们与项目的规划脱不了干系,对失败负有极大的责任;特别是
当失败与糟糕的代码有关时尤为如此!
“且慢!”你说。“不听经理的,我就会被炒鱿鱼。”多半不会。多数
经理想要知道实情,即便他们看起来不喜欢实情。多数经理想要好代
码,即便他们总是痴缠于进度。他们会奋力卫护进度和需求;那是他们
该干的。你则当以同等的热情卫护代码。
再说明白些,假使你是位医生,病人请求你在给他做手术前别洗
手,因为那会花太多时间,你会照办吗[4]?本该是病人说了算;但医生
却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风
险。医生如果按病人说的办,就是一种不专业的态度(更别说是犯罪
了)。
同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做
法。
1.3.3 迷题
程序员面临着一种基础价值谜题。有那么几年经验的开发者都知
道,之前的混乱拖了自己的后腿。但开发者们背负期限的压力,只好制
造混乱。简言之,他们没花时间让自己做得更快!真正的专业人士明
白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会
立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做得快的唯一方
法 ——就是始终尽可能保持代码整洁。1.3.4 整洁代码的艺术
假设你相信混乱的代码是祸首,假设你接受做得快的唯一方法是保
持代码整洁的说法,你一定会自问:“我怎么才能写出整洁的代码?”不
过,如果你不明白整洁对代码有何意义,尝试去写整洁代码就毫无所
益!
坏消息是写整洁代码很像是绘画。多数人都知道一幅画是好还是
坏。但能分辨优劣并不表示懂得绘画。能分辨整洁代码和肮脏代码,也
不意味着会写整洁代码!
写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。
这种“代码感”就是关键所在。有些人生而有之。有些人费点劲才能得
到。它不仅让我们看到代码的优劣,还予我们以借戒规之力化劣为优的
攻略。
缺乏“代码感”的程序员,看混乱是混乱,无处着手。有“代码感”的
程序员能从混乱中看出其他的可能与变化。“代码感”帮助程序员选出最
好的方案,并指导程序员制订修改行动计划,按图索骥。
简言之,编写整洁代码的程序员就像是艺术家,他能用一系列变换
把一块白板变作由优雅代码构成的系统。
1.3.5 什么是整洁代码
有多少程序员,就有多少定义。所以我只询问了一些非常知名且经
验丰富的程序员。
Bjarne Stroustrup,C++语言发明者,C++Programming
Language(中译版《C++程序设计语言》)一书作者。
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐
藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱
来。整洁的代码只做好一件事。
Bjarne用了“优雅”一词。说得好!我MacBook上的词典提供了如下
定义:外表或举止上令人愉悦的优美和雅观;令人愉悦的精致和简单。
注意对“愉悦”一词的强调。Bjarne显然认为整洁的代码读起来令人愉
悦。读这种代码,就像见到手工精美的音乐盒或者设计精良的汽车一
般,让你会心一笑。Bjarne 也提到效率——而且两次提及。这话出自 C++发明者之口,或许并不出奇;不过我认为并非是在单纯追求速度。被浪费掉的运算周
期并不雅观,并不令人愉悦。留意Bjarne 怎么描述那种不雅观的结果。
他用了“引诱”这个词。诚哉斯言。糟糕的代码引发混乱!别人修改糟糕
的代码时,往往会越改越烂。
务实的Dave Thomas和Andy Hunt从另一角度阐述了这种情况。他们
提到破窗理论[5]。窗户破损了的建筑让人觉得似乎无人照管。于是别人
也再不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外
墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。
Bjarne也提到完善错误处理代码。往深处说就是在细节上花心思。
敷衍了事的错误处理代码只是程序员忽视细节的一种表现。此外还有内
存泄漏,还有竞态条件代码。还有前后不一致的命名方式。结果就是凸
现出整洁代码对细节的重视。
Bjarne以“整洁的代码只做好一件事”结束论断。毋庸置疑,软件设
计的许多原则最终都会归结为这句警语。有那么多人发表过类似的言
论。糟糕的代码想做太多事,它意图混乱、目的含混。整洁的代码力求
集中。每个函数、每个类和每个模块都全神贯注于一事,完全不受四周
细节的干扰和污染。
Grady Booch,Object Oriented Analysis and Design with
Applications(中译版《面向对象分析与设计》)一书作者。
整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从
不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。Grady的观点与 Bjarne的观点有类似之处,但他从可读性的角度来
定义。我特别喜欢“整洁的代码如同优美的散文”这种看法。想想你读过
的某本好书。回忆一下,那些文字是如何在脑中形成影像!就像是看了
场电影,对吧?还不止!你还看到那些人物,听到那些声音,体验到那
些喜怒哀乐。
阅读整洁的代码和阅读Lord of the Rings(中译版《指环王》)自然
不同。不过,仍有可类比之处。如同一本好的小说般,整洁的代码应当
明确地展现出要解决问题的张力。它应当将这种张力推至高潮,以某种
显而易见的方案解决问题和张力,使读者发出“啊哈!本当如此!”的感叹。
窃以为Grady所谓“干净利落的抽象”(crisp abstraction),乃是绝妙
的矛盾修辞法。毕竟crisp几乎就是“具体”(concrete)的同义词。我
MacBook上的词典这样定义crisp一词:果断决绝,就事论事,没有犹豫
或不必要的细节。尽管有两种不同的定义,该词还是承载了有力的信
息。代码应当讲述事实,不引人猜测。它只该包含必需之物。读者应当
感受到我们的果断决绝。
“老大”Dave Thomas,OTI公司创始人,Eclipse战略教父。
整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测
试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事
的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽
量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所
有必需信息均可通过代码自身清晰表达。Dave老大在可读性上和Grady持相同观点,但有一个重要的不同之
处。Dave断言,整洁的代码便于其他人加以增补。这看似显而易见,但
亦不可过分强调。毕竟易读的代码和易修改的代码之间还是有区别的。
Dave将整洁系于测试之上!要在十年之前,这会让人大跌眼镜。但
测试驱动开发(Test Driven Development)已在行业中造成了深远影
响,成为基础规程之一。Dave说得对。没有测试的代码不干净。不管它有多优雅,不管有多可读、多易理解,微乎测试,其不洁亦可知也。
Dave 两次提及“尽量少”。显然,他推崇小块的代码。实际上,从
有软件起人们就在反复强调这一点。越小越好。
Dave也提到,代码应在字面上表达其含义。这一观点源自Knuth
的“字面编程”(literate programming)[6]。结论就是应当用人类可读的
方式来写代码。
Michael Feathers,Working Effectively with Legacy Code(中译版
《修改代码的艺术》)一书作者。
我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本
性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有
改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原
点,赞叹某人留给你的代码——全心投入的某人留下的代码。一言以蔽之:在意。这就是本书的题旨所在。或许该加个副标题,如何在意代码。
Michael一针见血。整洁代码就是作者着力照料的代码。有人曾花
时间让它保持简单有序。他们适当地关注到了细节。他们在意过。Ron Jeffries,Extreme Programming Installed(中译版《极限编
程实施》)以及 Extreme Programming Adventures in C(中译版
《C极限编程探险》)作者。
Ron 初入行就在战略空军司令部(Strategic Air Command)编写
Fortran 程序,此后几乎在每种机器上编写过每种语言的代码。他的言论
值得咀嚼。
近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了。
简单代码,依其重要顺序:能通过所有测试;
没有重复代码;
体现系统中的全部设计理念;
包括尽量少的实体,比如类、方法、函数等。
在以上诸项中,我最在意代码重复。如果同一段代码反复出现,就
表示某种想法未在代码中得到良好的体现。我尽力去找出到底那是什么,然后再尽力更清晰地表达出来。
在我看来,有意义的命名是体现表达力的一种方式,我往往会修改
好几次才会定下名字来。借助Eclipse这样的现代编码工具,重命名代价
极低,所以我无所顾忌。然而,表达力还不只体现在命名上。我也会检
查对象或方法是否想做的事太多。如果对象功能太多,最好是切分为两
个或多个对象。如果方法功能太多,我总是使用抽取手段(Extract
Method)重构之,从而得到一个能较为清晰地说明自身功能的方法,以
及另外数个说明如何实现这些功能的方法。
消除重复和提高表达力让我在整洁代码方面获益良多,只要铭记这
两点,改进脏代码时就会大有不同。不过,我时常关注的另一规则就不
太好解释了。
这么多年下来,我发现所有程序都由极为相似的元素构成。例
如“在集合中查找某物”。不管是雇员记录数据库还是名-值对哈希表,或者某类条目的数组,我们都会发现自己想要从集合中找到某一特定条
目。一旦出现这种情况,我通常会把实现手段封装到更抽象的方法或类
中。这样做好处多多。
可以先用某种简单的手段,比如哈希表来实现这一功能,由于对搜
索功能的引用指向了我那个小小的抽象,就能随需应变,修改实现手
段。这样就既能快速前进,又能为未来的修改预留余地。
另外,该集合抽象常常提醒我留意“真正”在发生的事,避免随意实
现集合行为,因为我真正需要的不过是某种简单的查找手段。
减少重复代码,提高表达力,提早构建简单抽象。这就是我写整洁
代码的方法。
Ron 以寥寥数段文字概括了本书的全部内容。不要重复代码,只做
一件事,表达力,小规模抽象。该有的都有了。
Ward Cunningham,Wiki发明者,eXtreme Programming (极限
编程)的创始人之一,Smalltalk语言和面向对象的思想领袖。所有在意代码者的教父。
如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让
编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代
码。
这种说法很 Ward。它教你听了之后就点头,然后继续听下去。如
此在理,如此浅显,绝不故作高深。你大概以为此言深合己意吧。再走近点看看。
“……深合己意”。你最近一次看到深合己意的模块是什么时候?模
块多半都繁复难解吧?难道没有触犯规则吗?你不是也曾挣扎着想抓住
些从整个系统中散落而出的线索,编织进你在读的那个模块吗?你最近
一次读到某段代码、并且如同对 Ward的说法点头一般对这段代码点
头,是什么时候的事了?
Ward期望你不会为整洁代码所震惊。你无需花太多力气。那代码
就是深合你意。它明确、简单、有力。每个模块都为下一个模块做好准
备。每个模块都告诉你下一个模块会是怎样的。整洁的程序好到你根本
不会注意到它。设计者把它做得像一切其他设计般简单。
那 Ward 有关“美”的说法又如何呢?我们都曾面临语言不是为要解
决的问题所设计的困境。但 Ward的说法又把球踢回我们这边。他说,漂亮的代码让编程语言像是专为解决那个问题而存在!所以,让语言变
得简单的责任就在我们身上了!当心,语言是冥顽不化的!是程序员让
语言显得简单。1.4 思想流派
我(鲍勃大叔)又是怎么想的呢?在我眼中整洁代码是什么样的?
本书将以详细到吓死人的程度告诉你,我和我的同道对整洁代码的看
法。我们会告诉你关于整洁变量名的想法,关于整洁函数的想法,关于
整洁类的想法,如此等等。我们视这些观点为当然,且不为其逆耳而致
歉。对我们而言,在职业生涯的这个阶段,这些观点确属当然,也是我
们整洁代码派的圭旨。武术家从不认同所谓最好的武术,也不认同所谓绝招。武术大师们
常常创建自己的流派,聚徒而授。因此我们才看到格雷西家族在巴西开
创并传授的格雷西柔术(Gracie Jiu Jistu),看到奥山龙峰(Okuyama
Ryuho)在东京开创并传授的八光流柔术(Hakkoryu Jiu Jistu),看到李
小龙(Bruce Lee)在美国开创并传授的截拳道(Jeet Kune Do)。
弟子们沉浸于创始人的授业。他们全心师从某位师傅,排斥其他师
傅。弟子有所成就后,可以转投另一位师傅,扩展自己的知识与技能。有些弟子最终百炼成钢,创出新招数,开宗立派。
任何门派都并非绝对正确。不过,身处某一门派时,我们总以其所
传之技为善。归根结底,练习八光流柔术或截拳道,自有其善法,但这
并不能否定其他门派所授之法。
可以把本书看作是对象导师(Object Mentor)[7]整洁代码派的说
明。里面要传授的就是我们勤操己艺的方法。如果你遵从这些教诲,你
就会如我们一般乐受其益,你将学会如何编写整洁而专业的代码。但无
论如何也别错以为我们是“正确的”。其他门派和师傅和我们一样专业。
你有必要也向他们学习。
实际上,书中很多建议都存在争议。或许你并不完全同意这些建
议。你可能会强烈反对其中一些建议。这样挺好的。我们不能要求做最
终权威。另外一方面,书中列出的建议,乃是我们长久苦思、从数十年
的从业经验和无数尝试与错误中得来。无论你同意与否,如果你没看到
或是不尊敬我们的观点,就真该自己害臊。1.5 我们是作者
Javadoc中的@author字段告诉我们自己是什么人。我们是作者。作
者都有读者。实际上,作者有责任与读者做良好沟通。下次你写代码的
时候,记得自己是作者,要为评判你工作的读者写代码。
你或许会问:代码真正“读”的成分有多少呢?难道力量主要不是用
在“写”上吗?
你是否玩过“编辑器回放”?20世纪80、90年代,Emac之类编辑器记
录每次击键动作。你可以在一小时工作之后,回放击键过程,就像是看
一部高速电影。我这么做过,结果很有趣。
回放过程显示,多数时间都是在滚动屏幕、浏览其他模块!
鲍勃进入模块。
他向下滚动到要修改的函数。
他停下来考虑可以做什么。
哦,他滚动到模块顶端,检查变量初始化。
现在他回到修改处,开始键入。
喔,他删掉了键入的内容。
他重新键入。
他又删除了!
他键入了一半什么东西,又删除掉。
他滚动到调用要修改函数的另一函数,看看是怎么调用的。
他回到修改处,重新键入刚才删掉的代码。
他停下来。
他再一次删掉代码!他打开另一个窗口,查看别的子类。那是个复载函数吗?……
你该明白了。读与写花费时间的比例超过10:1。写新代码时,我们
一直在读旧代码。
既然比例如此之高,我们就想让读的过程变得轻松,即便那会使得
编写过程更难。没可能光写不读,所以使之易读实际也使之易写。
这事概无例外。不读周边代码的话就没法写代码。编写代码的难
度,取决于读周边代码的难度。要想干得快,要想早点做完,要想轻松
写代码,先让代码易读吧。1.6 童子军军规
光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随
时间流逝而腐坏。我们应当更积极地阻止腐坏的发生。
借用美国童子军一条简单的军规,应用到我们的专业领域:
让营地比你来时更干净。[8]
如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清
理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过
长的函数,消除一点点重复代码,清理一个嵌套if语句。
你想要为一个代码随时间流逝而越变越好的项目工作吗?你还能相
信有其他更专业的做法吗?难道持续改进不是专业性的内在组成部分
吗?1.7 前传与原则
从许多角度看,本书都是我 2002 年写那本 Agile Software
Development:Principles,Patterns,and Practices(中译版《敏捷软件开
发:原则、模式与实践》,简称PPP)的“前传”。PPP关注面向对象设
计的原则,以及专业开发者采用的许多实践方法。假如你没读过 PPP,你会发现它像这本书的延续。如果你读过,会发现那本书的主张在代码
层面于本书中回响。
在本书中,你会发现对不同设计原则的引用,包括单一权责原则
(Single Responsibility Principle,SRP)、开放闭合原则(Open Closed
Principle,OCP)和依赖倒置原则(Dependency Inversion Principle,DIP)等。1.8 小结
艺术书并不保证你读过之后能成为艺术家,只能告诉你其他艺术家
用过的工具、技术和思维过程。本书同样也不担保让你成为好程序员。
它不担保能给你“代码感”。它所能做的,只是展示好程序员的思维过
程,还有他们使用的技巧、技术和工具。
和艺术书一样,本书也充满了细节。代码会很多。你会看到好代
码,也会看到糟糕的代码。你会看到糟糕的代码如何转化为好代码。你
会看到启发、规条和技巧的列表。你会看到一个又一个例子。但最终结
果取决于你自己。
还记得那个关于小提琴家在去表演的路上迷路的老笑话吗?他在街
角拦住一位长者,问他怎么才能去卡耐基音乐厅(Carnegie Hall)。长
者看了看小提琴家,又看了看他手中的琴,说道:“你还得练,孩子,还得练!”1.9 文献
[Beck07]:Implementation Patterns,Kent Beck,Addison-Wesley,2007.
[Knuth92]:Literate Programming, Donald E. Knuth, Center for the
Study of Language and Information, Leland Stanford Junior University,1992.
[1].原注:[Beck07]。
[2].译注:著名IT讽刺漫画。
[3].译注:原文为But the fault, dear Dilbert, is not in our stars, but in
ourselves.脱胎自莎士比亚戏剧《裘力斯·凯撒》第一幕第二场凯些斯的
台词The fault, dear Brutus, is not in our stars, but in ourselves, that we are
underlings.(若我们受人所制,亲爱的勃鲁托斯,那错也在我们身上,不能怪罪命运。)
[4].原注:1847年Ignaz Semmelweis(伊纳兹·塞麦尔维斯)提出医生应
洗手的建议时,遭到了反对,人们认为医生太忙,接诊时无暇洗手。
[5].原注:http:www.pragmaticprogrammer.combooksellers2004-
12.html。
[6].原注:[Knuth92]。
[7].译注:本书主要作者Robert C.Martin开办的技术咨询和培训公司。
[8].原注:摘自Robert Stephenson Smyth Baden-Powell(英国人,童子军
创始者)对童子军的遗言:“努力,让世界比你来时干净些……”第2章 有意义的命名
Tim Ottinger2.1 介绍
软件中随处可见命名。我们给变量、函数、参数、类和封包命名。
我们给源代码及源代码所在目录命名。我们给jar文件、war文件和ear文
件命名。我们命名、命名,不断命名。既然有这么多命名要做,不妨做
好它。下文列出了取个好名字的几条简单规则。2.2 名副其实
名副其实说起来简单。我们想要强调,这事很严肃。选个好名字要
花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好
的名称,就换掉旧的。这么做,读你代码的人(包括你自己)都会更开
心。
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉
你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来
补充,那就不算是名副其实。
int d; 消逝的时间,以日计
名称d什么也没说明。它没有引起对时间消逝的感觉,更别说以日
计了。我们应该选择指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
选择体现本意的名称能让人更容易理解和修改代码。下列代码的目
的何在?
public List getThem {
List list1 = new ArrayList;
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;}
为什么难以说明上列代码要做什么事?里面并没有复杂的表达式。
空格和缩进中规中矩。只用到三个变量和两个常量。甚至没有涉及任何
其他类或多态方法,只是(或者看起来是)一个数组的列表而已。
问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代
码中未被明确体现的程度。上列代码要求我们了解类似以下问题的答
案:
(1)theList中是什么类型的东西?
(2)theList零下标条目的意义是什么?
(3)值4的意义是什么?
(4)我怎么使用返回的列表?
问题的答案没体现在代码段中,可那就是它们该在的地方。比方
说,我们在开发一种扫雷游戏,我们发现,盘面是名为theList的单元格
列表,那就将其名称改为gameBoard。
盘面上每个单元格都用一个简单数组表示。我们还发现,零下标条
目是一种状态值,而该种状态值为4表示“已标记”。只要改为有意义的
名称,代码就会得到相当程度的改进:
public List getFlaggedCells {
List flaggedCells = new ArrayList;
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
注意,代码的简洁性并未被触及。运算符和常量的数量全然保持不
变,嵌套数量也全然保持不变。但代码变得明确多了。
还可以更进一步,不用 int 数组表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为isFlagged),从而掩盖住那个魔术
数[1]。于是得到函数的新版本:
public List getFlaggedCells {
List flaggedCells = new ArrayList;
for (Cell cell : gameBoard)
if (cell.isFlagged)
flaggedCells.add(cell);
return flaggedCells;
}
只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名
称的力量。2.3 避免误导
程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本
意相悖的词。例如,hp、aix和sco都不该用做变量名,因为它们都是
UNIX平台或类UNIX平台的专有名称。即便你是在编写三角计算程序,hp看起来是个不错的缩写[2],但那也可能会提供错误信息。
别用accountList来指称一组账号,除非它真的是List类型。List一词
对程序员有特殊意义。如果包纳账号的容器并非真是个List,就会引起
错误的判断[3]。所以,用accountGroup或bunchOfAccounts,甚至直接用
accounts都会好一些。
提防使用不同之处较小的名称。想区分模块中某处的
XYZControllerFor EfficientHandlingOfStrings和另一处的
XYZControllerForEfficientStorageOfStrings,会花多长时间呢?这两个词
外形实在太相似了。
以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误
导。我们很享受现代Java编程环境的自动代码完成特性。键入某个名称
的前几个字母,按一下某个热键组合(如果有的话),就能得到一列该
名称的可能形式。假如相似的名称依字母顺序放在一起,且差异很明
显,那就会相当有助益,因为程序员多半会压根不看你的详细注释,甚
至不看该类的方法列表就直接看名字挑一个对象。
误导性名称真正可怕的例子,是用小写字母l和大写字母O作为变量
名,尤其是在组合使用的时候。当然,问题在于它们看起来完全像是常
量“壹”和“零”。
int a = l;if (O == l)
a = O1;
else
l = 01;
读者可能会认为这纯属虚构,但我们确曾见过充斥这类玩意的代
码。有一次,代码作者建议用不同字体写变量名,好显得更清楚些,不
过这种方案得要通过口头和书面传递给未来所有的开发者才行。后来,只是做了简单的重命名操作,就解决了问题,而且也没搞出别的事。2.4 做有意义的区分
如果程序员只是为满足编译器或解释器的需要而写代码,就会制造
麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会
随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现
在更正拼写错误后导致编译器出错的情况。[4]
光是添加数字系列或是废话远远不够,即便这足以让编译器满意。
如果名称必须相异,那其意思也应该不同才对。
以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的
名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线
索。试看:
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];}
}
如果参数名改为source和destination,这个函数就会像样许多。
废话是另一种没意义的区分。假设你有一个 Product 类。如果还有
一个 ProductInfo 或ProductData类,那它们的名称虽然不同,意思却无
区别。Info和Data就像a、an和the一样,是意义含混的废话。
注意,只要体现出有意义的区分,使用 a和the 这样的前缀就没错。
例如,你可能把 a用在域内变量,而把the用于函数参数[5]。但如果你已
经有一个名为zork的变量,又想调用一个名为theZork的变量,麻烦就来
了。
废话都是冗余。Variable一词永远不应当出现在变量名中。Table一
词永远不应当出现在表名中。NameString会比Name好吗?难道Name会
是一个浮点数不成?如果是这样,就触犯了关于误导的规则。设想有个
名为Customer的类,还有一个名为CustomerObject的类。区别何在呢?
哪一个是表示客户历史支付情况的最佳途径?
有个应用反映了这种状况。为当事者讳,我们改了一下,不过犯错
的代码的确就是这个样子:
getActiveAccount;
getActiveAccounts;
getActiveAccountInfo;
程序员怎么能知道该调用哪个函数呢?
如果缺少明确约定,变量 moneyAmount 就与 money 没区别,customerInfo 与 customer没区别,accountData与account没区别,theMessage也与message没区别。要区分名称,就要以读者能鉴别不同之
处的方式来区分。2.5 使用读得出来的名称
人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理
单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来
处理言语,若不善加利用,实在是种耻辱。
如果名称读不出来,讨论的时候就会像个傻鸟。“哎,这儿,鼻涕
阿三喜摁踢(bee cee arr three cee enn tee)[6]上头,有个皮挨死极翘
(pee ess zee kyew)[7]整数,看见没?”这不是小事,因为编程本就是
一种社会活动。
有家公司,程序里面写了个 genymdhms(生成日期,年、月、日、时、分、秒),他们一般读作“gen why emm dee aich emm ess”[8]。我有
个见字照读的恶习,于是开口就念“gen-yah-mudda-hims”。后来好些设
计师和分析师都有样学样,听起来傻乎乎的。我们知道典故,所以会觉
得很搞笑。搞笑归搞笑,实际是在强忍糟糕的命名。在给新开发者解释
变量的意义时,他们总是读出傻乎乎的自造词,而非恰当的英语词。比
较
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = 102;
...
};
和
class Customer {private Date generationTimestamp;
private Date modificationTimestamp;;
private final String recordId = 102;
...
};
现在读起来就像人话了:“喂,Mikey,看看这条记录!生成时间戳
(generation timestamp) [9]被设置为明天了!不能这样吧?”2.6 使用可搜索的名称
单字母名称和数字常量有个问题,就是很难在一大篇文字中找出
来。
找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦
了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图
而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会
逃过搜索,从而造成错误。
同样,e也不是个便于搜索的好变量名。它是英文中最常用的字
母,在每个程序、每段代码中都有可能出现。由此而见,长名称胜于短
名称,搜得到的名称胜于用自造编码代写就的名称。
窃以为单字母名称仅用于短方法中的本地变量。名称长短应与其作
用域大小相对应[N5]。若变量或常量可能在代码中多处使用,则应赋其
以便于搜索的名称。再比较
for (int j=0; j<34; j++) {
s += (t[j]4)5;
}
和
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] realDaysPerIdealDay;
int realTaskWeeks = (realdays WORK_DAYS_PER_WEEK);sum += realTaskWeeks;
}
注意,上面代码中的sum并非特别有用的名称,不过它至少搜得
到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多,而列表中也只剩下了
体现作者意图的名称。2.7 避免使用编码
编码已经太多,无谓再自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之
外(那算是正常的),还要再搞懂另一种编码“语言”。这对于解决问题
而言,纯属多余的负担。带编码的名称通常也不便发音,容易打错。
2.7.1匈牙利语标记法
在往昔名称长短很要命的时代,我们毫无必要地破坏了不编码的规
矩,如今后悔不迭。Fortran 语言要求首字母体现出类型,导致了编码的
产生。BASIC 早期版本只允许使用一个字母再加上一位数字。匈牙利语
标记法(Hungarian Notation,HN)将这种态势愈演愈烈。
在Windows的C语言API的时代,HN相当重要,那时所有名称要么
是个整数句柄,要么是个长指针或者void指针,要不然就是string的几种
实现(有不同的用途和属性)之一。那时候编译器并不做类型检查,程
序员需要匈牙利语标记法来帮助自己记住类型。
现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类
型。而且,人们趋向于使用更小的类、更短的方法,好让每个变量的定
义都在视野范围之内。
Java程序员不需要类型编码。对象是强类型的,代码编辑环境已经
先进到在编译开始前就侦测到类型错误的程度!所以,如今HN和其他
类型编码形式都纯属多余。它们增加了修改变量、函数或类的名称或类
型的难度。它们增加了阅读代码的难度。它们制造了让编码系统误导读
者的可能性。PhoneNumber phoneString;
类型变化时,名称并不变化!
2.7.2 成员前缀
也不必用 m_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。你应当使用某种可以高亮或用颜色标出成员的
编辑环境。
public class Part {
private String m_dsc; The textual description
void setName(String name) {
m_dsc = name;
}
}--------------------------------------------------------------------------------------
public class Part {
String description;
void setDescription(String description) {
this.description = description;
}
}
此外,人们会很快学会无视前缀(或后缀),只看到名称中有意义
的部分。代码读得越多,眼中就越没有前缀。最终,前缀变作了不入法
眼的废料,变作了旧代码的标志物。
2.7.3 接口和实现
有时也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是个接口,要用具体类来实
现。你怎么来命名工厂和具体类呢?IShapeFactory和ShapeFactory吗?
我喜欢不加修饰的接口。前导字母I被滥用到了说好听点是干扰,说难
听点根本就是废话的程度。我不想让用户知道我给他们的是接口。我就
想让他们知道那是个ShapeFactory。如果接口和实现必须选一个来编码
的话,我宁肯选择实现。ShapeFactoryImp,甚至是丑陋的
CShapeFactory,都比对接口名称编码来得好。2.8 避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题
经常出现在选择是使用问题领域术语还是解决方案领域术语时。
单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循
环计数器自然有可能被命名为i或j或k。(但千万别用字母l!)这是因
为传统上惯用单字母名称做循环计数器。然而,在多数其他情况下,单
字母名称不是个好选择;读者必须在脑中将它映射为真实概念。仅仅是
因为有了a和b,就要取名为c,实在并非像样的理由。
程序员通常都是聪明人。聪明人有时会借脑筋急转弯炫耀其聪明。
总而言之,假使你记得r代表不包含主机名和图式(scheme)的小写字
母版url的话,那你真是太聪明了。
聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确
是王道。专业程序员善用其能,编写其他人能理解的代码。2.9 类名
类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser。避免使用Manager、Processor、Data或Info这样
的类名。类名不应当是动词。2.10 方法名
方法名应当是动词或动词短语,如postPayment、deletePage或
save。属性访问器、修改器和断言应该根据其值命名,并依Javabean标
准[10]加上get、set和is前缀。
string name = employee.getName;
customer.setName(mike);
if (paycheck.isPosted)...
重载构造器时,使用描述了参数的静态工厂方法名。例如,Complex fulcrumPoint = Complex.FromRealNumber(23.0);
通常好于
Complex fulcrumPoint = new Complex(23.0);
可以考虑将相应的构造器设置为private,强制使用这种命名手段。2.11 别扮可爱
如果名称太耍宝,那就只有同作者一般有幽默感的人才能记得住,而且还是在他们记得那个笑话的时候才行。谁会知道名为
HolyHandGrenade[11]的函数是用来做什么的呢?没错,这名字挺伶
俐,不过DeleteItems[12]或许是更好的名称。宁可明确,毋为好玩。
扮可爱的做法在代码中经常体现为使用俗话或俚语。例如,别用
whack( )[13]来表示kill( )。别用eatMyShorts( )[14]这类与文化紧密相关的
笑话来表示abort( )。
言到意到。意到言到。2.12 每个概念对应一个词
给每个抽象概念选一个词,并且一以贯之。例如,使用fetch、retrieve和get来给在多个类中的同种方法命名。你怎么记得住哪个类中
是哪个方法呢?很悲哀,你总得记住编写库或类的公司、机构或个人,才能想得起来用的是哪个术语。否则,就得耗费大把时间浏览各个文件
头及前面的代码。
Eclipse和IntelliJ之类现代编程环境提供了与环境相关的线索,比如
某个对象能调用的方法列表。不过要注意,列表中通常不会给出你为函
数名和参数列表编写的注释。如果参数名称来自函数声明,你就太幸运
了。函数名称应当独一无二,而且要保持一致,这样你才能不借助多余
的浏览就找到正确的方法。
同样,在同一堆代码中有controller,又有manager,还有driver,就
会令人困惑。DeviceManager和Protocol-Controller 之间有何根本区别?
为什么不全用 controllers 或 managers?他们都是Drivers吗?这种名称,让人觉得这两个对象是不同类型的,也分属不同的类。
对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降
福音。2.13 别用双关语
避免将同一单词用于不同目的。同一术语用于不同概念,基本上就
是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有
add方法。只要这些add方法的参数列表和返回值在语义上等价,就一切
顺利。
但是,可能会有人决定为“保持一致”而使用add这个词来命名,即
便并非真的想表示这种意思。比如,在多个类中都有add方法,该方法
通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一
个方法,把单个参数放到群集(collection)中。该把这个方法叫做 add
吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应
该用 insert或append之类词来命名才对。把该方法命名为add,就是双关
语了。
代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能
一目尽览,而不必殚精竭虑地研究。我们想要那种大众化的作者尽责写
清楚的平装书模式;我们不想要那种学者挖地三尺才能明白个中意义的
学院派模式。2.14 使用解决方案领域名称
记住,只有程序员才会读你的代码。所以,尽管用那些计算机科学
(Computer Science,CS)术语、算法名、模式名、数学术语吧。依据
问题所涉领域来命名可不算是聪明的做法,因为不该让协作者老是跑去
问客户每个名称的含义,其实他们早该通过另一名称了解这个概念了。
对于熟悉访问者(VISITOR)模式的程序来说,名称
AccountVisitor 富有意义。哪个程序员会不知道 JobQueue的意思呢?程
序员要做太多技术性工作。给这些事取个技术性的名称,通常是最靠谱
的做法。2.15 使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉
问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域
专家了。
优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题
领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域
的名称。2.16 添加有意义的语境
很少有名称是能自我说明的——多数都不能。反之,你需要用有良
好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这
么做,给名称添加前缀就是最后一招了。
设想你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当它们搁一块儿的时候,很明确是构成了一个地
址。不过,假使只是在某个方法中看见孤零零一个state变量呢?你会理
所当然推断那是某个地址的一部分吗?
可以添加前缀addrFirstName、addrLastName、addrState等,以此提
供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为Address的类。这样,即便是编译器也会知道这
些变量隶属某个更大的概念了。
看看代码清单2-1中的方法。以下变量是否需要更有意义的语境
呢?函数名仅给出了部分语境;算法提供了剩下的部分。遍览函数后,你会知道number、verb和pluralModifier这三个变量是“测估”信息的一部
分。不幸的是这语境得靠读者推断出来。第一眼看到这个方法时,这些
变量的含义完全不清楚。
代码清单2-1 语境不明确的变量
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {number = no;
verb = are;
pluralModifier = s;
} else if (count == 1) {
number = 1;
verb = is;
pluralModifier = ;
} else {
number = Integer.toString(count);
verb = are;
pluralModifier = s;
}
String guessMessage = String.format(
There %s %s %s%s, verb, number, candidate, pluralModifier);
print(guessMessage);
}
上列函数有点儿过长,变量的使用贯穿始终。要分解这个函数,需
要创建一个名为GuessStatisticsMessage的类,把三个变量做成该类的成
员字段。这样它们就在定义上变作了GuessStatisticsMessage的一部分。
语境的增强也让算法能够通过分解为更小的函数而变得更为干净利落。
(如代码清单2-2所示。)
代码清单2-2 有语境的变量
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
There %s %s %s%s,verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters;
} else if (count == 1) {
thereIsOneLetter;
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = are;
pluralModifier = s;
}
private void thereIsOneLetter {
number = 1;
verb = is;
pluralModifier = ;
}
private void thereAreNoLetters {
number = no;verb = are;
pluralModifier = s;
}
}2.17 不要添加没用的语境
设若有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在
其中给每个类添加GSD前缀就不是什么好点子。说白了,你是在和自己
在用的工具过不去。输入G,按下自动完成键,结果会得到系统中全部
类的列表,列表恨不得有一英里那么长。这样做聪明吗?为什么要搞得
IDE没法帮助你?
再比如,你在GSD应用程序中的记账模块创建了一个表示邮件地址
的类,然后给该类命名为GSDAccountAddress。稍后,你的客户联络应
用中需要用到邮件地址,你会用GSDAccountAddress吗?这名字听起来
没问题吗?在这17个字母里面,有10个字母纯属多余和与当前语境毫无
关联。
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语
境。
对于Address类的实体来说,accountAddress和customerAddress都是
不错的名称,不过用在类名上就不太好了。Address是个好类名。如果
需要与MAC地址、端口地址和Web地址相区别,我会考虑使用
PostalAddress、MAC和URI。这样的名称更为精确,而精确正是命名的
要点。2.18 最后的话
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。与
其说这是一种技术、商业或管理问题,还不如说是一种教学问题。其结
果是,这个领域内的许多人都没能学会做得很好。
我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果
名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方
法名。我们使用现代工具对付这些细节,好让自己集中精力于把代码写
得就像词句篇章、至少像是表和数据结构(词句并非总是呈现数据的最
佳手段)。改名可能会让某人吃惊,就像你做到其他代码改善工作一
样。别让这种事阻碍你的前进步伐。
不妨试试上面这些规则,看你的代码可读性是否有所提升。如果你
是在维护别人写的代码,使用重构工具来解决问题。效果立竿见影,而
且会持续下去。
[1].译注:即表示已标记的4。
[2].译注:即hypotenuse的缩写。
[3].原注:如后文提到的,即便容器就是个List,最好也别在名称中写出
容器类型名。
[4].原注:例如,就因为class已有他用,就给一个变量命名为klass,这
真是可怕的做法。
[5].原注:鲍勃大叔惯于在C++中这样做,但后来放弃了,因为现代IDE
使这种做法变得没必要了。
[6].译注:BCR3CNT的读音。[7].译注:PSZQ的读音。
[8].译注:YMDHMS的读音。
[9].译注:读到generation timestamp时,立刻就能与代码中的
generationTimestamp变量对应上。
[10].原注:http:java.sun.comproductsjavabeansdocsspec.html。
[11].译注:意为“圣手手雷”。
[12].译注:意为“删除条目”。
[13].美俚,劈砍。
[14].美俚,去死吧。第3章 函数在编程的早年岁月,系统由程序和子程序组成。后来,在Fortran和
PL1的年代,系统由程序、子程序和函数组成。如今,只有函数存活下
来。函数是所有程序中的第一组代码。本章将讨论如何写好函数。
请看代码清单3-1。在FitNesse[1]中,很难找到长函数,不过我还是搜寻到一个。它不光长,而且代码也很复杂,有大量字符串、怪异而不
显见的数据类型和API。花3分钟时间,看能读懂多少?
代码清单3-1 HtmlUtil.java(FitNesse 20070619)
public static String testableHtml(
PageData pageData,boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage = pageData.getWikiPage;
StringBuffer buffer = new StringBuffer;
if (pageData.hasAttribute(Test)) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler.getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append(!include -setup .)
.append(pagePathName)
.append(\n);
}
}
WikiPage setup =
PageCrawlerImpl.getInheritedPage(SetUp, wikiPage);
if (setup != null) {WikiPagePath setupPath =
wikiPage.getPageCrawler.getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append(!include -setup .)
.append(setupPathName)
.append(\n);
}
}
buffer.append(pageData.getContent);
if (pageData.hasAttribute(Test)) {
WikiPage teardown =
PageCrawlerImpl.getInheritedPage(TearDown, wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler.getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append(\n)
.append(!include -teardown .)
.append(tearDownPathName)
.append(\n);
}
if (includeSuiteSetup) {
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_TEARDOWN_NAME,wikiPage);if (suiteTeardown != null) {
WikiPagePath pagePath =
suiteTeardown.getPageCrawler.getFullPath
(suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append(!include -teardown .)
.append(pagePathName)
.append(\n);
}
}
}
pageData.setContent(buffer.toString);
return pageData.getHtml;
}
搞懂这个函数了吗?大概没有。有太多事发生,有太多不同层级的
抽象。奇怪的字符串和函数调用,混以双重嵌套、用标识来控制的if语
句等,不一而足。
不过,只要做几个简单的方法抽离和重命名操作,加上一点点重
构,就能在9行代码之内搞掂(如代码清单3-2所示)。用3分钟阅读以
下代码,看你能理解吗?
代码清单3-2 HtmlUtil.java(重构之后)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
boolean isTestPage = pageData.hasAttribute(Test);
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage;StringBuffer newPageContent = new StringBuffer;
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent);
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString);
}
return pageData.getHtml;
}
除非你正在研究 FitNesse,否则就理解不了所有细节。不过,你大
概能明白,该函数包含把一些设置和拆解页放入一个测试页面,再渲染
为HTML的操作。如果你熟悉JUnit[2],或许会想到,该函数归属于某个
基于Web的测试框架。而且,这当然没错。从代码清单3-2中获得信息
很容易,而代码清单3-1则晦涩难明。
是什么让代码清单3-2易于阅读和理解?怎么才能让函数表达其意
图?该给函数赋予哪些属性,好让读者一看就明白函数是属于怎样的程
序?3.1 短小
函数的第一规则是要短小。第二条规则是还要更短小。我无法证明
这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近40年来,我写过各种不同大小的函数。我写过令人憎恶的长达3000行
的厌物,也写过许多100行到300行的函数,我还写过20行到30行的。经
过漫长的试错,经验告诉我,函数就该小。
在20世纪80年代,我们常说函数不该长于一屏。当然,说这话的时
候,VT100屏幕只有24行、80列,而编辑器就得先占去4行空间放菜
单。如今,用上了精致的字体和宽大的显示器,一屏里面可以显示100
行,每行能容纳150个字符。每行都不应该有150个字符那么长。函数也
不该有100行那么长,20行封顶最佳。
函数到底该有多长?1991年,我去Kent Beck位于奥勒冈州
(Oregon)的家中拜访。我们坐到一起写了些代码。他给我看一个叫做
Sparkle(火花闪耀)的有趣的JavaSwing小程序。程序在屏幕上描画电
影Cinderella(《灰姑娘》)中仙女用魔棒造出的那种视觉效果。只要移
动鼠标,光标所在处就会爆发出一团令人欣喜的火花,沿着模拟重力场
划落到窗口底部。肯特给我看代码的时候,我惊讶于其中那些函数尺寸
之小。我看惯了Swing程序中长度数以里计的函数。但这个程序中每个
函数都只有两行、三行或四行长。每个函数都一目了然。每个函数都只
说一件事。而且,每个函数都依序把你带到下一个函数。这就是函数应
该达到的短小程度![3]
函数应该有多短小?通常来说,应该短于代码清单3-2中的函数!
代码清单3-2实在应该缩短成代码清单3-3这个样子。代码清单3-3 HtmlUtil.java(再次重构之后)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml;
}
代码块和缩进
if语句、else语句、while语句等,其中的代码块应该只有一行。该
行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因
为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进
层级不该多于一层或两层。当然,这样的函数易于阅读和理解。3.2 只做一件事
代码清单3-1显然想做好几件事。它创建缓冲区、获取页面、搜索
继承下来的页面、渲染路径、添加神秘的字符串、生成HTML,如此等
等。代码清单3-1手忙脚乱。而代码清单3-3则只做一件简单的事。它将
设置和拆解包纳到测试页面中。
过去30年以来,以下建议以不同形式一再出现:
函数应该做一件事。做好这件事。只做这一件事。
问题在于很难知道那件该做的事是什么。代码清单3-3只做了一件
事,对吧?其实也很容易看作是三件事:
(1)判断是否为测试页面;
(2)如果是,则容纳进设置和分拆步骤;
(3)渲染成HTML。那件事是什么?函数是做了一件事呢,还是做了三件事?注意,这
三个步骤均在该函数名下的同一抽象层上。可以用简洁的TO[4]起头段
落来描述这个函数:
TO RenderPageWithSetupsAndTeardowns, we check to see whether the
page is a test page and if so, we include the setups and teardowns. In either
case we render the page in HTML。
(要RenderPageWithSetupsAndTeardowns,检查页面是否为测试
页,如果是测试页,就容纳进设置和分拆步骤。无论是否测试页,都渲
染成HTML)
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只
做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名
称)拆分为另一抽象层上的一系列步骤。
代码清单3-1明显包括了处于多个不同抽象层级的步骤。显然,它所做的不止一件事。即便是代码清单3-2也有两个抽象层,这已被我们
将其缩短的能力所证明。然而,很难再将代码清单3-3做有意义的缩
短。可以将if语句拆出来做一个名为includeSetupAndTeardonws
IfTestpage的函数,但那只是重新诠释代码,并未改变抽象层级。
所以,要判断函数是否不止做了一件事,还有一个方法,就是看是
否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现[G34]。
函数中的区段
请看代码清单4-7。注意,generatePrimes函数被切分为
declarations、initializations和sieve等区段。这就是函数做事太多的明显
征兆。只做一件事的函数无法被合理地切分为多个区段。3.3 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。一
眼就能看出,代码清单3-1 违反了这条规矩。那里面有 getHtml( )等位于
较高抽象层的概念,也有 String pagePathName =
PathParser.render(pagePath)等位于中间抽象层的概念,还有.append(\n)
等位于相当低的抽象层的概念。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个
表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节
与基础概念混杂,更多的细节就会在函数中纠结起来。
自顶向下读代码:向下规则
我们想要让代码拥有自顶向下的阅读顺序。[5]我们想要让每个函数
后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能偱抽象层级向下阅读了。我把这叫做向下规则。
换一种说法。我们想要这样读程序:程序就像是一系列 TO起头的
段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续
TO起头段落。
To include the setups and teardowns, we include setups, then we include
the test page content, and then we include the teardowns.(要容纳设置和分
拆步骤,就先容纳设置步骤,然后纳入测试页面内容,再纳入分拆步
骤。)
To include the setups, we include the suite setup if this is a suite, then we
include the regular setup.(要容纳设置步骤,如果是套件,就纳入套件
设置步骤,然后再纳入普通设置步骤。)To include the suite setup, we search the parent hierarchy for
the“SuiteSetUp”page and add an include statement with the path of that
page.(要容纳套件设置步骤,先搜索“SuiteSetUp”页面的上级继承关
系,再添加一个包括该页面路径的语句。)
To search the parent. . (要搜索……)
程序员往往很难学会遵循这条规则,写出只停留于一个抽象层级上
的函数。尽管如此,学习这个技巧还是很重要。这是保持函数短小、确
保只做一件事的要诀。让代码读起来像是一系列自顶向下的TO起头段
落是保持抽象层级协调一致的有效技巧。
看看本章末尾的代码清单3-7。它展示了遵循这条原则重构的完整
testableHtml函数。留意每个函数是如何引出下一个函数,如何保持在同
一抽象层上的。3.4 switch语句
写出短小的switch语句很难[6]。即便是只有两种条件的switch语句
也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语
句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不
过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重
复。当然,我们利用多态来实现这一点。
请看代码清单3-4。它呈现了可能依赖于雇员类型的仅仅一种操
作。
代码清单3-4 Payroll.java
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责
原则(Single Responsibility Principle[7], SRP),因为有好几个修改它的
理由。第四,它违反了开放闭合原则(Open Closed Principle[8],OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦
的可能是到处皆有类似结构的函数。例如,可能会有
isPayday(Employee e, Date date),或
deliverPay(Employee e, Money pay),如此等等。它们的结构都有同样的问题。
该问题的解决方案(如代码清单3-5所示)是将switch语句埋到抽象
工厂[9]底下,不让任何人看到。该工厂使用switch语句为Employee的派
生物创建适当的实体,而不同的函数,如calculatePay、isPayday和
deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对
象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍
[G23]。当然也要就事论事,有时我也会部分或全部违反这条规矩。
代码清单3-5 Employee与工厂
public abstract class Employee {
public abstract boolean isPayday;
public abstract Money calculatePay;
public abstract void deliverPay(Money pay);
}-----------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws
InvalidEmployeeType;
}-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws
InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
}3.5 使用描述性的名称
在代码清单3-7中,我把示例函数的名称从testableHtml改为
SetupTeardownIncluder.render。这个名称好得多,因为它较好地描述了
函数做的事。我也给每个私有方法取个同样具有描述性的名称,如
isTestable或includeSetupAndTeardownPages。好名称的价值怎么好评都
不为过。记住沃德原则:“如果每个例程都让你感到深合己意,那就是
整洁代码。”要遵循这一原则,泰半工作都在于为只做一件事的小函数
取个好名字。函数越短小、功能越集中,就越便于取个好名字。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称
好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约
定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个
能说清其功用的名称。
别害怕花时间取名字。你当尝试不同的名称,实测其阅读效果。在
Eclipse或IntelliJ等现代IDE中改名称易如反掌。使用这些IDE测试不同名
称,直至找到最具有描述性的那一个为止。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。
追索好名称,往往导致对代码的改善重构。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词
给函数命名。例如,includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage等。这些名
称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上
述函数序列,你就会自问:“includeTeardownPages、includeSuiteTeardownPages和includeTeardownPage又会如何?”这就是所谓“深合己意”了。3.6 函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函
数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足
够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也
不要这么做。
参数不易对付。它们带有太多概念性。所以我在代码范例中几乎不
加参数。比如,以StringBuffer为例,我们可能不把它作为实体变量,而
是当作参数来传递,那样的话,读者每次看到它都得要翻译一遍。阅读
模块所讲述的故事时,includeSetupPage( )要比
includeSetupPageInto(newPage-Content)易于理解。参数与函数名处在
不同的抽象层级,它要求你了解目前并不特别重要的细节(即那个
StringBuffer)。从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参
数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。如果只有一个参数,也不太困难。有两个参数,问题就麻烦多了。如果参数多于两个,测试覆盖所有可能值的组合简直让人生
畏。
输出参数比输入参数还要难以理解。读函数时,我们惯于认为信息
通过参数输入函数,通过返回值从函数中输出。我们不太期望信息通过
参数输出。所以,输出参数往往让人苦思之后才恍然大悟。
相较于没有参数,只有一个输入参数算是第二好的做法。
SetupTeardownInclude.render (pageData)也相当易于理解。很明显,我
们将渲染pageData对象中的数据。
3.6.1 一元函数的普遍形式
向函数传入单个参数有两种极普遍的理由。你也许会问关于那个参
数的问题,就像在boolean fileExists(MyFile)中那样。也可能是操作该
参数,将其转换为其他什么东西,再输出之。例如,InputStream
fileOpen(MyFile)把String类型的文件名转换为InputStream类型的返回
值。这就是读者看到函数时所期待的东西。你应当选用较能区别这两种
理由的名称,而且总在一致的上下文中使用这两种形式。
还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件
(event)。在这种形式中,有输入参数而无输出参数。程序将函数看作
是一个事件,使用该参数修改系统状态,例如void
passwordAttemptFailedNtimes(int attempts)。小心使用这种形式。应该让
读者很清楚地了解它是个事件。谨慎地选用名称和上下文语境。
尽量避免编写不遵循这些形式的一元函数,例如,void
includeSetupPageInto(StringBuffer pageText)。对于转换,使用输出参数
而非返回值令人迷惑。如果函数要对输入参数进行转换操作,转换结果
就该体现为返回值。实际上,StringBuffer transform(StringBuffer in)要比
void transform(StringBuffer out)强,即便第一种形式只简单地返回输参数也是这样。至少,它遵循了转换的形式。
3.6.2 标识参数
标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。
这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。
如果标识为true将会这样做,标识为false则会那样做!
在代码清单3-7中,我们别无选择,因为调用者已经传入了那个标
识,而我想把重构范围限制在该函数及该函数以下范围之内。方法调用
render(true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到
render(Boolean isSuite),稍许有点帮助,不过仍然不够。应该把该函数
一分为二:reanderForSuite( )和renderForSingleTest( )。
3.6.3 二元函数
有两个参数的函数要比一元函数难懂。例如,writeField(name)比
writeField(outputStream,name)[10]好懂。
尽管两种情况下意义都很清楚,但第一个只要扫一眼就明白,更好
地表达了其意义。第二个就得暂停一下才能明白,除非我们学会忽略第
一个参数。而且最终那也会导致问题,因为我们根本就不该忽略任何代
码。忽略掉的部分就是缺陷藏身之地。
当然,有些时候两个参数正好。例如,Point p = new Point(0,0);就相
当合理。笛卡儿点天生拥有两个参数。如果看到new Point(0),我们会倍
感惊讶。然而,本例中的两个参数却只是单个值的有序组成部分!而
output-Stream和name则既非自然的组合,也不是自然的排序。
即便是如 assertEquals(expected, actual)这样的二元函数也有其问
题。你有多少次会搞错actual和expected的位置呢?这两个参数没有自然
的顺序。expected在前,actual在后,只是一种需要学习的约定罢了。二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小
心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换成一
元函数。例如,可以把 writeField 方法写成outputStream的成员之一,从
而能这样用:outputStream.writeField(name)。或者,也可以把
outputStream写成当前类的成员变量,从而无需再传递它。还可以分离
出类似 FieldWriter的新类,在其构造器中采用outputStream,并且包含
一个write方法。
3.6.4 三元函数
有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问
题都会加倍体现。建议你在写三元函数前一定要想清楚。
例如,设想assertEquals有三个参数:assertEquals(message, expected,actual)。有多少次,你读到 message,错以为它是expected呢?我就常栽
在这个三元函数上。实际上,每次我看到这里,总会绕半天圈子,最后
学会了忽略message参数。
另一方面,这里有个并不那么险恶的三元函数:assertEquals(1.0,amount, .001)。虽然也要费点神,还是值得的。得到“浮点值的等值是相
对而言”的提示总是好的。
3.6.5 参数对象
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参
数应该封装为类了。例如,下面两个声明的差别:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则
并非如此。当一组参数被共同传递,就像上例中的x和y那样,往往就是该有自己名称的某个概念的一部分。
3.6.6 参数列表
有时,我们想要向函数传入数量可变的参数。例如,String.format
方法:
String.format(%s worked %.2f hours., name, hours);
如果可变参数像上例中那样被同等对待,就和类型为List的单个参
数没什么两样。这样一来,String.formate实则是二元函数。下列
String.format的声明也很明显是二元的:
public String format(String format, Object... args)
同理,有可变参数的函数可能是一元、二元甚至三元。超过这个数
量就可能要犯错了。
void monad(Integer... args);
void dyad(String name, Integer... args);
void triad(String name, int count, Integer... args);
3.6.7 动词与关键字
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和
意图。对于一元函数,函数和参数应当形成一种非常良好的动词名词
对形式。例如,write(name)就相当令人认同。不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我
们,“name”是一个“field”。
最后那个例子展示了函数名称的关键字(keyword)形式。使用这
种形式,我们把参数的名称编码成了函数名。例如,assertEqual改成
assertExpectedEqualsActual(expected, actual)可能会好些。这大大减轻了
记忆参数顺序的负担。3.7 无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起
来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它
会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都
是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
以代码清单3-6中看似无伤大雅的函数为例。该函数使用标准算法
来匹配 userName和password。如果匹配成功,返回 true,如果失败则返
回 false。但它会有副作用。你知道问题所在吗?
代码清单3-6 UserValidator.java
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword;
String phrase = cryptographer.decrypt(codedPhrase, password);
if (Valid Password.equals(phrase)) {
Session.initialize;
return true;
}
}
return false;
}}
当然了,副作用就在于对Session.initialize( )的调用。checkPassword
函数,顾名思义,就是用来检查密码的。该名称并未暗示它会初始化该
次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。
这一副作用造出了一次时序性耦合。也就是说,checkPassword只能
在特定时刻调用(换言之,在初始化会话是安全的时候调用)。如果在
不合适的时候调用,会话数据就有可能沉默地丢失。时序性耦合令人迷
惑,特别是当它躲在副作用后面时。如果一定要时序性耦合,就应该在
函数名称中说明。在本例中,可以重命名函数为
checkPasswordAndInitializeSession,虽然那还是违反了“只做一件事”的
规则。
输出参数
参数多数会被自然而然地看作是函数的输入。如果你编过好些年程
序,我担保你一定被用作输出而非输入的参数迷惑过。例如:
appendFooter(s);
这个函数是把s添加到什么东西后面吗?或者它把什么东西添加到
了s后面?s是输入参数还是输出参数?稍许花点时间看看函数签名:
public void appendFooter(StringBuffer report)
事情清楚了,但付出了检查函数声明的代价。你被迫检查函数签
名,就得花上一点时间。应该避免这种中断思路的事。
在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面
向对象语言中对输出参数的大部分需求已经消失了,因为this也有输出
函数的意味在内。换言之,最好是这样调用appendFooter:
report.appendFooter;
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。3.8 分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该
修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混
乱。看看下面的例子:
public boolean set(String attribute, String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个
属性则返回false。这样就导致了以下语句:
if (set(username, unclebob))...
从读者的角度考虑一下吧。这是什么意思呢?它是在问username属
性值是否之前已设置为unclebob吗?或者它是在问username属性值是否
成功设置为unclebob呢?从这行调用很难判断其含义,因为set是动词还
是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形
容词。该语句读起来像是说“如果username属性值之前已被设置为
uncleob”,而不是“设置username属性值为unclebob,看看是否可行,然
后……”。要解决这个问题,可以将 set 函数重命名为
setAndCheckIfExists,但这对提高 if 语句的可读性帮助不大。真正的解
决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists(username)) {
setAttribute(username, unclebob);...
}3.9 使用异常替代返回错误码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓
励了在if语句判断中把指令当作表达式使用。
if (deletePage(page) == E_OK)
这不会引起动词形容词混淆,但却导致更深层次的嵌套结构。当
返回错误码时,就是在要求调用者立刻处理错误。
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey== E_OK){
logger.log(page deleted);
} else {
logger.log(configKey not deleted);
}
} else {
logger.log(deleteReference from registry failed);
}
} else {
logger.log(delete failed);
return E_ERROR;
}
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主
路径代码中分离出来,得到简化:
try {deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey);
}
catch (Exception e) {
logger.log(e.getMessage);
}
3.9.1 抽离TryCatch代码块
Trycatch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正
常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外
形成函数。
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception
{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey);
}private void logError(Exception e) {
logger.log(e.getMessage);
}
在上例中,delete函数只与错误处理有关。很容易理解然后就忽略
掉。deletePageAndAllReference函数只与完全删除一个page有关。错误
处理可以忽略掉。有了这样美妙的区隔,代码就更易于理解和修改了。
3.9.2 错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函
数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中
存在,它就该是这个函数的第一个单词,而且在catchfinally代码块后面
也不该有其他内容。
3.9.3 Error.java依赖磁铁
返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。
public enum Error {
OK,INVALID,NO_SUCH,LOCKED,OUT_OF_RESOURCES,WAITING_FOR_EVENT;
}
这样的类就是一块依赖磁铁(dependency magnet);其他许多类都
得导入和使用它。当Error枚举修改时,所有这些其他的类都需要重新编
译和部署。[11]这对Error类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧
的错误码,而不添加新的。
使用异常替代错误码,新异常就可以从异常类派生出来,无需重新
编译或重新部署[12]。3.10 别重复自己
[13]
回头仔细看看代码清单3-1,你会注意到,有个算法在SetUp、SuiteSetUp、TearDown和SuiteTearDown中总共被重复了 4 次。识别重复
不太容易,因为这 4 次重复与其他代码混在一起,而且也不完全一样。
这样的重复还是会导致问题,因为代码因此而臃肿,且当算法改变时需
要修改4处地方。而且也会增加4次放过错误的可能性。
使用代码清单3-7中的include方法修正了这些重复。再读一遍那段
代码,你会注意到,整个模块的可读性因为重复的消除而得到了提升。
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。例如,全部考德(Codd)[14]数据库范式都是为
消灭数据重复而服务。再想想看,面向对象编程是如何将代码集中到基
类,从而避免了冗余。面向方面编程(Aspect Oriented
Programming)、面向组件编程(Component Oriented Programming)多
少也都是消除重复的一种策略。看来,自子程序发明以来,软件开发领
域的所有创新都是在不断尝试从源代码中消灭重复。3.11 结构化编程
有些程序员遵循Edsger Dijkstra的结构化编程规则[15]。Dijkstra认
为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵
循这些规则,意味着在每个函数中只该有一个return语句,循环中不能
有break或continue语句,而且永永远远不能有任何goto语句。
我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益
不大。只有在大函数中,这些规则才会有明显的好处。
所以,只要函数保持短小,偶尔出现的return、break或continue语句
没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto只
在大函数中才有道理,所以应该尽量避免使用。3.12 如何写出这样的函数
写代码和写别的东西很像。在写论文或文章时,你先想什么就写什
么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心
目中的样子。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过
长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一
套单元测试,覆盖每行丑陋的代码。
然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和
重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数。
我并不从一开始就按照规则写函数。我想没人做得到。3.13 小结
每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设
计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到
那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可
怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言
设计的艺术。
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使
用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用
来讲那个故事。那种领域特定语言的一个部分,就是描述在系统中发生
的各种行为的函数层级。在一种狡猾的递归操作中,这些行为使用它们
定义的与领域紧密相关的语言讲述自己那个小故事。
本章所讲述的是有关编写良好函数的机制。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。不过永远别忘记,真
正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到
一起,形成一种精确而清晰的语言,帮助你讲故事。3.14 SetupTeardownIncluder程序
代码清单3-7 SetupTeardownIncluder.java
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.;
public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite)
throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pa ...... | | |
封面
扉页
版权
内容提要
序
关于封面
代码猴子与童子军军规
前言
第1章 整洁代码
1.1 要有代码
1.2 糟糕的代码
1.3 混乱的代价
1.3.1 华丽新设计
1.3.2 态度
1.3.3 迷题
1.3.4 整洁代码的艺术
1.3.5 什么是整洁代码
1.4 思想流派
1.5 我们是作者
1.6 童子军军规
1.7 前传与原则
1.8 小结
1.9 文献
第2章 有意义的命名
2.1 介绍
2.2 名副其实2.3 避免误导
2.4 做有意义的区分
2.5 使用读得出来的名称
2.6 使用可搜索的名称
2.7 避免使用编码
2.7.1匈牙利语标记法
2.7.2 成员前缀
2.7.3 接口和实现
2.8 避免思维映射
2.9 类名
2.10 方法名
2.11 别扮可爱
2.12 每个概念对应一个词
2.13 别用双关语
2.14 使用解决方案领域名称
2.15 使用源自所涉问题领域的名称
2.16 添加有意义的语境
2.17 不要添加没用的语境
2.18 最后的话
第3章 函数
3.1 短小
3.2 只做一件事
3.3 每个函数一个抽象层级
3.4 switch语句
3.5 使用描述性的名称
3.6 函数参数
3.6.1 一元函数的普遍形式
3.6.2 标识参数3.6.3 二元函数
3.6.4 三元函数
3.6.5 参数对象
3.6.6 参数列表
3.6.7 动词与关键字
3.7 无副作用
3.8 分隔指令与询问
3.9 使用异常替代返回错误码
3.9.1 抽离TryCatch代码块
3.9.2 错误处理就是一件事
3.9.3 Error.java依赖磁铁
3.10 别重复自己
3.11 结构化编程
3.12 如何写出这样的函数
3.13 小结
3.14 SetupTeardownIncluder程序
3.15 文献
第4章 注释
4.1 注释不能美化糟糕的代码
4.2 用代码来阐述
4.3 好注释
4.3.1 法律信息
4.3.2 提供信息的注释
4.3.3 对意图的解释
4.3.4 阐释
4.3.5 警示
4.3.6 TODO注释
4.3.7 放大4.3.8 公共API中的Javadoc
4.4 坏注释
4.4.1 喃喃自语
4.4.2 多余的注释
4.4.3 误导性注释
4.4.4 循规式注释
4.4.5 日志式注释
4.4.6 废话注释
4.4.7 可怕的废话
4.4.8 能用函数或变量时就别用注释
4.4.9 位置标记
4.4.10 括号后面的注释
4.4.11 归属与署名
4.4.12 注释掉的代码
4.4.13 HTML注释
4.4.14 非本地信息
4.4.15 信息过多
4.4.16 不明显的联系
4.4.17 函数头
4.4.18 非公共代码中的Javadoc
4.4.19 范例
4.5 文献
第5章 格式
5.1 格式的目的
5.2 垂直格式
5.2.1 向报纸学习
5.2.2 概念间垂直方向上的区隔
5.2.3 垂直方向上的靠近5.2.4 垂直距离
5.2.5 垂直顺序
5.3 横向格式
5.3.1 水平方向上的区隔与靠近
5.3.2 水平对齐
5.3.3 缩进
5.3.4 空范围
5.4 团队规则
5.5 鲍勃大叔的格式规则
第6章 对象和数据结构
6.1 数据抽象
6.2 数据、对象的反对称性
6.3 得墨忒耳律
6.3.1 火车失事
6.3.2 混杂
6.3.3 隐藏结构
6.4 数据传送对象
6.5 小结
6.6 文献
第7章 错误处理
7.1 使用异常而非返回码
7.2 先写Try-Catch-Finally语句
7.3 使用不可控异常
7.4 给出异常发生的环境说明
7.5 依调用者需要定义异常类
7.6 定义常规流程
7.7 别返回null值
7.8 别传递null值7.9 小结
7.10 文献
第8章 边界
8.1 使用第三方代码
8.2 浏览和学习边界
8.3 学习log4j
8.4 学习性测试的好处不只是免费
8.5 使用尚不存在的代码
8.6 整洁的边界
8.7 文献
第9章 单元测试
9.1 TDD三定律
9.2 保持测试整洁
9.3 整洁的测试
9.3.1 面向特定领域的测试语言
9.3.2 双重标准
9.4 每个测试一个断言
9.5 F.I.R.S.T.
9.6 小结
9.7 文献
第10章 类
10.1 类的组织
10.2 类应该短小
10.2.1 单一权责原则
10.2.2 内聚
10.2.3 保持内聚性就会得到许多短小的类
10.3 为了修改而组织
10.4 文献第11章 系统
11.1 如何建造一个城市
11.2 将系统的构造与使用分开
11.2.1 分解main
11.2.2 工厂
11.2.3 依赖注入
11.3 扩容
11.4 Java代理
11.5 纯Java AOP框架
11.6 AspectJ的方面
11.7 测试驱动系统架构
11.8 优化决策
11.9 明智使用添加了可论证价值的标准
11.10 系统需要领域特定语言
11.11 小结
11.12 文献
第12章 迭进
12.1 通过迭进设计达到整洁目的
12.2 简单设计规则1:运行所有测试
12.3 简单设计规则2~4:重构
12.4 不可重复
12.5 表达力
12.6 尽可能少的类和方法
12.7 小结
12.8 文献
第13章 并发编程
13.1 为什么要并发
13.2 挑战13.3 并发防御原则
13.3.1 单一权责原则
13.3.2 推论:限制数据作用域
13.3.3 推论:使用数据复本
13.3.4 推论:线程应尽可能地独立
13.4 了解Java库
13.5 了解执行模型
13.5.1 生产者-消费者模型
13.5.2 读者-作者模型
13.5.3 宴席哲学家
13.6 警惕同步方法之间的依赖
13.7 保持同步区域微小
13.8 很难编写正确的关闭代码
13.9 测试线程代码
13.9.1 将伪失败看作可能的线程问题
13.9.2 先使非线程代码可工作
13.9.3 编写可插拔的线程代码
13.9.4 编写可调整的线程代码
13.9.5 运行多于处理器数量的线程
13.9.6 在不同平台上运行
13.9.7 装置试错代码
13.9.8 硬编码
13.9.9 自动化
13.10 小结
13.11 文献
第14章 逐步改进
14.1 Args的实现
14.2 Args:草稿14.2.1 所以我暂停了
14.2.2 渐进
14.3 字符串参数
14.4 小结
第15章 JUnit内幕
15.1 JUnit框架
15.2 小结
第16章 重构SerialDate
16.1 首先,让它能工作
16.2 让它做对
16.3 小结
16.4 文献
第17章 味道与启发
17.1 注释
17.2 环境
17.3 函数
17.4 一般性问题
17.5 Java
17.6 名称
17.7 测试
17.8 小结
17.9 文献
附录A 并发编程II
A.1 客户端服务器的例子
A.1.1 服务器
A.1.2 添加线程代码
A.1.3 观察服务器端
A.1.4 小结A.2 执行的可能路径
A.2.1 路径数量
A.2.2 深入挖掘
A.2.3 小结
A.3 了解类库
A.3.1 Executor框架
A.3.2 非锁定的解决方案
A.3.3 非线程安全类
A.4 方法之间的依赖可能破坏并发代码
A.4.1 容忍错误
A.4.2 基于客户代码的锁定
A.4.3 基于服务端的锁定
A.5 提升吞吐量
A.5.1 单线程条件下的吞吐量
A.5.2 多线程条件下的吞吐量
A.6 死锁
A.6.1 互斥
A.6.2 上锁及等待
A.6.3 无抢先机制
A.6.4 循环等待
A.6.5 不互斥
A.6.6 不上锁及等待
A.6.7 满足抢先机制
A.6.8 不做循环等待
A.7 测试多线程代码
A.8 测试线程代码的工具支持
A.9 小结
A.10 教程:完整代码范例A.10.1 客户端服务器非线程代码
A.10.2 使用线程的客户端服务器代码
附录B org.jfree.date.SerialDate
结束语代码整洁之道
[美]Robert C.Martin 著
韩磊 译
人民邮电出版社
北京图书在版编目(CIP)数据
代码整洁之道(美)马丁(Martin,R.C)著;韩磊译.--北京:人民
邮电出版社,2010.1
ISBN 978-7-115-21687-8
Ⅰ.①代… Ⅱ.①马…②韩… Ⅲ.①软件开发 Ⅳ.①TP311.52
中国版本图书馆CIP数据核字(2009)第202911号
版权声明
Authorized translation from the English language edition,entitled Clean
Code:A Handbook of Agile Software Craftsmanship,9780132350884 by
Robert C.Martin,published by Pearson Education,Inc,publishing as Prentice
Hall,Copyright?2009 Pearson Education,Inc.
All rights reserved.No part of this book may be reproduced or
transmitted in any form or by any means,electronic or mechanical,including
photocopying,recording or by any information storage retrieval
system,without permission from Pearson Education,Inc.CHINESE
SIMPLIFIED language edition published by PEARSON EDUCATION
ASIA LTD.,and POSTS TELECOMMUNICATIONS PRESS
Copyright?2009.
本书封面贴有Pearson Education(培生教育出版集团)激光防伪
标签。无标签者不得销售。
代码整洁之道
◆著 [美]Robert C.Martin
译 韩磊
责任编辑 刘映欣
◆人民邮电出版社出版发行 北京市崇文区夕照寺街14号
邮编 100061 电子函件 315@ptpress.com.cn
网址 http:www.ptpress.com.cn三河市潮河印业有限公司印刷
◆开本:800×1000 116
印张:25.5
字数:554千字 2010年1月第1版
印数:1-5000册 2010年1月河北第1次印刷
著作权合同登记号 图字:01-2008-5467号
ISBN 978-7-115-21687-8
定价:59.00元
读者服务热线:(010)67132705 印装质量热线:(010)67129223
反盗版热线:(010)67171154内容提要
软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相
关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。
本书提出一种观念:代码质量与其整洁度成正比。干净的代码,既
在质量上较为可靠,也为后期维护、升级奠定了良好基础。作为编程领
域的佼佼者,本书作者给出了一系列行之有效的整洁代码操作实践。这
些实践在本书中体现为一条条规则(或称“启示”),并辅以来自现实项
目的正、反两面的范例。只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。
本书阅读对象为一切有志于改善代码质量的程序员及技术经理。书
中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编
程方面,虽为一“家”之言,然诚有可资借鉴的价值。序
乐嚼(Ga-Jol)是在丹麦最受欢迎的糖果品种之一,它浓郁的甘草
味道,完美地弥补了此地潮湿且时常寒冷的天气。对于我们这些丹麦
人,乐嚼的妙处还在于包装盒顶上印制的哲言慧语。今早我买了一包两
件装,在其包装盒上发现这句丹麦谚语:
rlighed i sm? ting er ikke nogen lille ting.
“小处诚实非小事。”这句话正好是我想在这里说的。以小见大。本
书写到了一些价值殊胜的小主题。
神在细节之中,建筑师Ludwig mies van der Rohe(路德维希·密斯·
范·德·罗)[1]如是说。这句话引发了有关软件开发、特别是敏捷软件开
发中架构所处地位的若干争论。鲍勃(Bob)[2]和我时常发现自己沉湎
于此类对话中。没错,Ludwig mies van der Rohe的确专注于效用和基于
宏伟架构之上的永恒建筑形式。然而,他也为自己设计的每所房屋挑选
每个门把手。为什么?因为小处见大。
就 TDD[3]话题展开目前仍在继续的“辩论”时,鲍勃和我认识到,我们均同意软件架构在开发中占据重要地位,但就其确切意义而言,我
们之间还有分歧。然而,这种矛与盾孰利的讨论相对而言并不重要,因
为在项目开始之时,我们理所当然应该让专业人士投入些许时间去思考
及规划。20世纪90年代末期有关仅以测试和代码驱动设计的概念已一去
不返。相对于任何宏伟愿景,对细节的关注甚至是更为关键的专业性基
础。首先,开发者通过小型实践获得可用于大型实践的技能和信用度。
其次,宏大建筑中最细小的部分,比如关不紧的门、有点儿没铺平的地板,甚至是凌乱的桌面,都会将整个大局的魅力毁灭殆尽。这就是整洁
代码之所系。
架构只是软件开发用到的借喻之一,主要用在那种等同于建筑师交
付毛坯房一般交付初始软件产品的场合。在Scrum和敏捷(Agile)的日
子里,人们关注的是快速将产品推向市场。我们要求工厂全速运转、生
产软件。这就是人类工厂:懂思考、会感受的编码人,他们由产品备忘
或用户故事开始创造产品。来自制造业的借喻在这种场合大行其道。例
如,Scrum 就从装配线式的日本汽车生产方式中获益良多。
即便是在汽车工业里,大量工作也并不在于生产而在于维护——或
避免维护。对于软件而言,百分之八十或更多的工作量集中在我们美其
名曰“维护”的事情上:其实就是修修补补。与其接受西方关于制造好软
件的传统看法,不如将其看作建筑工业中的房屋修理工,或者汽车领域
的汽修工。日本式管理对于这种事怎么说的呢?
大约在1951年,一种名为“全员生产维护”(Total Productive
Maintenance,TPM)的质量保证手段在日本出现。它关注维护甚于关
注生产。TPM的主要支柱之一是所谓的5S原则体系。5S是一套规程,用“规程”这个词,是为了读者便于理解。5S原则其实是精益(Lean)
——西方视野中的一个时髦词,也是在软件领域渐领风骚的时髦词——
的基石所在。正如鲍勃大叔(Uncle Bob)在前言中写到的,良好的软
件实践遵循这些规程:专注、镇定和思考。这并非总只有关实作,有关
推动工厂设备以最高速度运转。5S哲学包括以下概念:
整理(Seiri)[4],或谓组织(想想英语中的sort(分类、排序)一
词)。搞清楚事物之所在——通过恰当地命名之类的手段——至关重
要。觉得命名标识无关紧要?读读后面的章节吧。
整顿(Seiton),或谓整齐(想想英文中的systematize(系统化)
一词)。有句美国老话说:物皆有其位,而后物尽归其位(A place for
everything, and everything in its place)。每段代码都该在你希望它所在的地方——如果不在那里,就需要重构了。
清楚(Seiso),或谓清洁(想想英文中的shine(锃亮)一词)。
清理工作地的拉线、油污和边角废料。对于那种四处遗弃的带注释的代
码及反映过往或期望的无注释代码,本书作者怎么说的来着?除之而后
快。
清洁(Seiketsu),或谓标准化。有关如何保持工作地清洁的组内
共识。本书有没有提到在开发组内使用一贯的代码风格和实践手段?这
些标准从哪里来?读读看。
身美(Shitsuke)[5],或谓纪律(自律)。在实践中贯彻规程,并
时时体现于个人工作上,而且要乐于改进。
如果你接受挑战——没错,就是挑战,阅读并应用本书,你就会理
解和赞赏上述最后一条。我们最终是在驶向一种负责任的专业精神之根
源所在,这种专业性隶属于一个关注产品生命周期的专业领域。在我们
遵循 TPM 来维护机动车和其他机械时,停机维护——等待缺陷显现出
来——并不常见。我们更上一层楼:每天检查机械,在磨损机件停止工
作之前就换掉它,或者按常例每1000英里(约1609.3km)就更换润滑
油、防止磨损和开裂。对于代码,应无情地做重构。还可以更进一步,就像TPM运动在50多年前的创新:一开始就打造更易维护的机械。写出
可读的代码,重要程度不亚于写出可执行的代码。1960年左右,围绕
TPM引入的终极实践(ultimate practice),关注用全新机械替代旧机
械。诚如Fred Brooks所言,我们或许应该每7年就重做一次软件的主要
模块,清理缓慢陈腐的代码。也许我们该把重构周期从以年计缩短到以
周、以天甚至以小时计。那便是细节所在了。
细节中自有天地,而在生活中应用此类手段时也有微言大义,就像
我们一成不变地对那些源自日本的做法寄予厚望一般。这并非只是东方
的生活观;英美民间也遍是这类警句。上引“整顿”(Seiton)二字就曾
出现在某位俄亥俄州牧师的笔下,他把齐整看作是“荡涤种种罪恶之良方”。“清楚”(Seiso)又如何呢?整洁近乎虔诚(Cleanliness is next to
godliness)。一张脏乱的桌子足以夺去一所丽宅的光彩。老话怎么
说“身美”(Shitsuke)的?守小节者不亏大节(He who is faithful in little
is faithful in much)。对于时时准备在恰当时机做重构,为未来
的“大”决定夯实基础,而不是置诸脑后,有什么说法吗?及时一针省九
针(A stitch in time saves nine)。早起的鸟儿有虫吃(The early bird
catches the worm)。日事日毕(Don’t put off until tomorrow what you can
do today)。在精益实践落入软件咨询师之手前,这就是其所谓“最后时
机”的本义所在。摆正单项工作在整体中的位置呢?巨木生于树籽
(Mighty oaks from little acorns grow)。如何在日常生活中做好简单的
防备性工作呢?防病好过治病(An ounce of prevention is worth a pound
of cure)。一天一苹果,医生远离我(An apple a day keeps the doctor
away)。整洁代码以其对细节的关注,荣耀了深埋于我们现有、或曾
有、或该有的壮丽文化之下的智慧根源。
即便是在宏伟的建筑作品中,我们也听到关注细节的回响。想想
Ludwig mies van der Rohe的门把手吧。那正是整理(seiri)。认真对待
每个变量名。你当用为自己第一个孩子命名般的谨慎来给变量命名。
正如每位房主所知,此类照料和修葺永无休止。建筑师Christopher
Alexander——模式与模式语言之父——把每个设计动作看作是较小的局
部修复动作。他认为,设计良好结构才是建筑师的本职所在,而更大的
建筑形态则当留给模式及居住者搬进的家私来完成。设计始终在持续进
行,不只是在新建一个房间时,也在我们重新粉刷墙面、更换旧地毯或
者换厨房水槽时。大多数艺术门类也持类似主张。在寻找其他推崇细节
的人时,我们发现,19世纪法国作家Gustav Flaubert(古斯塔夫·福楼
拜)名列其中。法国诗人Paul Valery(保尔·瓦雷里)认为,每首诗歌都
无写完之时,得持续重写,直至放弃为止。全心倾注于细节,屡见于追
求卓越的行为之中。虽然这无甚新意,但阅读本书对读者仍是一种挑战,你要重拾久已弃置脑后的良好规则,自发自主,“响应改变”。
不幸的是,我们往往见不到人们把对细节的关注当作编程艺术的基
础要件。我们过早地放弃了在代码上的工作,并不是因为它业已完成,而是因为我们的价值体系关注外在表现甚于关注要交付之物的本质。疏
忽最终结出了恶果:坏东西一再出现。无论是在行业里还是学术领域,研究者都很重视代码的整洁问题。供职于贝尔软件生产研究实验室
(Bell Labs Software Production Research)——没错,就是生产!——
时,我们有些不太严密的发现,认为前后一致的缩进风格明显标志了较
低的缺陷率。我们原指望将质量归因于架构、编程语言或者其他高级概
念;我们的专业能力归功于对工具的掌握和各种高高在上的设计方法,至于那些安置于厂区的机器,那些编码者,他们居然通过简单地保持一
致缩进风格创造了价值,这简直是一种侮辱。我在17年前就在书中写
过,这种风格远不止是一种单纯的能力那么简单。日本式的世界观深知
日常工作者的价值,而且,还深知工作者简单的日常行为所锻造的开发
系统的价值。质量是上百万次全心投入的结果——而非仅归功于任何来
自天堂的伟大方法。这些行为简单却不简陋,也不意味着简易。相反,它们是人力所能达的不仅伟大而且美丽的造物。忽略它们,就不成其为
完整的人。
当然,我仍然提倡放宽思路,也推崇根植于深厚领域知识和软件可
用性的各种架构手法的价值。但本书与此无关——至少,没有明显关
系。本书精妙之处,其意义之深远,不该无人赏识。它正与Peter
Sommerlad、Kevlin Henny及Giovanni Asproni等真正写代码的人现今所
持的观念相吻合。他们鼓吹“代码即设计”和“简单代码”。我们要谨记,界面就是程序,而且其结构也极大地反映出程序结构,但也理应始终谦
逊地承认设计存在于代码中,这至关紧要。制造上的返工导致成本上
升,但重做设计却创造出价值。我们应当视代码为设计——作为过程而
非终点的设计——这种高尚行为的漂亮体现。耦合与内聚的架构韵律在代码中脉动。Larry Constantine以代码的形式——而不是用UML那种高
高在上的抽象概念——来描述耦合与内聚。Richard Garbriel
在“Abstraction Descant”(抽象刍议)一文中告诉我们,抽象即恶。代码
除恶,而整洁的代码则大抵是圣洁的。
回到我那个小小的乐嚼包装盒,我想要重点提一下,那句丹麦谚语
不只是教我们重视小处,更教我们小处要诚实。这意味着对代码诚实、对同僚坦承代码现状,最重要的是在代码问题上不自欺。是否已尽全
力“把露营地清理得比来时还干净”?签入代码前是否已做重构?这可不
是皮毛小事,它正高卧于敏捷价值的正中位置。Scrum 有一种建议的实
践,主张重构是“完成”(Done)概念的一部分。无论是架构还是代码都
不强求完美,只求竭诚尽力而已。人孰无过,神亦容之(To err is
human; to forgive, divine)。在Scrum中,我们使一切可见。我们晾出脏
衣服。我们坦承代码状态,因为它永不完美。我们日渐成为完整的人,配得起神的眷顾,也越来越接近细节中的伟大之处。
在自己的专业领域中,我们亟需能得到的一切帮助。假使干净的地
板能减少事故发生,假使归置到位的工具能提升生产力,我也会倾力做
到。至于本书,在我看过的有关将精益原则应用于软件的印刷品中,是
最具实用性的。那班求索者多年来并肩奋斗,不但是为求一己之进步,更将他们的知识通过和你手上正在做的事一般的工作贡献给这个行业。
看过鲍勃大叔寄来的原稿之后,我发现,世界竟略有改善了。
对高瞻远瞩的练习业已结束,我要去清理自己的书桌了。
James O.Coplien于丹麦默尔鲁普
[1].译注:20世纪中期著名现代建筑大师,秉承“少即是多”的建筑设计
哲学,缔造了玻璃幕墙等现代建筑结构。
[2].译注:本书主要作者Robert C. Martin绰号Uncle Bob,这里的“鲍
勃”及后文的“鲍勃大叔”就是指Robert C. Martin。[3].译注:Test Driven Development,测试驱动开发。
[4].译注:这些概念最初出现于日本,5个概念的日文罗马字拼音首字母
正好都是S,所以这里也保留了日文罗马字拼音写法。中译本以日文汉
字直接译出,读者留意,不可直接对应其中文意思。
[5].译注:中文意为“素养、教养”。关于封面
封面的图片是M104:草帽星系(The Sombrero Galaxy)。M104坐
落于处女座(Virgo),距地球仅3000万光年。其核心是一个质量超大
的黑洞,有100万个太阳那么重。
这幅图是否让你想起了Klingon星球(克林贡)[1]的卫星Praxis(普
拉西斯)爆炸的事?我清楚地记得,在《星舰迷航 VI》中,大爆炸之
后碎片四溅,飞舞出一个赤道光环的场景。至此,光环就成为科幻电影
中爆炸场景的必然产物了。甚至就在《星舰迷航》系列电影的后续情节
中,Alderaan(阿尔德然)的爆炸也有类似场景出现。
环绕M104的光环是什么造成的?它为何会有如此巨大的膨胀率和
如此明亮而微小的内核?在我看来,仿佛那位于中心位置的黑洞勃然大
怒,向星系的中心扔出了一个3万光年大的洞一般。在这场宇宙大崩塌
所及范围之内的居民全都大难临头了。
超大质量的黑洞以星体为食,将星体的相当部分质量转换为能量。
方程式E = MC2已经足够体现杠杆作用了,但当 M 有一颗星体那么大的
质量时,看吧!在那巨兽酒足饭饱之前,有多少星体会一头撞进它的胃
里?核心部分空洞的大小,是否说明了些什么呢?
封面上的M104图片,是用来自于哈勃望远镜的那幅著名的可见光
相片(上图)和Spitzer(斯比泽)轨道探测器最新的红外影像(下图)
组合而成。
在红外影像中,光环中的热粒子闪耀着穿过了中心膨胀体。这两幅影像组合起来,显现出我们从未见过的景象,展示了久远之前曾熊熊燃
烧的火海。
封面图片:来自斯比泽太空望远镜
[1].系列剧《星舰迷航》(Star Trek)中的故事情节,Praxis星爆炸,由
此导致联邦和Klingon达成首次和平协议。代码猴子与童子军军规
2007年3月,我在SD West 2007技术大会上聆听了Robert C.
Martin(鲍勃大叔)的主题演讲“Craftsmanship and the Problem of
Productivity: Secrets for Going Fast without Making a Mess”。一身休闲打
扮的鲍勃大叔,以一曲嘲笑低水平编码者的Code Monkey(代码猴子)
开场。
是的,我们就是一群代码猴子,上蹿下跳,自以为领略了编程的真
谛。可惜,当我们抓着几个酸桃子,得意洋洋坐到树枝上,却对自己造
成的混乱熟视无睹。那堆“可以运行”的乱麻程序,就在我们的眼皮底下
慢慢腐坏。
从听到那场以TDD为主题的演讲之后,我就一直关注鲍勃大叔,还
有他在TDD和整洁代码方面的言论。去年,人民邮电出版社计算机分社
拿一本书给我看,封面上赫然写着Robert C. Martin的大名。看完原书序
和前言,我已经按捺不住,接下了翻译此书的任务。这本书名为Clean
Code,乃是Object Mentor(鲍勃大叔开办的技术咨询和培训公司)一干
大牛在编程方面的经验累积。按鲍勃大叔的话来说,就是“Object
Mentor整洁代码派”的说明。
正如 Coplien 在序中所言,宏大建筑中最细小的部分,比如关不紧
的门、有点儿没铺平的地板,甚至是凌乱的桌面,都会将整个大局的魅
力毁灭殆尽。这就是整洁代码之所系。Coplien列举了许多谚语,证明整
洁的价值,中国也有修身齐家治国平天下之语。整洁代码的重要性毋庸
置疑,问题是如何写出真正整洁的代码。本书既是整洁代码的定义,亦是如何写出整洁代码的指南。鲍勃大
叔认为,“写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的‘整洁
感’。这种‘代码感’就是关键所在……它不仅让我们看到代码的优劣,还
予我们以借戒规之力化劣为优的攻略。”作者阐述了在命名、函数、注
释、代码格式、对象和数据结构、错误处理、边界问题、单元测试、类、系统、并发编程等方面如何做到整洁的经验与最佳实践。长期遵照
这些经验编写代码,所谓“代码感”也就自然而然滋生出来。更有价值的
部分是鲍勃大叔本人对3个Java项目的剖析与改进过程的实操记录。通
过这多达3章的重构记录,鲍勃大叔充分地证明了童子军军规在编程领
域同样适用:离开时要比发现时更整洁。为了向读者呈现代码的原始状
态,这部分代码及本书其他部分的绝大多数代码注释都不做翻译。如果
读者有任何疑问,可通过邮件与我沟通(cleancode.cn@gmail.com)。
接触开发技术十多年以来,特别是从事IT技术媒体工作六年以来,我见过许多对于代码整洁性缺乏足够重视的开发者。不算过分地说,这
是职业素养与基本功的双重缺陷。我翻译The Elements of C Style(中
译版《C编程风格》)和本书,实在也是希望在这方面看到开发者重视
度和实际应用的提升。
在本书的结束语中,鲍勃大叔提到别人给他的一条腕带,上面的字
样是Test Obsessed(沉迷测试)。鲍勃大叔“发现自己无法取下腕带。不
仅是因为腕带很紧,而且那也是条精神上的紧箍咒。……它一直提醒
我,我做了写出整洁代码的承诺。”有了这条腕带,代码猴子成了模范
童子军。我想,每位开发者都需要这样一条腕带吧?
韩磊
2009年11月前言
衡量代码质量的唯一有效标准:WTFmin
承Thom Holwerda惠允,自http:www.osnews.comstory19266WTFs_m
再制你的代码在哪道门后面?你的团队或公司在哪道门后面?为什么会
在那里?只是一次普通的代码复查,还是产品面世后才发现一连串严重
问题?我们是否在战战兢兢地调试自己之前错以为没问题的代码?客户
是否在流失?经理们是否把我们盯得如芒刺在背?当事态变得严重起
来,如何保证我们在那道正确的门后做补救工作?答案是:技艺
(craftsmanship)。
习艺之要有二:知和行。你应当习得有关原则、模式和实践的知
识,穷尽应知之事,并且要对其了如指掌,通过刻苦实践掌握它。
我可以教你骑自行车的物理学原理。实际上,经典数学的表达方式
相对而言确实简洁明了。重力、摩擦力、角动量、质心等,用一页写满
方程式的纸就能说明白。有了这些方程式,我可以为你证明出骑车完全
可行,而且还可以告诉你骑车所需的全部知识。即便如此,你在初次骑
车时还是会跌倒在地。
编码亦同此理。我们可以写下整洁代码的所有“感觉良好”的原则,放手让你去干(换言之,让你从自行车上摔下来)。那样的话,我们算
是哪门子老师?而你又会成为怎样的学生呢?
不!本书可不会这么做。
学写整洁代码很难。它可不止于要求你掌握原则和模式。你得在这
上面花工夫。你须自行实践,且体验自己的失败。你须观察他人的实践
与失败。你须看看别人是怎样蹒跚学步,再转头研究他们的路数。你须
看看别人是如何绞尽脑汁做出决策,又是如何为错误决策付出代价。
阅读本书要多用心思。这可不是那种降落前就能读完的“感觉不
错”的飞机书。本书要让你用功,而且是非常用功。如何用功?阅读代
码——大量代码。而且你要去琢磨某段代码好在什么地方、坏在什么地
方。在我们分解,而后组合模块时,你得亦步亦趋地跟上。这得花些工
夫,不过值得一试。
本书大致可分为3个部分。前几章介绍编写整洁代码的原则、模式和实践。这部分有相当多的示例代码,读起来颇具挑战性。读完这几
章,就为阅读第2部分做好了准备。如果你就此止步,只能祝你好运
啦!
第2部分最需要花工夫。这部分包括几个复杂性不断增加的案例研
究。每个案例都清理一些代码——把有问题的代码转化为问题少一些的
代码。这部分极为详细。你的思维要在讲解和代码段之间跳来跳去。你
得分析和理解那些代码,琢磨每次修改的来龙去脉。
你付出的劳动将在第3部分得到回报。这部分只有一章,列出从上
述案例研究中得到的启示和灵感。在遍览和清理案例中的代码时,我们
把每个操作理由记录为一种启示或灵感。我们尝试去理解自己对阅读和
修改代码的反应,尽力了解为什么会有这样的感受、为什么会如此行
事。结果得到了一套描述在编写、阅读、清理代码时思维方式的知识
库。
如果你在阅读第2部分的案例研究时没有好好用功,那么这套知识
库对你来说可能所值无几。在这些案例研究中,每次修改都仔细注明了
相关启示的标号。这些标号用方括号标出,如:[H22]。由此你可以看
到这些启示在何种环境下被应用和编写。启示本身不值钱,启示与案例
研究中清理代码的具体决策之间的关系才有价值。
如果你跳过案例研究部分,只阅读了第1部分和第3部分,那就不过
是又看了一本关于写出好软件的“感觉不错”的书。但如果你肯花时间琢
磨那些案例,亦步亦趋——站在作者的角度,迫使自己以作者的思维路
径考虑问题,就能更深刻地理解这些原则、模式、实践和启示。这样的
话,就像一个熟练地掌握了骑车的技术后,自行车就如同其身体的延伸
部分那样;对你来说,本书所介绍的整洁代码的原则、模式、实践和启
示就成为了本身具有的技艺,而不再是“感觉不错”的知识。
致谢
插图感谢两位艺术家Jennifer Kohnke和Angela Brooks。Jennifer绘制了每
章起始处创意新颖、效果惊人的插图,以及Kent Beck、Ward
Cunningham、Bjarne Stroustrup、Ron Jeffries、Grady Booch、Dave
Thomas、Michael Feathers和我本人的肖像。
Angela 绘制了文中那些精致的插图。这些年她为我画了一些画,包
括 Agile Software Development: Principles, Patterns, and Practices(中译版
《敏捷软件开发:原则、模式与实践》)一书中的大量插图。她是我的
长女,常给我带来极大的愉悦。第1章 整洁代码阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好
的程序员。很好。我们需要更好的程序员。
这是本有关编写好程序的书。它充斥着代码。我们要从各个方向来
考察这些代码。从顶向下,从底往上,从里而外。读完后,就能知道许
多关于代码的事了。而且,我们还能说出好代码和糟糕的代码之间的差
异。我们将了解到如何写出好代码。我们也会知道,如何将糟糕的代码
改成好代码。1.1 要有代码
有人也许会以为,关于代码的书有点儿落后于时代——代码不再是
问题;我们应当关注模型和需求。确实,有人说过我们正在临近代码的
终结点。很快,代码就会自动产生出来,不需要再人工编写。程序员完
全没用了,因为商务人士可以从规约直接生成程序。
扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些
层面上,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器
可以执行的细节程度,就是编程要做的事。而这种规约正是代码。
我期望语言的抽象程度继续提升。我也期望领域特定语言的数量继
续增加。那会是好事一桩。但那终结不了代码。实际上,在较高层次上
用领域特定语言撰写的规约也将是代码!它也得严谨、精确、规范和详
细,好让机器理解和执行。
那帮以为代码终将消失的伙计,就像是巴望着发现一种无规范数学
的数学家们一般。他们巴望着,总有一天能创造出某种机器,我们只要
想想、嘴都不用张就能叫它依计行事。那机器要能透彻理解我们,只有
这样,它才能把含糊不清的需求翻译为可完美执行的程序,精确满足需
求。
这种事永远不会发生。即便是人类,倾其全部的直觉和创造力,也
造不出满足客户模糊感觉的成功系统来。如果说需求规约原则教给了我
们什么,那就是归置良好的需求就像代码一样正式,也能作为代码的可
执行测试来使用。
记住,代码确然是我们最终用来表达需求的那种语言。我们可以创
造各种与需求接近的语言。我们可以创造帮助把需求解析和汇整为正式结构的各种工具。然而,我们永远无法抛弃必要的精确性——所以代码
永存。1.2 糟糕的代码
最近我在读Kent Beck著Implementation Patterns(中译版《实现模
式》)[1]一书的序言。他这样写道:“……本书基于一种不太牢靠的前
提:好代码的确重要……”这前提不牢靠?我反对!我认为这是该领域
最强固、最受支持、最被强调的前提了(我想Kent也知道)。我们知道
好代码重要,是因为其短缺实在困扰了我们太久。
20 世纪 80 年代末,有家公司写了个很流行的杀手应用,许多专业
人士都买来用。然后,发布周期开始拉长。缺陷总是不能修复。装载时
间越来越久,崩溃的几率也越来越大。至今我还记得自己在某天沮丧地
关掉那个程序,从此再不用它。在那之后不久,该公司就关门大吉了。20年后,我见到那家公司的一位早期雇员,问他当年发生了什么
事。他的回答叫我愈发恐惧起来。原来,当时他们赶着推出产品,代码
写得乱七八糟。特性越加越多,代码也越来越烂,最后再也没法管理这
些代码了。是糟糕的代码毁了这家公司。
你是否曾为糟糕的代码所深深困扰?如果你是位有点儿经验的程序
员,定然多次遇到过这类困境。我们有专用来形容这事的词:沼泽
(wading)。我们趟过代码的水域。我们穿过灌木密布、瀑布暗藏的沼
泽地。我们拼命想找到出路,期望有点什么线索能启发我们到底发生了
什么事;但目光所及,只是越来越多死气沉沉的代码。你当然曾为糟糕的代码所困扰过。那么——为什么要写糟糕的代码
呢?
是想快点完成吗?是要赶时间吗?有可能。或许你觉得自己要干好
所需的时间不够;假使花时间清理代码,老板就会大发雷霆。或许你只
是不耐烦再搞这套程序,期望早点结束。或许你看了看自己承诺要做的
其他事,意识到得赶紧弄完手上的东西,好接着做下一件工作。这种事
我们都干过。
我们都曾经瞟一眼自己亲手造成的混乱,决定弃之而不顾,走向新
一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂
程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不
(Later equals never)。1.3 混乱的代价
只要你干过两三年编程,就有可能曾被某人的糟糕的代码绊倒过。
如果你编程不止两三年,也有可能被这种代码拖过后腿。进度延缓的程
度会很严重。有些团队在项目初期进展迅速,但有那么一两年的时间却
慢如蜗行。对代码的每次修改都影响到其他两三处代码。修改无小事。
每次添加或修改代码,都得对那堆扭纹柴了然于心,这样才能往上扔更
多的扭纹柴。这团乱麻越来越大,再也无法理清,最后束手无策。
随着混乱的增加,团队生产力也持续下降,趋向于零。当生产力下
降时,管理层就只有一件事可做了:增加更多人手到项目中,期望提升
生产力。可是新人并不熟悉系统的设计。他们搞不清楚什么样的修改符
合设计意图,什么样的修改违背设计意图。而且,他们以及团队中的其
他人都背负着提升生产力的可怕压力。于是,他们制造更多的混乱,驱
动生产力向零那端不断下降。如图1-1所示。
图1-1 生产力vs.时间
1.3.1 华丽新设计最后,开发团队造反了,他们告诉管理层,再也无法在这令人生厌
的代码基础上做开发。他们要求做全新的设计。管理层不愿意投入资源
完全重启炉灶,但他们也不能否认生产力低得可怕。他们只好同意开发
者的要求,授权去做一套看上去很美的华丽新设计。
于是就组建了一支新军。谁都想加入这个团队,因为它是张白纸。
他们可以重新来过,搞出点真正漂亮的东西来。但只有最优秀、最聪明
的家伙被选中。其余人等则继续维护现有系统。
现在有两支队伍在竞赛了。新团队必须搭建一套新系统,要能实现
旧系统的所有功能。另外,还得跟上对旧系统的持续改动。在新系统功
能足以抗衡旧系统之前,管理层不会替换掉旧系统。
竞赛可能会持续极长时间。我就见过延续了十年之久的。到了完成
的时候,新团队的老成员早已不知去向,而现有成员则要求重新设计一
套新系统,因为这套系统太烂了。
假使你经历过哪怕是一小段我谈到的这种事,那么你一定知道,花
时间保持代码整洁不但有关效率,还有关生存。
1.3.2 态度
你是否遇到过某种严重到要花数个星期来做本来只需数小时即可完
成的事的混乱状况?你是否见过本来只需做一行修改,结果却涉及上百
个模块的情况?这种事太常见了。
怎么会发生这种事?为什么好代码会这么快就变质成糟糕的代码?
理由多得很。我们抱怨需求变化背离了初期设计。我们哀叹进度太紧
张,没法干好活。我们把问题归咎于那些愚蠢的经理、苛求的用户、没
用的营销方式和那些电话消毒剂。不过,亲爱的呆伯特(Dilbert)[2],我们是自作自受[3]。我们太不专业了。
这话可不太中听。怎么会是自作自受呢?难道不关需求的事?难道不关进度的事?难道不关那些蠢经理和没用的营销手段的事?难道他们
就不该负点责吗?
不。经理和营销人员指望从我们这里得到必须的信息,然后才能做
出承诺和保证;即便他们没开口问,我们也不该羞于告知自己的想法。
用户指望我们验证需求是否都在系统中实现了。项目经理指望我们遵守
进度。我们与项目的规划脱不了干系,对失败负有极大的责任;特别是
当失败与糟糕的代码有关时尤为如此!
“且慢!”你说。“不听经理的,我就会被炒鱿鱼。”多半不会。多数
经理想要知道实情,即便他们看起来不喜欢实情。多数经理想要好代
码,即便他们总是痴缠于进度。他们会奋力卫护进度和需求;那是他们
该干的。你则当以同等的热情卫护代码。
再说明白些,假使你是位医生,病人请求你在给他做手术前别洗
手,因为那会花太多时间,你会照办吗[4]?本该是病人说了算;但医生
却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风
险。医生如果按病人说的办,就是一种不专业的态度(更别说是犯罪
了)。
同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做
法。
1.3.3 迷题
程序员面临着一种基础价值谜题。有那么几年经验的开发者都知
道,之前的混乱拖了自己的后腿。但开发者们背负期限的压力,只好制
造混乱。简言之,他们没花时间让自己做得更快!真正的专业人士明
白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会
立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做得快的唯一方
法 ——就是始终尽可能保持代码整洁。1.3.4 整洁代码的艺术
假设你相信混乱的代码是祸首,假设你接受做得快的唯一方法是保
持代码整洁的说法,你一定会自问:“我怎么才能写出整洁的代码?”不
过,如果你不明白整洁对代码有何意义,尝试去写整洁代码就毫无所
益!
坏消息是写整洁代码很像是绘画。多数人都知道一幅画是好还是
坏。但能分辨优劣并不表示懂得绘画。能分辨整洁代码和肮脏代码,也
不意味着会写整洁代码!
写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。
这种“代码感”就是关键所在。有些人生而有之。有些人费点劲才能得
到。它不仅让我们看到代码的优劣,还予我们以借戒规之力化劣为优的
攻略。
缺乏“代码感”的程序员,看混乱是混乱,无处着手。有“代码感”的
程序员能从混乱中看出其他的可能与变化。“代码感”帮助程序员选出最
好的方案,并指导程序员制订修改行动计划,按图索骥。
简言之,编写整洁代码的程序员就像是艺术家,他能用一系列变换
把一块白板变作由优雅代码构成的系统。
1.3.5 什么是整洁代码
有多少程序员,就有多少定义。所以我只询问了一些非常知名且经
验丰富的程序员。
Bjarne Stroustrup,C++语言发明者,C++Programming
Language(中译版《C++程序设计语言》)一书作者。
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐
藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱
来。整洁的代码只做好一件事。
Bjarne用了“优雅”一词。说得好!我MacBook上的词典提供了如下
定义:外表或举止上令人愉悦的优美和雅观;令人愉悦的精致和简单。
注意对“愉悦”一词的强调。Bjarne显然认为整洁的代码读起来令人愉
悦。读这种代码,就像见到手工精美的音乐盒或者设计精良的汽车一
般,让你会心一笑。Bjarne 也提到效率——而且两次提及。这话出自 C++发明者之口,或许并不出奇;不过我认为并非是在单纯追求速度。被浪费掉的运算周
期并不雅观,并不令人愉悦。留意Bjarne 怎么描述那种不雅观的结果。
他用了“引诱”这个词。诚哉斯言。糟糕的代码引发混乱!别人修改糟糕
的代码时,往往会越改越烂。
务实的Dave Thomas和Andy Hunt从另一角度阐述了这种情况。他们
提到破窗理论[5]。窗户破损了的建筑让人觉得似乎无人照管。于是别人
也再不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外
墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。
Bjarne也提到完善错误处理代码。往深处说就是在细节上花心思。
敷衍了事的错误处理代码只是程序员忽视细节的一种表现。此外还有内
存泄漏,还有竞态条件代码。还有前后不一致的命名方式。结果就是凸
现出整洁代码对细节的重视。
Bjarne以“整洁的代码只做好一件事”结束论断。毋庸置疑,软件设
计的许多原则最终都会归结为这句警语。有那么多人发表过类似的言
论。糟糕的代码想做太多事,它意图混乱、目的含混。整洁的代码力求
集中。每个函数、每个类和每个模块都全神贯注于一事,完全不受四周
细节的干扰和污染。
Grady Booch,Object Oriented Analysis and Design with
Applications(中译版《面向对象分析与设计》)一书作者。
整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从
不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。Grady的观点与 Bjarne的观点有类似之处,但他从可读性的角度来
定义。我特别喜欢“整洁的代码如同优美的散文”这种看法。想想你读过
的某本好书。回忆一下,那些文字是如何在脑中形成影像!就像是看了
场电影,对吧?还不止!你还看到那些人物,听到那些声音,体验到那
些喜怒哀乐。
阅读整洁的代码和阅读Lord of the Rings(中译版《指环王》)自然
不同。不过,仍有可类比之处。如同一本好的小说般,整洁的代码应当
明确地展现出要解决问题的张力。它应当将这种张力推至高潮,以某种
显而易见的方案解决问题和张力,使读者发出“啊哈!本当如此!”的感叹。
窃以为Grady所谓“干净利落的抽象”(crisp abstraction),乃是绝妙
的矛盾修辞法。毕竟crisp几乎就是“具体”(concrete)的同义词。我
MacBook上的词典这样定义crisp一词:果断决绝,就事论事,没有犹豫
或不必要的细节。尽管有两种不同的定义,该词还是承载了有力的信
息。代码应当讲述事实,不引人猜测。它只该包含必需之物。读者应当
感受到我们的果断决绝。
“老大”Dave Thomas,OTI公司创始人,Eclipse战略教父。
整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测
试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事
的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽
量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所
有必需信息均可通过代码自身清晰表达。Dave老大在可读性上和Grady持相同观点,但有一个重要的不同之
处。Dave断言,整洁的代码便于其他人加以增补。这看似显而易见,但
亦不可过分强调。毕竟易读的代码和易修改的代码之间还是有区别的。
Dave将整洁系于测试之上!要在十年之前,这会让人大跌眼镜。但
测试驱动开发(Test Driven Development)已在行业中造成了深远影
响,成为基础规程之一。Dave说得对。没有测试的代码不干净。不管它有多优雅,不管有多可读、多易理解,微乎测试,其不洁亦可知也。
Dave 两次提及“尽量少”。显然,他推崇小块的代码。实际上,从
有软件起人们就在反复强调这一点。越小越好。
Dave也提到,代码应在字面上表达其含义。这一观点源自Knuth
的“字面编程”(literate programming)[6]。结论就是应当用人类可读的
方式来写代码。
Michael Feathers,Working Effectively with Legacy Code(中译版
《修改代码的艺术》)一书作者。
我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本
性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有
改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原
点,赞叹某人留给你的代码——全心投入的某人留下的代码。一言以蔽之:在意。这就是本书的题旨所在。或许该加个副标题,如何在意代码。
Michael一针见血。整洁代码就是作者着力照料的代码。有人曾花
时间让它保持简单有序。他们适当地关注到了细节。他们在意过。Ron Jeffries,Extreme Programming Installed(中译版《极限编
程实施》)以及 Extreme Programming Adventures in C(中译版
《C极限编程探险》)作者。
Ron 初入行就在战略空军司令部(Strategic Air Command)编写
Fortran 程序,此后几乎在每种机器上编写过每种语言的代码。他的言论
值得咀嚼。
近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了。
简单代码,依其重要顺序:能通过所有测试;
没有重复代码;
体现系统中的全部设计理念;
包括尽量少的实体,比如类、方法、函数等。
在以上诸项中,我最在意代码重复。如果同一段代码反复出现,就
表示某种想法未在代码中得到良好的体现。我尽力去找出到底那是什么,然后再尽力更清晰地表达出来。
在我看来,有意义的命名是体现表达力的一种方式,我往往会修改
好几次才会定下名字来。借助Eclipse这样的现代编码工具,重命名代价
极低,所以我无所顾忌。然而,表达力还不只体现在命名上。我也会检
查对象或方法是否想做的事太多。如果对象功能太多,最好是切分为两
个或多个对象。如果方法功能太多,我总是使用抽取手段(Extract
Method)重构之,从而得到一个能较为清晰地说明自身功能的方法,以
及另外数个说明如何实现这些功能的方法。
消除重复和提高表达力让我在整洁代码方面获益良多,只要铭记这
两点,改进脏代码时就会大有不同。不过,我时常关注的另一规则就不
太好解释了。
这么多年下来,我发现所有程序都由极为相似的元素构成。例
如“在集合中查找某物”。不管是雇员记录数据库还是名-值对哈希表,或者某类条目的数组,我们都会发现自己想要从集合中找到某一特定条
目。一旦出现这种情况,我通常会把实现手段封装到更抽象的方法或类
中。这样做好处多多。
可以先用某种简单的手段,比如哈希表来实现这一功能,由于对搜
索功能的引用指向了我那个小小的抽象,就能随需应变,修改实现手
段。这样就既能快速前进,又能为未来的修改预留余地。
另外,该集合抽象常常提醒我留意“真正”在发生的事,避免随意实
现集合行为,因为我真正需要的不过是某种简单的查找手段。
减少重复代码,提高表达力,提早构建简单抽象。这就是我写整洁
代码的方法。
Ron 以寥寥数段文字概括了本书的全部内容。不要重复代码,只做
一件事,表达力,小规模抽象。该有的都有了。
Ward Cunningham,Wiki发明者,eXtreme Programming (极限
编程)的创始人之一,Smalltalk语言和面向对象的思想领袖。所有在意代码者的教父。
如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让
编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代
码。
这种说法很 Ward。它教你听了之后就点头,然后继续听下去。如
此在理,如此浅显,绝不故作高深。你大概以为此言深合己意吧。再走近点看看。
“……深合己意”。你最近一次看到深合己意的模块是什么时候?模
块多半都繁复难解吧?难道没有触犯规则吗?你不是也曾挣扎着想抓住
些从整个系统中散落而出的线索,编织进你在读的那个模块吗?你最近
一次读到某段代码、并且如同对 Ward的说法点头一般对这段代码点
头,是什么时候的事了?
Ward期望你不会为整洁代码所震惊。你无需花太多力气。那代码
就是深合你意。它明确、简单、有力。每个模块都为下一个模块做好准
备。每个模块都告诉你下一个模块会是怎样的。整洁的程序好到你根本
不会注意到它。设计者把它做得像一切其他设计般简单。
那 Ward 有关“美”的说法又如何呢?我们都曾面临语言不是为要解
决的问题所设计的困境。但 Ward的说法又把球踢回我们这边。他说,漂亮的代码让编程语言像是专为解决那个问题而存在!所以,让语言变
得简单的责任就在我们身上了!当心,语言是冥顽不化的!是程序员让
语言显得简单。1.4 思想流派
我(鲍勃大叔)又是怎么想的呢?在我眼中整洁代码是什么样的?
本书将以详细到吓死人的程度告诉你,我和我的同道对整洁代码的看
法。我们会告诉你关于整洁变量名的想法,关于整洁函数的想法,关于
整洁类的想法,如此等等。我们视这些观点为当然,且不为其逆耳而致
歉。对我们而言,在职业生涯的这个阶段,这些观点确属当然,也是我
们整洁代码派的圭旨。武术家从不认同所谓最好的武术,也不认同所谓绝招。武术大师们
常常创建自己的流派,聚徒而授。因此我们才看到格雷西家族在巴西开
创并传授的格雷西柔术(Gracie Jiu Jistu),看到奥山龙峰(Okuyama
Ryuho)在东京开创并传授的八光流柔术(Hakkoryu Jiu Jistu),看到李
小龙(Bruce Lee)在美国开创并传授的截拳道(Jeet Kune Do)。
弟子们沉浸于创始人的授业。他们全心师从某位师傅,排斥其他师
傅。弟子有所成就后,可以转投另一位师傅,扩展自己的知识与技能。有些弟子最终百炼成钢,创出新招数,开宗立派。
任何门派都并非绝对正确。不过,身处某一门派时,我们总以其所
传之技为善。归根结底,练习八光流柔术或截拳道,自有其善法,但这
并不能否定其他门派所授之法。
可以把本书看作是对象导师(Object Mentor)[7]整洁代码派的说
明。里面要传授的就是我们勤操己艺的方法。如果你遵从这些教诲,你
就会如我们一般乐受其益,你将学会如何编写整洁而专业的代码。但无
论如何也别错以为我们是“正确的”。其他门派和师傅和我们一样专业。
你有必要也向他们学习。
实际上,书中很多建议都存在争议。或许你并不完全同意这些建
议。你可能会强烈反对其中一些建议。这样挺好的。我们不能要求做最
终权威。另外一方面,书中列出的建议,乃是我们长久苦思、从数十年
的从业经验和无数尝试与错误中得来。无论你同意与否,如果你没看到
或是不尊敬我们的观点,就真该自己害臊。1.5 我们是作者
Javadoc中的@author字段告诉我们自己是什么人。我们是作者。作
者都有读者。实际上,作者有责任与读者做良好沟通。下次你写代码的
时候,记得自己是作者,要为评判你工作的读者写代码。
你或许会问:代码真正“读”的成分有多少呢?难道力量主要不是用
在“写”上吗?
你是否玩过“编辑器回放”?20世纪80、90年代,Emac之类编辑器记
录每次击键动作。你可以在一小时工作之后,回放击键过程,就像是看
一部高速电影。我这么做过,结果很有趣。
回放过程显示,多数时间都是在滚动屏幕、浏览其他模块!
鲍勃进入模块。
他向下滚动到要修改的函数。
他停下来考虑可以做什么。
哦,他滚动到模块顶端,检查变量初始化。
现在他回到修改处,开始键入。
喔,他删掉了键入的内容。
他重新键入。
他又删除了!
他键入了一半什么东西,又删除掉。
他滚动到调用要修改函数的另一函数,看看是怎么调用的。
他回到修改处,重新键入刚才删掉的代码。
他停下来。
他再一次删掉代码!他打开另一个窗口,查看别的子类。那是个复载函数吗?……
你该明白了。读与写花费时间的比例超过10:1。写新代码时,我们
一直在读旧代码。
既然比例如此之高,我们就想让读的过程变得轻松,即便那会使得
编写过程更难。没可能光写不读,所以使之易读实际也使之易写。
这事概无例外。不读周边代码的话就没法写代码。编写代码的难
度,取决于读周边代码的难度。要想干得快,要想早点做完,要想轻松
写代码,先让代码易读吧。1.6 童子军军规
光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随
时间流逝而腐坏。我们应当更积极地阻止腐坏的发生。
借用美国童子军一条简单的军规,应用到我们的专业领域:
让营地比你来时更干净。[8]
如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清
理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过
长的函数,消除一点点重复代码,清理一个嵌套if语句。
你想要为一个代码随时间流逝而越变越好的项目工作吗?你还能相
信有其他更专业的做法吗?难道持续改进不是专业性的内在组成部分
吗?1.7 前传与原则
从许多角度看,本书都是我 2002 年写那本 Agile Software
Development:Principles,Patterns,and Practices(中译版《敏捷软件开
发:原则、模式与实践》,简称PPP)的“前传”。PPP关注面向对象设
计的原则,以及专业开发者采用的许多实践方法。假如你没读过 PPP,你会发现它像这本书的延续。如果你读过,会发现那本书的主张在代码
层面于本书中回响。
在本书中,你会发现对不同设计原则的引用,包括单一权责原则
(Single Responsibility Principle,SRP)、开放闭合原则(Open Closed
Principle,OCP)和依赖倒置原则(Dependency Inversion Principle,DIP)等。1.8 小结
艺术书并不保证你读过之后能成为艺术家,只能告诉你其他艺术家
用过的工具、技术和思维过程。本书同样也不担保让你成为好程序员。
它不担保能给你“代码感”。它所能做的,只是展示好程序员的思维过
程,还有他们使用的技巧、技术和工具。
和艺术书一样,本书也充满了细节。代码会很多。你会看到好代
码,也会看到糟糕的代码。你会看到糟糕的代码如何转化为好代码。你
会看到启发、规条和技巧的列表。你会看到一个又一个例子。但最终结
果取决于你自己。
还记得那个关于小提琴家在去表演的路上迷路的老笑话吗?他在街
角拦住一位长者,问他怎么才能去卡耐基音乐厅(Carnegie Hall)。长
者看了看小提琴家,又看了看他手中的琴,说道:“你还得练,孩子,还得练!”1.9 文献
[Beck07]:Implementation Patterns,Kent Beck,Addison-Wesley,2007.
[Knuth92]:Literate Programming, Donald E. Knuth, Center for the
Study of Language and Information, Leland Stanford Junior University,1992.
[1].原注:[Beck07]。
[2].译注:著名IT讽刺漫画。
[3].译注:原文为But the fault, dear Dilbert, is not in our stars, but in
ourselves.脱胎自莎士比亚戏剧《裘力斯·凯撒》第一幕第二场凯些斯的
台词The fault, dear Brutus, is not in our stars, but in ourselves, that we are
underlings.(若我们受人所制,亲爱的勃鲁托斯,那错也在我们身上,不能怪罪命运。)
[4].原注:1847年Ignaz Semmelweis(伊纳兹·塞麦尔维斯)提出医生应
洗手的建议时,遭到了反对,人们认为医生太忙,接诊时无暇洗手。
[5].原注:http:www.pragmaticprogrammer.combooksellers2004-
12.html。
[6].原注:[Knuth92]。
[7].译注:本书主要作者Robert C.Martin开办的技术咨询和培训公司。
[8].原注:摘自Robert Stephenson Smyth Baden-Powell(英国人,童子军
创始者)对童子军的遗言:“努力,让世界比你来时干净些……”第2章 有意义的命名
Tim Ottinger2.1 介绍
软件中随处可见命名。我们给变量、函数、参数、类和封包命名。
我们给源代码及源代码所在目录命名。我们给jar文件、war文件和ear文
件命名。我们命名、命名,不断命名。既然有这么多命名要做,不妨做
好它。下文列出了取个好名字的几条简单规则。2.2 名副其实
名副其实说起来简单。我们想要强调,这事很严肃。选个好名字要
花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好
的名称,就换掉旧的。这么做,读你代码的人(包括你自己)都会更开
心。
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉
你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来
补充,那就不算是名副其实。
int d; 消逝的时间,以日计
名称d什么也没说明。它没有引起对时间消逝的感觉,更别说以日
计了。我们应该选择指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
选择体现本意的名称能让人更容易理解和修改代码。下列代码的目
的何在?
public List
List
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;}
为什么难以说明上列代码要做什么事?里面并没有复杂的表达式。
空格和缩进中规中矩。只用到三个变量和两个常量。甚至没有涉及任何
其他类或多态方法,只是(或者看起来是)一个数组的列表而已。
问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代
码中未被明确体现的程度。上列代码要求我们了解类似以下问题的答
案:
(1)theList中是什么类型的东西?
(2)theList零下标条目的意义是什么?
(3)值4的意义是什么?
(4)我怎么使用返回的列表?
问题的答案没体现在代码段中,可那就是它们该在的地方。比方
说,我们在开发一种扫雷游戏,我们发现,盘面是名为theList的单元格
列表,那就将其名称改为gameBoard。
盘面上每个单元格都用一个简单数组表示。我们还发现,零下标条
目是一种状态值,而该种状态值为4表示“已标记”。只要改为有意义的
名称,代码就会得到相当程度的改进:
public List
List
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
注意,代码的简洁性并未被触及。运算符和常量的数量全然保持不
变,嵌套数量也全然保持不变。但代码变得明确多了。
还可以更进一步,不用 int 数组表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为isFlagged),从而掩盖住那个魔术
数[1]。于是得到函数的新版本:
public List
List
for (Cell cell : gameBoard)
if (cell.isFlagged)
flaggedCells.add(cell);
return flaggedCells;
}
只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名
称的力量。2.3 避免误导
程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本
意相悖的词。例如,hp、aix和sco都不该用做变量名,因为它们都是
UNIX平台或类UNIX平台的专有名称。即便你是在编写三角计算程序,hp看起来是个不错的缩写[2],但那也可能会提供错误信息。
别用accountList来指称一组账号,除非它真的是List类型。List一词
对程序员有特殊意义。如果包纳账号的容器并非真是个List,就会引起
错误的判断[3]。所以,用accountGroup或bunchOfAccounts,甚至直接用
accounts都会好一些。
提防使用不同之处较小的名称。想区分模块中某处的
XYZControllerFor EfficientHandlingOfStrings和另一处的
XYZControllerForEfficientStorageOfStrings,会花多长时间呢?这两个词
外形实在太相似了。
以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误
导。我们很享受现代Java编程环境的自动代码完成特性。键入某个名称
的前几个字母,按一下某个热键组合(如果有的话),就能得到一列该
名称的可能形式。假如相似的名称依字母顺序放在一起,且差异很明
显,那就会相当有助益,因为程序员多半会压根不看你的详细注释,甚
至不看该类的方法列表就直接看名字挑一个对象。
误导性名称真正可怕的例子,是用小写字母l和大写字母O作为变量
名,尤其是在组合使用的时候。当然,问题在于它们看起来完全像是常
量“壹”和“零”。
int a = l;if (O == l)
a = O1;
else
l = 01;
读者可能会认为这纯属虚构,但我们确曾见过充斥这类玩意的代
码。有一次,代码作者建议用不同字体写变量名,好显得更清楚些,不
过这种方案得要通过口头和书面传递给未来所有的开发者才行。后来,只是做了简单的重命名操作,就解决了问题,而且也没搞出别的事。2.4 做有意义的区分
如果程序员只是为满足编译器或解释器的需要而写代码,就会制造
麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会
随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现
在更正拼写错误后导致编译器出错的情况。[4]
光是添加数字系列或是废话远远不够,即便这足以让编译器满意。
如果名称必须相异,那其意思也应该不同才对。
以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的
名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线
索。试看:
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];}
}
如果参数名改为source和destination,这个函数就会像样许多。
废话是另一种没意义的区分。假设你有一个 Product 类。如果还有
一个 ProductInfo 或ProductData类,那它们的名称虽然不同,意思却无
区别。Info和Data就像a、an和the一样,是意义含混的废话。
注意,只要体现出有意义的区分,使用 a和the 这样的前缀就没错。
例如,你可能把 a用在域内变量,而把the用于函数参数[5]。但如果你已
经有一个名为zork的变量,又想调用一个名为theZork的变量,麻烦就来
了。
废话都是冗余。Variable一词永远不应当出现在变量名中。Table一
词永远不应当出现在表名中。NameString会比Name好吗?难道Name会
是一个浮点数不成?如果是这样,就触犯了关于误导的规则。设想有个
名为Customer的类,还有一个名为CustomerObject的类。区别何在呢?
哪一个是表示客户历史支付情况的最佳途径?
有个应用反映了这种状况。为当事者讳,我们改了一下,不过犯错
的代码的确就是这个样子:
getActiveAccount;
getActiveAccounts;
getActiveAccountInfo;
程序员怎么能知道该调用哪个函数呢?
如果缺少明确约定,变量 moneyAmount 就与 money 没区别,customerInfo 与 customer没区别,accountData与account没区别,theMessage也与message没区别。要区分名称,就要以读者能鉴别不同之
处的方式来区分。2.5 使用读得出来的名称
人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理
单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来
处理言语,若不善加利用,实在是种耻辱。
如果名称读不出来,讨论的时候就会像个傻鸟。“哎,这儿,鼻涕
阿三喜摁踢(bee cee arr three cee enn tee)[6]上头,有个皮挨死极翘
(pee ess zee kyew)[7]整数,看见没?”这不是小事,因为编程本就是
一种社会活动。
有家公司,程序里面写了个 genymdhms(生成日期,年、月、日、时、分、秒),他们一般读作“gen why emm dee aich emm ess”[8]。我有
个见字照读的恶习,于是开口就念“gen-yah-mudda-hims”。后来好些设
计师和分析师都有样学样,听起来傻乎乎的。我们知道典故,所以会觉
得很搞笑。搞笑归搞笑,实际是在强忍糟糕的命名。在给新开发者解释
变量的意义时,他们总是读出傻乎乎的自造词,而非恰当的英语词。比
较
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = 102;
...
};
和
class Customer {private Date generationTimestamp;
private Date modificationTimestamp;;
private final String recordId = 102;
...
};
现在读起来就像人话了:“喂,Mikey,看看这条记录!生成时间戳
(generation timestamp) [9]被设置为明天了!不能这样吧?”2.6 使用可搜索的名称
单字母名称和数字常量有个问题,就是很难在一大篇文字中找出
来。
找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦
了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图
而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会
逃过搜索,从而造成错误。
同样,e也不是个便于搜索的好变量名。它是英文中最常用的字
母,在每个程序、每段代码中都有可能出现。由此而见,长名称胜于短
名称,搜得到的名称胜于用自造编码代写就的名称。
窃以为单字母名称仅用于短方法中的本地变量。名称长短应与其作
用域大小相对应[N5]。若变量或常量可能在代码中多处使用,则应赋其
以便于搜索的名称。再比较
for (int j=0; j<34; j++) {
s += (t[j]4)5;
}
和
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] realDaysPerIdealDay;
int realTaskWeeks = (realdays WORK_DAYS_PER_WEEK);sum += realTaskWeeks;
}
注意,上面代码中的sum并非特别有用的名称,不过它至少搜得
到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多,而列表中也只剩下了
体现作者意图的名称。2.7 避免使用编码
编码已经太多,无谓再自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之
外(那算是正常的),还要再搞懂另一种编码“语言”。这对于解决问题
而言,纯属多余的负担。带编码的名称通常也不便发音,容易打错。
2.7.1匈牙利语标记法
在往昔名称长短很要命的时代,我们毫无必要地破坏了不编码的规
矩,如今后悔不迭。Fortran 语言要求首字母体现出类型,导致了编码的
产生。BASIC 早期版本只允许使用一个字母再加上一位数字。匈牙利语
标记法(Hungarian Notation,HN)将这种态势愈演愈烈。
在Windows的C语言API的时代,HN相当重要,那时所有名称要么
是个整数句柄,要么是个长指针或者void指针,要不然就是string的几种
实现(有不同的用途和属性)之一。那时候编译器并不做类型检查,程
序员需要匈牙利语标记法来帮助自己记住类型。
现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类
型。而且,人们趋向于使用更小的类、更短的方法,好让每个变量的定
义都在视野范围之内。
Java程序员不需要类型编码。对象是强类型的,代码编辑环境已经
先进到在编译开始前就侦测到类型错误的程度!所以,如今HN和其他
类型编码形式都纯属多余。它们增加了修改变量、函数或类的名称或类
型的难度。它们增加了阅读代码的难度。它们制造了让编码系统误导读
者的可能性。PhoneNumber phoneString;
类型变化时,名称并不变化!
2.7.2 成员前缀
也不必用 m_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。你应当使用某种可以高亮或用颜色标出成员的
编辑环境。
public class Part {
private String m_dsc; The textual description
void setName(String name) {
m_dsc = name;
}
}--------------------------------------------------------------------------------------
public class Part {
String description;
void setDescription(String description) {
this.description = description;
}
}
此外,人们会很快学会无视前缀(或后缀),只看到名称中有意义
的部分。代码读得越多,眼中就越没有前缀。最终,前缀变作了不入法
眼的废料,变作了旧代码的标志物。
2.7.3 接口和实现
有时也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是个接口,要用具体类来实
现。你怎么来命名工厂和具体类呢?IShapeFactory和ShapeFactory吗?
我喜欢不加修饰的接口。前导字母I被滥用到了说好听点是干扰,说难
听点根本就是废话的程度。我不想让用户知道我给他们的是接口。我就
想让他们知道那是个ShapeFactory。如果接口和实现必须选一个来编码
的话,我宁肯选择实现。ShapeFactoryImp,甚至是丑陋的
CShapeFactory,都比对接口名称编码来得好。2.8 避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题
经常出现在选择是使用问题领域术语还是解决方案领域术语时。
单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循
环计数器自然有可能被命名为i或j或k。(但千万别用字母l!)这是因
为传统上惯用单字母名称做循环计数器。然而,在多数其他情况下,单
字母名称不是个好选择;读者必须在脑中将它映射为真实概念。仅仅是
因为有了a和b,就要取名为c,实在并非像样的理由。
程序员通常都是聪明人。聪明人有时会借脑筋急转弯炫耀其聪明。
总而言之,假使你记得r代表不包含主机名和图式(scheme)的小写字
母版url的话,那你真是太聪明了。
聪明程序员和专业程序员之间的区别在于,专业程序员了解,明确
是王道。专业程序员善用其能,编写其他人能理解的代码。2.9 类名
类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser。避免使用Manager、Processor、Data或Info这样
的类名。类名不应当是动词。2.10 方法名
方法名应当是动词或动词短语,如postPayment、deletePage或
save。属性访问器、修改器和断言应该根据其值命名,并依Javabean标
准[10]加上get、set和is前缀。
string name = employee.getName;
customer.setName(mike);
if (paycheck.isPosted)...
重载构造器时,使用描述了参数的静态工厂方法名。例如,Complex fulcrumPoint = Complex.FromRealNumber(23.0);
通常好于
Complex fulcrumPoint = new Complex(23.0);
可以考虑将相应的构造器设置为private,强制使用这种命名手段。2.11 别扮可爱
如果名称太耍宝,那就只有同作者一般有幽默感的人才能记得住,而且还是在他们记得那个笑话的时候才行。谁会知道名为
HolyHandGrenade[11]的函数是用来做什么的呢?没错,这名字挺伶
俐,不过DeleteItems[12]或许是更好的名称。宁可明确,毋为好玩。
扮可爱的做法在代码中经常体现为使用俗话或俚语。例如,别用
whack( )[13]来表示kill( )。别用eatMyShorts( )[14]这类与文化紧密相关的
笑话来表示abort( )。
言到意到。意到言到。2.12 每个概念对应一个词
给每个抽象概念选一个词,并且一以贯之。例如,使用fetch、retrieve和get来给在多个类中的同种方法命名。你怎么记得住哪个类中
是哪个方法呢?很悲哀,你总得记住编写库或类的公司、机构或个人,才能想得起来用的是哪个术语。否则,就得耗费大把时间浏览各个文件
头及前面的代码。
Eclipse和IntelliJ之类现代编程环境提供了与环境相关的线索,比如
某个对象能调用的方法列表。不过要注意,列表中通常不会给出你为函
数名和参数列表编写的注释。如果参数名称来自函数声明,你就太幸运
了。函数名称应当独一无二,而且要保持一致,这样你才能不借助多余
的浏览就找到正确的方法。
同样,在同一堆代码中有controller,又有manager,还有driver,就
会令人困惑。DeviceManager和Protocol-Controller 之间有何根本区别?
为什么不全用 controllers 或 managers?他们都是Drivers吗?这种名称,让人觉得这两个对象是不同类型的,也分属不同的类。
对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降
福音。2.13 别用双关语
避免将同一单词用于不同目的。同一术语用于不同概念,基本上就
是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有
add方法。只要这些add方法的参数列表和返回值在语义上等价,就一切
顺利。
但是,可能会有人决定为“保持一致”而使用add这个词来命名,即
便并非真的想表示这种意思。比如,在多个类中都有add方法,该方法
通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一
个方法,把单个参数放到群集(collection)中。该把这个方法叫做 add
吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应
该用 insert或append之类词来命名才对。把该方法命名为add,就是双关
语了。
代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能
一目尽览,而不必殚精竭虑地研究。我们想要那种大众化的作者尽责写
清楚的平装书模式;我们不想要那种学者挖地三尺才能明白个中意义的
学院派模式。2.14 使用解决方案领域名称
记住,只有程序员才会读你的代码。所以,尽管用那些计算机科学
(Computer Science,CS)术语、算法名、模式名、数学术语吧。依据
问题所涉领域来命名可不算是聪明的做法,因为不该让协作者老是跑去
问客户每个名称的含义,其实他们早该通过另一名称了解这个概念了。
对于熟悉访问者(VISITOR)模式的程序来说,名称
AccountVisitor 富有意义。哪个程序员会不知道 JobQueue的意思呢?程
序员要做太多技术性工作。给这些事取个技术性的名称,通常是最靠谱
的做法。2.15 使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉
问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域
专家了。
优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题
领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域
的名称。2.16 添加有意义的语境
很少有名称是能自我说明的——多数都不能。反之,你需要用有良
好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这
么做,给名称添加前缀就是最后一招了。
设想你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量。当它们搁一块儿的时候,很明确是构成了一个地
址。不过,假使只是在某个方法中看见孤零零一个state变量呢?你会理
所当然推断那是某个地址的一部分吗?
可以添加前缀addrFirstName、addrLastName、addrState等,以此提
供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为Address的类。这样,即便是编译器也会知道这
些变量隶属某个更大的概念了。
看看代码清单2-1中的方法。以下变量是否需要更有意义的语境
呢?函数名仅给出了部分语境;算法提供了剩下的部分。遍览函数后,你会知道number、verb和pluralModifier这三个变量是“测估”信息的一部
分。不幸的是这语境得靠读者推断出来。第一眼看到这个方法时,这些
变量的含义完全不清楚。
代码清单2-1 语境不明确的变量
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {number = no;
verb = are;
pluralModifier = s;
} else if (count == 1) {
number = 1;
verb = is;
pluralModifier = ;
} else {
number = Integer.toString(count);
verb = are;
pluralModifier = s;
}
String guessMessage = String.format(
There %s %s %s%s, verb, number, candidate, pluralModifier);
print(guessMessage);
}
上列函数有点儿过长,变量的使用贯穿始终。要分解这个函数,需
要创建一个名为GuessStatisticsMessage的类,把三个变量做成该类的成
员字段。这样它们就在定义上变作了GuessStatisticsMessage的一部分。
语境的增强也让算法能够通过分解为更小的函数而变得更为干净利落。
(如代码清单2-2所示。)
代码清单2-2 有语境的变量
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
There %s %s %s%s,verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters;
} else if (count == 1) {
thereIsOneLetter;
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = are;
pluralModifier = s;
}
private void thereIsOneLetter {
number = 1;
verb = is;
pluralModifier = ;
}
private void thereAreNoLetters {
number = no;verb = are;
pluralModifier = s;
}
}2.17 不要添加没用的语境
设若有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在
其中给每个类添加GSD前缀就不是什么好点子。说白了,你是在和自己
在用的工具过不去。输入G,按下自动完成键,结果会得到系统中全部
类的列表,列表恨不得有一英里那么长。这样做聪明吗?为什么要搞得
IDE没法帮助你?
再比如,你在GSD应用程序中的记账模块创建了一个表示邮件地址
的类,然后给该类命名为GSDAccountAddress。稍后,你的客户联络应
用中需要用到邮件地址,你会用GSDAccountAddress吗?这名字听起来
没问题吗?在这17个字母里面,有10个字母纯属多余和与当前语境毫无
关联。
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语
境。
对于Address类的实体来说,accountAddress和customerAddress都是
不错的名称,不过用在类名上就不太好了。Address是个好类名。如果
需要与MAC地址、端口地址和Web地址相区别,我会考虑使用
PostalAddress、MAC和URI。这样的名称更为精确,而精确正是命名的
要点。2.18 最后的话
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。与
其说这是一种技术、商业或管理问题,还不如说是一种教学问题。其结
果是,这个领域内的许多人都没能学会做得很好。
我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果
名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方
法名。我们使用现代工具对付这些细节,好让自己集中精力于把代码写
得就像词句篇章、至少像是表和数据结构(词句并非总是呈现数据的最
佳手段)。改名可能会让某人吃惊,就像你做到其他代码改善工作一
样。别让这种事阻碍你的前进步伐。
不妨试试上面这些规则,看你的代码可读性是否有所提升。如果你
是在维护别人写的代码,使用重构工具来解决问题。效果立竿见影,而
且会持续下去。
[1].译注:即表示已标记的4。
[2].译注:即hypotenuse的缩写。
[3].原注:如后文提到的,即便容器就是个List,最好也别在名称中写出
容器类型名。
[4].原注:例如,就因为class已有他用,就给一个变量命名为klass,这
真是可怕的做法。
[5].原注:鲍勃大叔惯于在C++中这样做,但后来放弃了,因为现代IDE
使这种做法变得没必要了。
[6].译注:BCR3CNT的读音。[7].译注:PSZQ的读音。
[8].译注:YMDHMS的读音。
[9].译注:读到generation timestamp时,立刻就能与代码中的
generationTimestamp变量对应上。
[10].原注:http:java.sun.comproductsjavabeansdocsspec.html。
[11].译注:意为“圣手手雷”。
[12].译注:意为“删除条目”。
[13].美俚,劈砍。
[14].美俚,去死吧。第3章 函数在编程的早年岁月,系统由程序和子程序组成。后来,在Fortran和
PL1的年代,系统由程序、子程序和函数组成。如今,只有函数存活下
来。函数是所有程序中的第一组代码。本章将讨论如何写好函数。
请看代码清单3-1。在FitNesse[1]中,很难找到长函数,不过我还是搜寻到一个。它不光长,而且代码也很复杂,有大量字符串、怪异而不
显见的数据类型和API。花3分钟时间,看能读懂多少?
代码清单3-1 HtmlUtil.java(FitNesse 20070619)
public static String testableHtml(
PageData pageData,boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage = pageData.getWikiPage;
StringBuffer buffer = new StringBuffer;
if (pageData.hasAttribute(Test)) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler.getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append(!include -setup .)
.append(pagePathName)
.append(\n);
}
}
WikiPage setup =
PageCrawlerImpl.getInheritedPage(SetUp, wikiPage);
if (setup != null) {WikiPagePath setupPath =
wikiPage.getPageCrawler.getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append(!include -setup .)
.append(setupPathName)
.append(\n);
}
}
buffer.append(pageData.getContent);
if (pageData.hasAttribute(Test)) {
WikiPage teardown =
PageCrawlerImpl.getInheritedPage(TearDown, wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler.getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append(\n)
.append(!include -teardown .)
.append(tearDownPathName)
.append(\n);
}
if (includeSuiteSetup) {
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_TEARDOWN_NAME,wikiPage);if (suiteTeardown != null) {
WikiPagePath pagePath =
suiteTeardown.getPageCrawler.getFullPath
(suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append(!include -teardown .)
.append(pagePathName)
.append(\n);
}
}
}
pageData.setContent(buffer.toString);
return pageData.getHtml;
}
搞懂这个函数了吗?大概没有。有太多事发生,有太多不同层级的
抽象。奇怪的字符串和函数调用,混以双重嵌套、用标识来控制的if语
句等,不一而足。
不过,只要做几个简单的方法抽离和重命名操作,加上一点点重
构,就能在9行代码之内搞掂(如代码清单3-2所示)。用3分钟阅读以
下代码,看你能理解吗?
代码清单3-2 HtmlUtil.java(重构之后)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
boolean isTestPage = pageData.hasAttribute(Test);
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage;StringBuffer newPageContent = new StringBuffer;
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent);
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString);
}
return pageData.getHtml;
}
除非你正在研究 FitNesse,否则就理解不了所有细节。不过,你大
概能明白,该函数包含把一些设置和拆解页放入一个测试页面,再渲染
为HTML的操作。如果你熟悉JUnit[2],或许会想到,该函数归属于某个
基于Web的测试框架。而且,这当然没错。从代码清单3-2中获得信息
很容易,而代码清单3-1则晦涩难明。
是什么让代码清单3-2易于阅读和理解?怎么才能让函数表达其意
图?该给函数赋予哪些属性,好让读者一看就明白函数是属于怎样的程
序?3.1 短小
函数的第一规则是要短小。第二条规则是还要更短小。我无法证明
这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近40年来,我写过各种不同大小的函数。我写过令人憎恶的长达3000行
的厌物,也写过许多100行到300行的函数,我还写过20行到30行的。经
过漫长的试错,经验告诉我,函数就该小。
在20世纪80年代,我们常说函数不该长于一屏。当然,说这话的时
候,VT100屏幕只有24行、80列,而编辑器就得先占去4行空间放菜
单。如今,用上了精致的字体和宽大的显示器,一屏里面可以显示100
行,每行能容纳150个字符。每行都不应该有150个字符那么长。函数也
不该有100行那么长,20行封顶最佳。
函数到底该有多长?1991年,我去Kent Beck位于奥勒冈州
(Oregon)的家中拜访。我们坐到一起写了些代码。他给我看一个叫做
Sparkle(火花闪耀)的有趣的JavaSwing小程序。程序在屏幕上描画电
影Cinderella(《灰姑娘》)中仙女用魔棒造出的那种视觉效果。只要移
动鼠标,光标所在处就会爆发出一团令人欣喜的火花,沿着模拟重力场
划落到窗口底部。肯特给我看代码的时候,我惊讶于其中那些函数尺寸
之小。我看惯了Swing程序中长度数以里计的函数。但这个程序中每个
函数都只有两行、三行或四行长。每个函数都一目了然。每个函数都只
说一件事。而且,每个函数都依序把你带到下一个函数。这就是函数应
该达到的短小程度![3]
函数应该有多短小?通常来说,应该短于代码清单3-2中的函数!
代码清单3-2实在应该缩短成代码清单3-3这个样子。代码清单3-3 HtmlUtil.java(再次重构之后)
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml;
}
代码块和缩进
if语句、else语句、while语句等,其中的代码块应该只有一行。该
行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因
为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进
层级不该多于一层或两层。当然,这样的函数易于阅读和理解。3.2 只做一件事
代码清单3-1显然想做好几件事。它创建缓冲区、获取页面、搜索
继承下来的页面、渲染路径、添加神秘的字符串、生成HTML,如此等
等。代码清单3-1手忙脚乱。而代码清单3-3则只做一件简单的事。它将
设置和拆解包纳到测试页面中。
过去30年以来,以下建议以不同形式一再出现:
函数应该做一件事。做好这件事。只做这一件事。
问题在于很难知道那件该做的事是什么。代码清单3-3只做了一件
事,对吧?其实也很容易看作是三件事:
(1)判断是否为测试页面;
(2)如果是,则容纳进设置和分拆步骤;
(3)渲染成HTML。那件事是什么?函数是做了一件事呢,还是做了三件事?注意,这
三个步骤均在该函数名下的同一抽象层上。可以用简洁的TO[4]起头段
落来描述这个函数:
TO RenderPageWithSetupsAndTeardowns, we check to see whether the
page is a test page and if so, we include the setups and teardowns. In either
case we render the page in HTML。
(要RenderPageWithSetupsAndTeardowns,检查页面是否为测试
页,如果是测试页,就容纳进设置和分拆步骤。无论是否测试页,都渲
染成HTML)
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只
做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名
称)拆分为另一抽象层上的一系列步骤。
代码清单3-1明显包括了处于多个不同抽象层级的步骤。显然,它所做的不止一件事。即便是代码清单3-2也有两个抽象层,这已被我们
将其缩短的能力所证明。然而,很难再将代码清单3-3做有意义的缩
短。可以将if语句拆出来做一个名为includeSetupAndTeardonws
IfTestpage的函数,但那只是重新诠释代码,并未改变抽象层级。
所以,要判断函数是否不止做了一件事,还有一个方法,就是看是
否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现[G34]。
函数中的区段
请看代码清单4-7。注意,generatePrimes函数被切分为
declarations、initializations和sieve等区段。这就是函数做事太多的明显
征兆。只做一件事的函数无法被合理地切分为多个区段。3.3 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。一
眼就能看出,代码清单3-1 违反了这条规矩。那里面有 getHtml( )等位于
较高抽象层的概念,也有 String pagePathName =
PathParser.render(pagePath)等位于中间抽象层的概念,还有.append(\n)
等位于相当低的抽象层的概念。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个
表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节
与基础概念混杂,更多的细节就会在函数中纠结起来。
自顶向下读代码:向下规则
我们想要让代码拥有自顶向下的阅读顺序。[5]我们想要让每个函数
后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能偱抽象层级向下阅读了。我把这叫做向下规则。
换一种说法。我们想要这样读程序:程序就像是一系列 TO起头的
段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续
TO起头段落。
To include the setups and teardowns, we include setups, then we include
the test page content, and then we include the teardowns.(要容纳设置和分
拆步骤,就先容纳设置步骤,然后纳入测试页面内容,再纳入分拆步
骤。)
To include the setups, we include the suite setup if this is a suite, then we
include the regular setup.(要容纳设置步骤,如果是套件,就纳入套件
设置步骤,然后再纳入普通设置步骤。)To include the suite setup, we search the parent hierarchy for
the“SuiteSetUp”page and add an include statement with the path of that
page.(要容纳套件设置步骤,先搜索“SuiteSetUp”页面的上级继承关
系,再添加一个包括该页面路径的语句。)
To search the parent. . (要搜索……)
程序员往往很难学会遵循这条规则,写出只停留于一个抽象层级上
的函数。尽管如此,学习这个技巧还是很重要。这是保持函数短小、确
保只做一件事的要诀。让代码读起来像是一系列自顶向下的TO起头段
落是保持抽象层级协调一致的有效技巧。
看看本章末尾的代码清单3-7。它展示了遵循这条原则重构的完整
testableHtml函数。留意每个函数是如何引出下一个函数,如何保持在同
一抽象层上的。3.4 switch语句
写出短小的switch语句很难[6]。即便是只有两种条件的switch语句
也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语
句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不
过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重
复。当然,我们利用多态来实现这一点。
请看代码清单3-4。它呈现了可能依赖于雇员类型的仅仅一种操
作。
代码清单3-4 Payroll.java
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责
原则(Single Responsibility Principle[7], SRP),因为有好几个修改它的
理由。第四,它违反了开放闭合原则(Open Closed Principle[8],OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦
的可能是到处皆有类似结构的函数。例如,可能会有
isPayday(Employee e, Date date),或
deliverPay(Employee e, Money pay),如此等等。它们的结构都有同样的问题。
该问题的解决方案(如代码清单3-5所示)是将switch语句埋到抽象
工厂[9]底下,不让任何人看到。该工厂使用switch语句为Employee的派
生物创建适当的实体,而不同的函数,如calculatePay、isPayday和
deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对
象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍
[G23]。当然也要就事论事,有时我也会部分或全部违反这条规矩。
代码清单3-5 Employee与工厂
public abstract class Employee {
public abstract boolean isPayday;
public abstract Money calculatePay;
public abstract void deliverPay(Money pay);
}-----------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws
InvalidEmployeeType;
}-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws
InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
}3.5 使用描述性的名称
在代码清单3-7中,我把示例函数的名称从testableHtml改为
SetupTeardownIncluder.render。这个名称好得多,因为它较好地描述了
函数做的事。我也给每个私有方法取个同样具有描述性的名称,如
isTestable或includeSetupAndTeardownPages。好名称的价值怎么好评都
不为过。记住沃德原则:“如果每个例程都让你感到深合己意,那就是
整洁代码。”要遵循这一原则,泰半工作都在于为只做一件事的小函数
取个好名字。函数越短小、功能越集中,就越便于取个好名字。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称
好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约
定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个
能说清其功用的名称。
别害怕花时间取名字。你当尝试不同的名称,实测其阅读效果。在
Eclipse或IntelliJ等现代IDE中改名称易如反掌。使用这些IDE测试不同名
称,直至找到最具有描述性的那一个为止。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。
追索好名称,往往导致对代码的改善重构。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词
给函数命名。例如,includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage等。这些名
称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上
述函数序列,你就会自问:“includeTeardownPages、includeSuiteTeardownPages和includeTeardownPage又会如何?”这就是所谓“深合己意”了。3.6 函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函
数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足
够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也
不要这么做。
参数不易对付。它们带有太多概念性。所以我在代码范例中几乎不
加参数。比如,以StringBuffer为例,我们可能不把它作为实体变量,而
是当作参数来传递,那样的话,读者每次看到它都得要翻译一遍。阅读
模块所讲述的故事时,includeSetupPage( )要比
includeSetupPageInto(newPage-Content)易于理解。参数与函数名处在
不同的抽象层级,它要求你了解目前并不特别重要的细节(即那个
StringBuffer)。从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参
数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。如果只有一个参数,也不太困难。有两个参数,问题就麻烦多了。如果参数多于两个,测试覆盖所有可能值的组合简直让人生
畏。
输出参数比输入参数还要难以理解。读函数时,我们惯于认为信息
通过参数输入函数,通过返回值从函数中输出。我们不太期望信息通过
参数输出。所以,输出参数往往让人苦思之后才恍然大悟。
相较于没有参数,只有一个输入参数算是第二好的做法。
SetupTeardownInclude.render (pageData)也相当易于理解。很明显,我
们将渲染pageData对象中的数据。
3.6.1 一元函数的普遍形式
向函数传入单个参数有两种极普遍的理由。你也许会问关于那个参
数的问题,就像在boolean fileExists(MyFile)中那样。也可能是操作该
参数,将其转换为其他什么东西,再输出之。例如,InputStream
fileOpen(MyFile)把String类型的文件名转换为InputStream类型的返回
值。这就是读者看到函数时所期待的东西。你应当选用较能区别这两种
理由的名称,而且总在一致的上下文中使用这两种形式。
还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件
(event)。在这种形式中,有输入参数而无输出参数。程序将函数看作
是一个事件,使用该参数修改系统状态,例如void
passwordAttemptFailedNtimes(int attempts)。小心使用这种形式。应该让
读者很清楚地了解它是个事件。谨慎地选用名称和上下文语境。
尽量避免编写不遵循这些形式的一元函数,例如,void
includeSetupPageInto(StringBuffer pageText)。对于转换,使用输出参数
而非返回值令人迷惑。如果函数要对输入参数进行转换操作,转换结果
就该体现为返回值。实际上,StringBuffer transform(StringBuffer in)要比
void transform(StringBuffer out)强,即便第一种形式只简单地返回输参数也是这样。至少,它遵循了转换的形式。
3.6.2 标识参数
标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。
这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。
如果标识为true将会这样做,标识为false则会那样做!
在代码清单3-7中,我们别无选择,因为调用者已经传入了那个标
识,而我想把重构范围限制在该函数及该函数以下范围之内。方法调用
render(true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到
render(Boolean isSuite),稍许有点帮助,不过仍然不够。应该把该函数
一分为二:reanderForSuite( )和renderForSingleTest( )。
3.6.3 二元函数
有两个参数的函数要比一元函数难懂。例如,writeField(name)比
writeField(outputStream,name)[10]好懂。
尽管两种情况下意义都很清楚,但第一个只要扫一眼就明白,更好
地表达了其意义。第二个就得暂停一下才能明白,除非我们学会忽略第
一个参数。而且最终那也会导致问题,因为我们根本就不该忽略任何代
码。忽略掉的部分就是缺陷藏身之地。
当然,有些时候两个参数正好。例如,Point p = new Point(0,0);就相
当合理。笛卡儿点天生拥有两个参数。如果看到new Point(0),我们会倍
感惊讶。然而,本例中的两个参数却只是单个值的有序组成部分!而
output-Stream和name则既非自然的组合,也不是自然的排序。
即便是如 assertEquals(expected, actual)这样的二元函数也有其问
题。你有多少次会搞错actual和expected的位置呢?这两个参数没有自然
的顺序。expected在前,actual在后,只是一种需要学习的约定罢了。二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小
心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换成一
元函数。例如,可以把 writeField 方法写成outputStream的成员之一,从
而能这样用:outputStream.writeField(name)。或者,也可以把
outputStream写成当前类的成员变量,从而无需再传递它。还可以分离
出类似 FieldWriter的新类,在其构造器中采用outputStream,并且包含
一个write方法。
3.6.4 三元函数
有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问
题都会加倍体现。建议你在写三元函数前一定要想清楚。
例如,设想assertEquals有三个参数:assertEquals(message, expected,actual)。有多少次,你读到 message,错以为它是expected呢?我就常栽
在这个三元函数上。实际上,每次我看到这里,总会绕半天圈子,最后
学会了忽略message参数。
另一方面,这里有个并不那么险恶的三元函数:assertEquals(1.0,amount, .001)。虽然也要费点神,还是值得的。得到“浮点值的等值是相
对而言”的提示总是好的。
3.6.5 参数对象
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参
数应该封装为类了。例如,下面两个声明的差别:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则
并非如此。当一组参数被共同传递,就像上例中的x和y那样,往往就是该有自己名称的某个概念的一部分。
3.6.6 参数列表
有时,我们想要向函数传入数量可变的参数。例如,String.format
方法:
String.format(%s worked %.2f hours., name, hours);
如果可变参数像上例中那样被同等对待,就和类型为List的单个参
数没什么两样。这样一来,String.formate实则是二元函数。下列
String.format的声明也很明显是二元的:
public String format(String format, Object... args)
同理,有可变参数的函数可能是一元、二元甚至三元。超过这个数
量就可能要犯错了。
void monad(Integer... args);
void dyad(String name, Integer... args);
void triad(String name, int count, Integer... args);
3.6.7 动词与关键字
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和
意图。对于一元函数,函数和参数应当形成一种非常良好的动词名词
对形式。例如,write(name)就相当令人认同。不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我
们,“name”是一个“field”。
最后那个例子展示了函数名称的关键字(keyword)形式。使用这
种形式,我们把参数的名称编码成了函数名。例如,assertEqual改成
assertExpectedEqualsActual(expected, actual)可能会好些。这大大减轻了
记忆参数顺序的负担。3.7 无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起
来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它
会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都
是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
以代码清单3-6中看似无伤大雅的函数为例。该函数使用标准算法
来匹配 userName和password。如果匹配成功,返回 true,如果失败则返
回 false。但它会有副作用。你知道问题所在吗?
代码清单3-6 UserValidator.java
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword;
String phrase = cryptographer.decrypt(codedPhrase, password);
if (Valid Password.equals(phrase)) {
Session.initialize;
return true;
}
}
return false;
}}
当然了,副作用就在于对Session.initialize( )的调用。checkPassword
函数,顾名思义,就是用来检查密码的。该名称并未暗示它会初始化该
次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。
这一副作用造出了一次时序性耦合。也就是说,checkPassword只能
在特定时刻调用(换言之,在初始化会话是安全的时候调用)。如果在
不合适的时候调用,会话数据就有可能沉默地丢失。时序性耦合令人迷
惑,特别是当它躲在副作用后面时。如果一定要时序性耦合,就应该在
函数名称中说明。在本例中,可以重命名函数为
checkPasswordAndInitializeSession,虽然那还是违反了“只做一件事”的
规则。
输出参数
参数多数会被自然而然地看作是函数的输入。如果你编过好些年程
序,我担保你一定被用作输出而非输入的参数迷惑过。例如:
appendFooter(s);
这个函数是把s添加到什么东西后面吗?或者它把什么东西添加到
了s后面?s是输入参数还是输出参数?稍许花点时间看看函数签名:
public void appendFooter(StringBuffer report)
事情清楚了,但付出了检查函数声明的代价。你被迫检查函数签
名,就得花上一点时间。应该避免这种中断思路的事。
在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面
向对象语言中对输出参数的大部分需求已经消失了,因为this也有输出
函数的意味在内。换言之,最好是这样调用appendFooter:
report.appendFooter;
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。3.8 分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该
修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混
乱。看看下面的例子:
public boolean set(String attribute, String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个
属性则返回false。这样就导致了以下语句:
if (set(username, unclebob))...
从读者的角度考虑一下吧。这是什么意思呢?它是在问username属
性值是否之前已设置为unclebob吗?或者它是在问username属性值是否
成功设置为unclebob呢?从这行调用很难判断其含义,因为set是动词还
是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形
容词。该语句读起来像是说“如果username属性值之前已被设置为
uncleob”,而不是“设置username属性值为unclebob,看看是否可行,然
后……”。要解决这个问题,可以将 set 函数重命名为
setAndCheckIfExists,但这对提高 if 语句的可读性帮助不大。真正的解
决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists(username)) {
setAttribute(username, unclebob);...
}3.9 使用异常替代返回错误码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓
励了在if语句判断中把指令当作表达式使用。
if (deletePage(page) == E_OK)
这不会引起动词形容词混淆,但却导致更深层次的嵌套结构。当
返回错误码时,就是在要求调用者立刻处理错误。
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey== E_OK){
logger.log(page deleted);
} else {
logger.log(configKey not deleted);
}
} else {
logger.log(deleteReference from registry failed);
}
} else {
logger.log(delete failed);
return E_ERROR;
}
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主
路径代码中分离出来,得到简化:
try {deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey);
}
catch (Exception e) {
logger.log(e.getMessage);
}
3.9.1 抽离TryCatch代码块
Trycatch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正
常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外
形成函数。
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception
{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey);
}private void logError(Exception e) {
logger.log(e.getMessage);
}
在上例中,delete函数只与错误处理有关。很容易理解然后就忽略
掉。deletePageAndAllReference函数只与完全删除一个page有关。错误
处理可以忽略掉。有了这样美妙的区隔,代码就更易于理解和修改了。
3.9.2 错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函
数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中
存在,它就该是这个函数的第一个单词,而且在catchfinally代码块后面
也不该有其他内容。
3.9.3 Error.java依赖磁铁
返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。
public enum Error {
OK,INVALID,NO_SUCH,LOCKED,OUT_OF_RESOURCES,WAITING_FOR_EVENT;
}
这样的类就是一块依赖磁铁(dependency magnet);其他许多类都
得导入和使用它。当Error枚举修改时,所有这些其他的类都需要重新编
译和部署。[11]这对Error类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧
的错误码,而不添加新的。
使用异常替代错误码,新异常就可以从异常类派生出来,无需重新
编译或重新部署[12]。3.10 别重复自己
[13]
回头仔细看看代码清单3-1,你会注意到,有个算法在SetUp、SuiteSetUp、TearDown和SuiteTearDown中总共被重复了 4 次。识别重复
不太容易,因为这 4 次重复与其他代码混在一起,而且也不完全一样。
这样的重复还是会导致问题,因为代码因此而臃肿,且当算法改变时需
要修改4处地方。而且也会增加4次放过错误的可能性。
使用代码清单3-7中的include方法修正了这些重复。再读一遍那段
代码,你会注意到,整个模块的可读性因为重复的消除而得到了提升。
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。例如,全部考德(Codd)[14]数据库范式都是为
消灭数据重复而服务。再想想看,面向对象编程是如何将代码集中到基
类,从而避免了冗余。面向方面编程(Aspect Oriented
Programming)、面向组件编程(Component Oriented Programming)多
少也都是消除重复的一种策略。看来,自子程序发明以来,软件开发领
域的所有创新都是在不断尝试从源代码中消灭重复。3.11 结构化编程
有些程序员遵循Edsger Dijkstra的结构化编程规则[15]。Dijkstra认
为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵
循这些规则,意味着在每个函数中只该有一个return语句,循环中不能
有break或continue语句,而且永永远远不能有任何goto语句。
我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益
不大。只有在大函数中,这些规则才会有明显的好处。
所以,只要函数保持短小,偶尔出现的return、break或continue语句
没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto只
在大函数中才有道理,所以应该尽量避免使用。3.12 如何写出这样的函数
写代码和写别的东西很像。在写论文或文章时,你先想什么就写什
么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心
目中的样子。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过
长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一
套单元测试,覆盖每行丑陋的代码。
然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和
重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数。
我并不从一开始就按照规则写函数。我想没人做得到。3.13 小结
每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设
计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到
那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可
怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言
设计的艺术。
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使
用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用
来讲那个故事。那种领域特定语言的一个部分,就是描述在系统中发生
的各种行为的函数层级。在一种狡猾的递归操作中,这些行为使用它们
定义的与领域紧密相关的语言讲述自己那个小故事。
本章所讲述的是有关编写良好函数的机制。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。不过永远别忘记,真
正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到
一起,形成一种精确而清晰的语言,帮助你讲故事。3.14 SetupTeardownIncluder程序
代码清单3-7 SetupTeardownIncluder.java
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.;
public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite)
throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pa ......
您现在查看是摘要介绍页, 详见PDF附件(10033KB,782页)。





