代码整洁之道读书笔记

  • by fangpc

序言部分

  • "神在细节之中" — 建筑师路德维希
  • 5S哲学(精益)
  • 整理(Seiri):搞清楚事物之所在——通过恰当地命名之类的手段——至关重要
  • 整顿(Seiton):每段代码都应该在你希望它所在的地方——如果不在那里,就需要重构了
  • 清楚(Seiso):或谓清洁,清理工作地拉线、油污和边角废料
  • 清洁(Seiketsu):或谓标准化,开发组内使用统一的代码风格和实践手段
  • 身美(Shisuke):或谓纪律(自律),在实践中贯彻规程,并时时体现于个人工作上,而且要乐于改进

代码猴子和童子军军规

  • 沉迷测试(Test Obsessed)
  • 不要做代码猴子(Do not be a code monkey)

第一章——整洁代码

1、代码永不消失

代码就是衔接人脑理解需求的含糊性和机器指令的精确性的桥梁。哪怕未来会有对现在高级编程语言的再一次抽象——但这个抽象规范自身仍旧是代码。所以既然代码会一直存在下去,且自己都干了程序员这一行了,就好好的对待它吧。

2、读远比写多

当你录下你平时的编码的过程,回放时,你会发现读代码所花的时间远比写的多,甚至超过 10:1。所以整洁的代码会增加可读性。

3、稍后等于永不

糟糕的代码会导致项目的难以维护。而当你正在写糟糕的代码的时候,心里却圣洁的想:“有朝一日再回来整理”。但现实是残酷的,正如勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)

4、精益求精

写代码就跟写文章一样,先自由发挥,再细节打磨。追求完美。

第二章——有意义的命名

1.名副其实并且也命名有意义。 说起来很简单。选个好名字需要花时间,但省下的时间比花掉的多。注意命名,一旦有好的命名,就换掉旧的。

int d;// 消失的时间,以日计。

int elapsedTimeInDays;

2.避免误导。比如不是List类型,就不要用个accountList来命名,这样形成误导。

3.做有意的区分。

Public static void copyChars(char a1[],char a2[]){
for(int i=0;i<a1.length;i++){
a2[i]=a1[i];
}
}

如果参数名称改为source和destination ,这个函数就会像样很多。废话都是冗余的,Variable一词 永远不应当出现在变量名中。Table一词永远不应当出现在表名中。NameString 会比 Name好吗,难道Name 会是一个浮点数不成?如有一个Customer的类,有又一个CustomerObject的类。是不是就凌乱了。

4.使用便于搜索的的名称

单个字母或者数字常量是很难在一大堆文章中找出来。比如字母e,它是英文中最常用的字母。长名胜于短名称,搜得到的名称胜于自编的名称。 窃以为单字母的名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。

5.类名应该是名词或短语,像Customer,Account,避免使用Manager,Processor,Data或者Info这样的类名。类名不应当是动词。方法名应该是动词或动词短语,如postPayment ,deletePage或Save,属性访问、修改和断言应该根据其值来命名,并加上get,set,is这些前缀。

6.别扮可爱,耍宝,比如谁知道HolyHandGrenada 函数是干什么的,没错这个名字挺伶俐,但是不过DeleteItems或许是更好的名字。

7.每个概念对应一个词。并且一以贯之。

在一堆代码中有Controller,又有manager,driver。就会令人困惑。比如DeviceManager和Protal-Controller之间又什么本质区别?

第三章——函数

1.短小:函数要短小,更短小。极长的函数逻辑是不清晰的,读很长的函数往往会被很多琐碎的细节分神,好的函数应该是模块化的。
2.只做一件事:一个函数应该尽量只做并做好一件事。
3.向下规则:代码的阅读顺序应该是自顶向下的,要让每个函数后面都跟着位于下一抽象层级的函数。Logic -> Service -> Dao。
4.函数命名:
- 函数越短、功能越集中,就越便于取个好名字。
- 别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释要好。
- 命名方式要保持一致。
5.函数参数:零参数函数 > 单参数函数 > 双参数参数 > 三参数参数(避免),参数越少越好
- 测试友好
- 函数封闭性更好
- 标识参数丑陋不堪,骇人听闻
- 无副作用:如果一定要时序性耦合,就该在函数名称中说明。
- 分隔指令与询问:一个函数做一件事,代表一个动作,不易引起歧义。
- 抛异常代替返回错误码:把try-catch代码块抽出来形成检测函数
- DRY:杜绝重复代码,凡是需要用至少两次的代码,给它单独写成一个类或函数
6.结构化编程:
- Dijkstra结构化编程规则:每个函数、函数中的每个代码块都应该有一个入口、一个出口。每个函数只能有一个return语句,循环中不能有break或continue语句,永远不能由goto

第四章——注释

1.注释不能美化糟糕的代码:与其花时间编写解释你写出的糟糕代码的注释,不如花时间整理你那糟糕的代码。
2.用代码来阐述:用代码来解释意图而不是注释
3.好注释:
- 对意图的解释:某个决定后面的意图。
- 阐释:翻译参数或返回值,阐释性注释本身就有不正确的风险。当改变了参数或者返回值的时候记得更新阐释性注释。
- 警示:不要那么做,或者怎么做会有怎样的风险和结果
- TODO注释: 应该做,但是还没做。不要让TODO成为在系统中留下糟糕代码的借口。
4.坏注释:
- 喃喃自语
- 冗余注释
- 误导性注释
- 循规式注释

第五章——格式

1.概念间垂直方向上的间隔:代码中的空白行会增加代码的可读性。在封包声明、导入声明、每个函数之间都要用空白行隔开。
	@Service("userService")
public class UserServiceImpl implements IUserService {
@Resource
private UserDao userDao;
public User getUserById(int userId) {
// TODO Auto-generated method stub
return userDao.selectById(userId);
}
@Override
public List<User> selectUser(int start, int limit) {
// TODO Auto-generated method stub
return userDao.selectUser(start,limit);
}
@Override
public List<User> getUsersListPage(Page page) {
// TODO Auto-generated method stub
return userDao.getUsersListPage(page);
} }
	@Service("userService")
public class UserServiceImpl implements IUserService { @Resource
private UserDao userDao;
public User getUserById(int userId) {
// TODO Auto-generated method stub
return userDao.selectById(userId);
} @Override
public List<User> selectUser(int start, int limit) {
// TODO Auto-generated method stub
return userDao.selectUser(start,limit);
} @Override
public List<User> getUsersListPage(Page page) {
// TODO Auto-generated method stub
return userDao.getUsersListPage(page);
} }
2.垂直方向上的靠近:空白行隔开了概念,靠近的代码行则暗示它们之间的紧密联系。紧密相关的代码应该互相靠近。
3.垂直距离:
- 变量声明:变量声明应该尽可能在其使用的位置。
- 实体变量:在类的顶部声明,在设计良好的类中,实体变量应该被该类的大多数方法所用。
- 相关函数:调用函数和被调用函数放在一起,并且调用函数放在被调用函数上面。
	private String distributionAward(ReferralRankRecord referralRankRecord) {
Map<Integer, String> awardRules = JsonUtils.jsonToMap(awardsConfiguration,
new TypeReference<Map<Integer, String>>() {
}); List<Integer> rankingDemarcation = new ArrayList<>(awardRules.keySet());
int awardGradeIndex = binarySearchAward(rankingDemarcation, referralRankRecord.getRankingNumber());
if (awardGradeIndex > awardDemarcation && referralRankRecord.getCount() < 3) {
return noAward;
} return awardRules.get(awardGradeIndex);
} private int binarySearchAward(List<Integer> rankingDemarcation, int rankingNumber) {
int left = 0;
int right = rankingDemarcation.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (rankingDemarcation.get(mid) == rankingNumber) {
return rankingDemarcation.get(mid);
} else if (rankingDemarcation.get(mid) < rankingNumber) {
left = mid + 1;
} else {
right = mid - 1;
}
} return rankingDemarcation.get(right);
}
	- 概念相关:概念相关的代码应该放到一起。相关性越强,彼此之间的距离就应该越短。
- 自顶向下贯穿源代码模块的信息流,比如自上向下展示函数调用依赖顺序。
4.横向格式:
- 水平方向上的区隔与靠近:
* 空格字符可以把相关性较弱的事物分隔开
* 赋值操作周围加上空格字符用于强调赋值语句有两个确定而重要的要素:左边和右边。空格字符加强了分割效果。
	left=mid+1;
				left = mid + 1;
		- 函数名和左圆括号之间不加空格表明函数和其参数紧密相关,括号中的参数要隔开表明参数是相互分离的。
	private int binarySearchAward(List<Integer> rankingDemarcation, int rankingNumber)
	private int binarySearchAward (List<Integer> rankingDemarcation, int rankingNumber)
		- 水平对齐:
				public class ReferralRankRecord {

				    private int rankingNumber;          // 排名
private int userId; // 转介绍人userId
private int count; // 转介绍数量
private long latestUpdateTime; //最近一条转介绍记录更新时间
}
				public class ReferralRankRecord {

				    private int  rankingNumber;          // 排名
private int userId; // 转介绍人userId
private int count; // 转介绍数量
private long latestUpdateTime; //最近一条转介绍记录更新时间
}
		- 缩进:缩进可以帮助程序员快速跳过与当前关注的情形无关的范围,如果没有缩进,程序将变得无法阅读
* 类声明不缩进
* 类中方法相对该类缩进一个层级
* 方法实现相对于方法声明缩进一个层级
* 代码块的实现相对于其容器代码块缩进一个层级
* 扩展和缩进范围,避免范围层级坍塌到一行
- 团队规则:
* 同一个团队,同一种代码风格
* 把规则编写进IDE,一直沿用

第六章——对象和数据结构

1.数据抽象:数据抽象则是指对数据类型的定义和使用过程
- 以抽象形态表述数据,而不是暴露数据的细节,具象机动车和抽象机动车的例子,如果机动车改成以天然气为燃料,具象机动车接口需要做修改,而抽象机动车类不需要修改
			public interface Vehicle{
double getFuelTankCapacityInGallons();
double getGallonsofGasoline();
}
			public interface Vehicle{
double getPecentFuelRemaining();
}
	- 暴露抽象接口可以使依赖方无须了解数据具体实现就能操作数据本体。
- 如果上述的机动车改成以电力为能源,或者变成混动模式,抽象机动车类则也要做相应的修改?
- 不一定都需要暴露抽象的概念,如pecent,如果就是想单纯的取某个属性,如长方体的长、宽、高,人的身高、体重、生日这些具体的属性
2.数据、对象的反对称性
- 对象与数据结构之间的差异
* 对象把数据隐藏于抽象之后,暴露出操作数据的函数
* 数据结构暴露其具体数据,没有提供有意义的函数。
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。
- 过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。
3.得墨忒耳定律
- 对象O的M方法,可以访问/调用如下的:
* 对象O本身
* M方法中创建或实例化的任意对象
* 作为参数传递给M的对象
* 对象O直接的组件对象
- 一些比喻
* 不要和陌生人说话
* 在超市购完物付款时,是将钱包中的钱取出交给收银员,而不是直接把钱包交个收银员让收银员把钱拿出来
* 人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走
- 为什么要遵循这一定律
* 可以更改一个类,而无需更改许多其它的类
* 隐藏了某个类是如何工作的
* 改变调用方法,而无需改变其他的东西
* 让代码更少的耦合,主叫方法只耦合一个对象,而并非所有的内部依赖
* 更好的模拟现实世界,想象超市付款和命令狗行走的比喻
- 违反得墨忒耳定律的例子
* 链式调用——火车失事代码,这类连串的调用通常被认为是肮脏的风格,应该避免
```java
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
``` 但有时消除这些"."可能并不太合适
```java
getStudent().getParents().getBirthdays();
```
改成下面这样可能并不太好,并没有太大意义 ```java
getStudentParentsBirthdays()
``` Java8的stream操作也不难理解,更简洁?
			activityService.getPermittedActivitysByUserId(userId).stream()
.map(assistanceActivityWrapper::wrap).collect(Collectors.toList());
4.混杂:
- 一半是对象,一半是数据结构,这是一种糟糕的设计。它增加了添加新函数的难度,也增加了添加新数据结构的难度。
- 隐藏结构:
* 既然取得路径的目的是为了创建文件,那么不妨让 ctxt 对象来做这件事:
```java
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
```
- 数据传送对象
* 最为精炼的数据结构,是一个只有公共变量、没有函数的类
* 这种数据结构有时被称为 DTO,Data Transfer Objects,数据传送对象
* 最常见的是Bean结构,其拥有赋值器和取值器操作的私有变量。
				public class CommodityInfo {

				    private String title; //商品详情,产品参数,常见问题
private List<String> imageUrls; public String getTitle() {
return title;
} public void setTitle(String title) {
this.title = title;
} public List<String> getImageUrls() {
return imageUrls;
} public void setImageUrls(List<String> imageUrls) {
this.imageUrls = imageUrls;
}
}
		* Active Record:
** 是一种特殊的DTO形式,它拥有公共变量,通常也有save和find类似的可浏览的方法
** 但有些程序员在这类对象中塞入了业务规则方法,造成对象和数据结构的混合体
5.小结:
- 对象暴露行为,隐藏数据,所以便于添加新对象难于添加新行为
- 数据结构暴露数据,没有明显行为,便于添加新行为而难以添加新数据结构

五六章思考题

1.Lombok的副作用:
- @Data注解 编译时自动添加Setter、Getter、toString()、equals()和hashCode()方法,除了Setter、Getter外,其它方法不太能用到
- 使用@Data、@Getter、@Setter后get和set方法不显示了,不容易发现问题,isOpen,生成的get和set方法分别是isOpen()和getOpen,但有些框架如jackson默认的set方法是getIsOpen(),会报错
- @SneakyThrows 隐藏异常 将检查异常包装为运行时异常,对API调用者不友好,调用者不能显示的获知需要处理检查异常
3.带业务逻辑的 Getter/Setter 是不是好的?
- 不好,Getter/Setter单纯的取和设置某个属性的值就好,不要掺杂业务逻辑

第七章——错误处理

1.错误处理是必须的,但如果对错误的处理搞乱了原来代码的逻辑,就是处理不当或者是错误的做法。
2.使用错误异常而非返回码:
- 抛异常,调用者可以选择处理即捕获异常,也可以选择不处理即继续向上层抛出异常
- 使用返回码,调用者需要知道每个返回码代表的含义,写一堆if-else逻辑,代码不够整洁
- 使用异常可以使错误处理从主逻辑中分离出来,使主逻辑清晰
3.先写Try-Catch-Finally语句
- try代码块像是事务,无论try代码块中发生了什么,catch代码块将程序维持在一种持续状态
- 在编写可能抛出异常的代码时,最好先写try-catch-finally语句,能帮你定义代码的用户应该期待什么,无论try代码块中执行的代码出什么错都一样。
4.使用不可控异常(unchecked exception):
- 可控异常(checked exception)就是指在方法签名中标注异常。
- 可控异常的代价是违反开放闭合原则,你对较底层的代码修改时,可能会波及很多上层代码。
- 开放封闭原则Marting定义:
- “对于扩展是开放的”,这意味着模块的行为是可以扩展的,当应用程序的需求改变时,我们可以对其模块进行扩展,使其具有满足那些需求变更的新行为。换句话说,不可以改变模块的功能。
- 加入抽象类,底层便于扩展,高层代码可以保持不变。客户端依赖抽象类基类,因此提供任何一个具体子类给客户端都不会违背开放封闭原则
- 接口继承要好于实现继承。接口继承有强大的自适应能力。基于实现继承的,给继承顶部节点添加新成员的改动会影响到该层级结构下的所有成员,而接口要比类灵活的多。
- 依赖接口的最大优势是接口变化的可能性要比实现小很多,如果接口发生变化,客户端也必须要做相应的改动。
- 需求迭代带来的接口变化有时候是难免的
5.给出异常发生的环境说明:
- 抛出的每个异常,都应提供足够的环境说明,以便判断错误的来源
- 利用日志系统,传递足够多的信息给catch块,并记录下来
- 利于线上问题的排查
6.依调用者需要定义异常类:
- 在应用程序中定义异常类时,最重要的要考虑这些异常是如何被捕获的
- 对重复式处理方法的多种异常应进一步打包后抛出,而不是多个相同的catch?
- 对异常的处理若有固定流程,也应该打包?
7.避免NPE,从函数不返回null、不向函数传递null参开始

第八章——边界

1.接口调用者和接口提供者之间存在张力,第三方程序包和框架提供者追求普适性,而使用者追求定制化以满足特定需求
2.学习性测试的好处不只是免费
- 学习性测试时一种精确试验,帮助我们增进对API的理解
- 腾讯广点通API的沙盒工具
- 头条巨量引擎的广告预览工具
- 学习性测试不光免费,当第三方程序包发布了新版本,可以运行学习性测试,看看程序包的行为有没有改变,可能帮助我们确保第三方程序包按照我们想要的方式工作
3.学习使用log4j
4.使用尚不存在的代码:
- mock我们想要得到的接口和数据,好处之一是有助于保持客户代码集中于它该完成的工作,坏处可能是对接有问题
- 约定好API的逻辑,mock数据,并行开发,逻辑约定的越好,对接越无缝
5.整洁的边界:
- 边界上的改动,有良好的软件设计,无需巨大投入和重写即可进行修改
- 边界上的代码需要清晰的分割和定义期望的测试。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制
- 可以使用适配器模式将我们的接口转换为第三方提供的接口

第九章——单元测试

1.保持测试整洁:
- 测试代码和生产代码一样重要
2.测试带来的好处:
- 单元测试让代码可扩展、可维护、可复用
- 测试可以让你毫无顾虑的改进架构和设计
- 测试有利于防止生产代码腐败
3.测试代码越脏,生产代码就会越脏
4.整洁的测试:
- 可读性、可读性、可读性
- 构造-操作-检验(BUILD_OPERATE-CHECK)
- 第一个环节构造测试数据
- 第二个环节操作测试数据
- 第三个部分检验操作是否得到期望的结果
- 面向特定领域的测试语言:
- 测试API并非起初就被设计出来,而是由测试代码进行后续重构逐渐演进而来
- 开发者需要将测试代码重构为更简洁而具有表达力的形式
- 双重标准
- 与CPU效率或内存有关的问题,在生产环境不可以做,但是在测试环境中完全没有问题
- StringBuffer丑陋?
5.每个测试一个概念:
- 一个测试函数只测试一个概念
6.F.I.R.S.T
- 测试代码要够快,要频繁测试,以更早、更快的发现生产代码的bug
- 每个测试都应该相互独立,某个测试不应该为下一个测试设定条件。应该可以单独运行每个测试,以及以任何顺序运行测试
- 测试代码应该不挑环境,无论是在测试环境、生产环境还是没有网络的本地,在何时何地都应该可重复的运行
- 测试应该有布尔值输出,测试的结果不应该从日志或者对比文本等不直观的方式被展现出来
- 测试应该被及时编写,最好应该在编写生产代码之前编写测试代码

第十章——类

1.类的组织:
- 类应该从变量列表开始:
- 公共静态变量应该先出现,接着是私有实体变量、很少有公共变量
- 公共函数在变量列表之后,公共函数调用的私有工具函数跟在公共函数后面,符合自顶向下原则
- 封装:
- 保持变量和工具函数的私有性但并不执着于此
- 放松封装总是下策
2.类应该短小
- 用权责来衡量类的大小
- 单一权责原则(SRP):类或者模块应该有且仅有一个加以修改的理由
- 内聚:
- 类中的每个方法都应该操作一个或多个实体变量
- 内聚性高意味着类中的方法和变量相互依赖、互相结合成一个逻辑整体
3.为了修改而组织:
- 开放-闭合原则(OCP):对扩展开放,对修改封闭
- 隔离修改:借助接口和抽象类来隔离这些细节带来的影响

善用工具

  • 规范化自己的代码不能只靠意志,更要靠工具,少点个人风格,多点通用规矩,并学会使用CheckStyle工具。
  • 很难命名的变量、函数,找小伙伴商量下,别自己一个人憋着
  • 宁可变量名长,也不要让变量名短得让人无法揣测其含义
  • 避免重复(DRY),尽可能杜绝重复代码,凡是需要用至少两次的代码,给它单独写成一个类或函数
05-11 13:49