第二章 有意义的命名
-
名副其实
-
通过名称能看出变量代表的含义
int d;// 消逝的时间 int elapsedTimeInDays; int daysSinceCreation;
- 选择体现本意的名称能让人更容易理解和修改代码
public List<int[]> getThem() { List<int[]> list1 = new ArrayList<int[]>(); for (int[] x : theList) if (x[0] == 4) list1.add(x); return list1; }
-
疑问
- theList中是什么类型的东西
- theList 零下标代表什么
- 值4的意义是什么
- 返回的列表如何使用
-
示例说明:
- 比方说,我们在开发一种扫雷游戏,盘面名为
theList
的单元格列表,那就将其名称改为gameBoard
- 盘面上每个单元格都用一个简单数组表示,并且零下标代表一种状态值,当值为4时表示“已标记”
- 那么返回盘面上已标记的位置,我们称为
flaggedCells
- 比方说,我们在开发一种扫雷游戏,盘面名为
public List<int[]> getFlaggedCells() { List<int[]> flaggedCells = new ArrayList<int[]>(); for (int[] cell : gameBorad) if (cell[STATUS_VALUE] == FLAGGED) flaggedCells.add(cell); return flaggedCells; }
- 此种情况下,还可继续优化,不用int数组表示单元格,而是使用
Cell
类,Cell#isFlagged
代表表格是否被标记
public List<Cell> getFlaggedCells() { List<Cell> flaggedCells = new ArrayList<Cell>(); for (Cell cell : gameBorad) if (cell.isFlagged()) flaggedCells.add(cell); return flaggedCells; }
-
-
避免误导
XYZControllerForEfficientHandlingOfStrings
和XYZControllerForEfficientStoragelingOfStrings
-
做有意义的区分
- 很难区分调用哪个函数
getActiveAccount()
getActiveAccounts()
getActiveAccountInfo()
- 以下变量命名如缺少明确约定,该如何区分
moneyAmout
和money
customer
和customerInfo
accountData
和account
- 很难区分调用哪个函数
-
使用可搜索的名称
- 单字母名称和数字常量有个问题,很难在一大篇文字中找出来
- 而较长变量名称可以很方便被检索出
- 单字母名称仅应该用于短方法的本地变量,名称长短应与其作用域大小相对应
- 示例
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; }
第二章 类与接口
-
类名
- 类名和对象名是名词或名词短语
Customer
、WikiPage
、Account
和AddressParser
- 避免使用
Manager
、Processor
、Data
或Info
这样的类名
- 类名和对象名是名词或名词短语
-
方法名
-
方法名应当是动词或者动词短语
-
postPayment
、deletePage
或save
-
属性访问器、修改器和断言应根据其值命名,并依
Javabean
标准加上set
、get
和is
前缀 -
重载构造器时,建议使用静态工厂方法
-
Complex fulcruPoint = Complex.FromRealNumber(23.0); 通常好于 Complex fulcrumPoint = new Complex(23.0);
-
第三章 如何写好函数
-
函数的第一要则是要短小
-
第二要则还要更短小
-
函数应该做一件事。做好这件事。只做这一件事
- 如何判断函数是做了一件事还是多件事
- 函数所包含步骤都在函数名下的同一抽象层上
- 例子见代码整洁之道 3.2 33页
- 如何判断函数是做了一件事还是多件事
-
每个函数一个抽象层级
-
自顶向下读代码:向下规则
- 我们想要让代码拥有自顶向下的阅读顺序
- 我们想要让每个函数后面都更着位于下一抽象层级的函数
- 这样在查看函数列表时,就能循环抽象层级向下阅读了
public class Sports {
private Ball ball;
public void playBall(boolean flag) {
if(flag) {
playBasketball();
} else {
//todo
}
}
private void playBasketball() {
wearBasketballSuite();
warmUp();
play(new Basketball());
}
private void wearBasketballSuite() {
wearBasketballClothes();
wearBasketballShoes();
}
private void wearBasketballClothes() {
System.out.println("already wore basketball clothes");
}
private void wearBasketballShoes() {
System.out.println("already wore basketball shoes");
}
private void warmUp() {
//todo
}
private void play(Ball ball) {
//todo
}
}
-
抽离try/catch 代码块:try/catch代码块专职处理异常,将业务逻辑封装为新的函数,扔在try/catch代码块中,这样一来,错误处理和业务功能处理美妙地分割开来
-
使用异常代替错误码:当错误码定义为枚举或类时,如果该枚举或者类修改了,会导致依赖其的类需要重新构建和部署了;而如果使用异常代替错误码,就直接可以从异常类派生出来
别重复自己
- 面向对象编程将代码集中到基类,避免了冗余
- 面向方面编程、面向组件编程均是消除重复代码的一种策略
第四章 注释
第五章 格式
-
纵向格式
- 几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路通过空白行区隔开来
- 紧密联系的函数放在一起
- 被调用的函数应该放在执行调用的函数下面 —> 建立了自顶向下贯穿源代码模块的良好信息流
-
横向格式
- 每一行的字符数不宜太多
得墨忒耳律(The Law of Demeter)
模块不应了解它所操作对象的内部情况
该定律认为,类C的方法f只应该调用以下对象的方法(只跟朋友谈话,不与陌生人谈话)
- C
- 由f 创建的对象
- 作为参数传递给f的对象
- 由C的实体变量持有的对象
第六章 对象与数据结构
- 对象暴露行为,隐藏数据
- 数据结构暴露数据,而没有明显的行为
第七章 错误处理
-
使用异常而非使用返回码
-
使用不可控异常
-
给出异常发生的环境说明:充分的错误消息:失败的操作和失败类型
-
依调用者需要定义异常类
-
定义常规流程
- 当异常中也需要处理业务逻辑时,可以将try …catch 中的逻辑封装为函数
- 这种手法叫作特例模式
-
别返回null值:如果方法返回null值,请抛出异常,或者使用特例对象来代替
- 这样就不用频繁地检查方法的返回值是否为空
- 避免空指针异常
public List<Stirng> getUserNameList() {
List<String> userNames = userDao.getUserNameList();
if (Collections.isEmpty(userNames)) {
return Collections.emptyList();
}
return userNames;
}
- 禁止向方法传入null,除非方法要求这样做
将错误处理隔离看待,独立于主要逻辑之外,这样的代码强固而整洁
第八章 边界
- 使用第三方代码时,可以抽象自己的接口,通过接口调用第三方代码,降低应用与第三方耦合
- 当第三方代码升级或功能改变时,通过调整包装的接口来实现相同的功能
第十章 类
标准的Java约定:
-
类应该从一组变量列表开始,顺序如下
- 公共静态常量
- 私有静态变量
- 私有实体变量
-
公共函数跟在变量列表之后
- 某个公共函数调用的私有函数紧随在该公共函数后面
- 这才符合自顶向下原则
-
类应该短小
- 对于函数,通过计算代码行数衡量大小,而对于类,我们计算权责(responsibility)
- 单一职责原则(SRP)认为,类或模块应有且只有一条加以修改的理由
-
内聚
- 在类中,函数应保证使用到尽可能多的成员变量,这样的类才更内聚
- 保持函数和参数列表短小的策略
- 在类中,少数函数共享了许多成员变量,这些变量仅被少数函数使用,就将其拆分为新的子类吧
-
隔离修改
- OCP (开放-闭合原则):对扩展开发,对修改关闭
- DIP (依赖倒置原则) :类应该依赖于抽象而不是依赖于具体细节
第十一章 系统
-
将系统的构造和使用分隔开
- 应尽量避免以下写法
- 应将对象构造的启始和设置,与正常的运行时逻辑分离出来
public Service getService() { if (service == null) { service = new MyServiceImpl(...); } return service }
-
将构造与使用分开的方法
-
将全部构造过程搬迁到main或称之为main的模块中
- 设计系统的其余部分时,假设所有对象都已正确构造和设置
- 其余部分只是使用构造和设置好的对象,而对构造过程一无所知
-
需要应用程序自己构造对象时,建议使用抽象工厂模式
- 应用可自行控制何时创建,但是无法知晓构造细节
-
依赖注入
-
AOP
-
第十二章 迭进
Kent Beck提出简单设计的四条规则
- 运行所有测试
- 不可重复
- 表达程序员的意图
- 尽可能减少类和方法的数量
第十三章 并发编程
对象是过程的抽象,线程是调度的抽象
第十四章 逐步改进
逐步改进
第十七章 味道与启发
-
注释:注释应该谈及代码自身没提到的东西
- 不恰当的信息
- 废弃的注释
- 冗余注释
- 糟糕的注释
- 注释掉的代码
-
环境
-
函数
-
过多的参数
- 没参数最好
- 一个次之,两个、三个再次之
- 三个以上参数非常值得质疑,应坚决避免
-
输出参数(对于修改对象状态的函数来说)
- 输出参数违反直觉
- 如果函数非要修改什么东西的状态,就修改它所在对象的状态就好了
-
标志参数
- 布尔值参数说明函数不止做一件事,应该消灭掉
-
死函数
- 不被调用的方法应该丢弃
-
-
一般性问题
- 一个源文件存在多种语言
- 明显的行为未被实现
- 不正确的边界行为
- 行为的边界处理得不全面,当函数处于边界条件或极端条件时,会出现异常
-
忽视安全
-
重复
- DRY (Don’t Repeat Yourself)
-
在错误的抽象层级上的代码
- 较高层级概念放在基类中
- 较低层级概念放在派生类中
- 只与细节实现有关的常量、变量或工具函数放在子类中
-
基类依赖于派生类
- 基类和派生类部署在不同jar中
- 派生类修改后,派生类组件重新部署时,不需要部署基类组件
- 基类和派生类部署在不同jar中
-
信息过多:设计良好的模块有着非常小的接口
- 优秀的开发人员会限制类或模块中暴露的接口数量
- 类中的方法越少越好
- 函数知道的变量越少越好
- 类拥有的实体变量越少越好
- 优秀的开发人员会限制类或模块中暴露的接口数量
-
死代码
- 删除不执行的代码
- 不会发生条件的if语句
- 不会抛出异常的try/catch块
- 不会被调用的小工具方法
- 不会发生的switch/case条件
- 删除不执行的代码
-
垂直分隔
- 变量和函数应该靠近被使用的地方定义
- 本地变量定义与使用的垂直距离要短
- 私有函数首次使用与定义时的垂直距离要短
- 变量和函数应该靠近被使用的地方定义
-
前后不一致
- 最小惊异原则:小心选择约定,选中后,就小心持续遵循
- 如使用
response
来持有HttpServletResponse
对象,其他使用HttpServletResponse
的地方也使用同样的变量名 - 方法命名上也要前后一致,处理同一对象的系列方法使用类似的命名
- 操作 + 对象 来命名
- 如使用
- 最小惊异原则:小心选择约定,选中后,就小心持续遵循
-
混淆视听
- 没用的变量
- 从不调用的函数
- 没信息量的注释
- 上述都应被移除
-
选择算子函数
- 不要试图通过参数来改变函数的行为
- 这样的话,应该重构为两/多个函数
-
位置错误的权责
-
代码应放在读者自然而然期待它所带在的地方
-
不恰当的静态方法
-
静态方法不能被重写,当某个方法存在多态的情形时,不应该被设为
static
-
使用解释性变量
- 让程序可读的最有力方法之一就是用有意义的单词命名变量
-
函数名称应该表达其行为
Date newDate = date.add(5);
- 这里
add
的是5天、5个星期、还是5个小时呢 - 这里返回一个新日期,旧日期会改动吗
- 如果函数向日期添加5天且修改该日期,命名为
addDaysTo
或increaseByDays
- 如果函数返回一个表示5天后的日期,而不修改日期实体,命名为
daysLater
或daysSince
- 这里
-
把逻辑依赖改为物理依赖
A
依赖于B
,但是A
中依赖了应属于B
中的成员C
,这就叫作逻辑依赖- 将
C
提取到B
中,通过A
对B
的依赖而访问C
,这便从逻辑依赖转换为了物理依赖
-
用多态替代
If/Else
或Switch/Case
- 出现多个
Swtich/Case
时,应考虑使用 多态
- 出现多个
-
遵循标准约定
-
遵循团队的编码标准
-
用命名常量替代魔术数
-
抽离
if|while
中的条件为恰当命名的函数 -
避免否定性条件(便于理解)
-
函数只该做一件事
-
掩蔽时序耦合
-
常常有必要使用时序耦合,不应该掩蔽它
public static MoogDiver { Gradient gradient; List<Spline> splines; public void dive(String reason) { saturateGradient(); reticulateSplines(); diveForMoog(reason); } }
-
三个函数的次序很重要
- 捕鱼先织网
- 织网先编绳
-
然而代码并没有强制这种时序耦合,程序员在调用
saturateGradient
之前调用reticulateSplines
,从而导致抛出UnsaturatedGradientException
异常。 -
代码重构后,如下所示
public static MoogDiver { Gradient gradient; List<Spline> splines; public void dive(String reason) { Gradient gradient = saturateGradient(); List<Spline> splines = reticulateSplines(gradient); diveForMoog(splines, reason); } }
-
这样通过创建顺序队列暴露了时序耦合,每个函数都产生出下个函数所需的结果,这样必须按顺序调用函数了
-
虽然增加了函数的复杂度,却曝露了该种情况真正的时序复杂性,从而避免了异常
-
-
封装边界条件
-
函数应该只在一个抽象层级上
-
拆分不同抽象层级时重构的最重要功能之一
-
在较高层级放置可配置数据
-
位于较高层级的配置性常量易于修改,它们向下贯穿应用程序
-
不要继承常量
-
枚举代替常量,枚举拥有方法和字段
-
使用描述性名称、名称应与抽象层级相符
-
为较大作用范围变量选用较长名称
-
名称应该说明副作用
-
名称应该说明函数、变量或类的一切信息,不要用名称掩蔽副作用
-
不要用简单的动词来描述做了不止一个简单动作的函数
public ObjectOutputStream getOos() throws IOException { if (oos == null) { oos = new ObjectOutputStream(socket.getOutputStream()); } return oos; }
-
该函数不只是获取一个
oos
,如果oos
不存在,还会创建一个,更好的命名可以叫作createOrReturnOos
-