### Introduce Assertion(引入断言)
某一段代码需要对程序状态(state)做出某种假设。
以assertion(断言)明确表现这种假设。
~~~
double getExpenseLimit() {
// should have either expense limit or a primary project
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
=>
~~~
double getExpenseLimit() {
Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
**动机(Motivation)**
常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。例如「平方报计算」只对正值才能进行(译注:这里没考虑复数与虚数),又例如某个对象 可能假设其值域(fields)至少有一个不等于null。
这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时程序员会以注释写出这样的假设。而我要介绍的是一种更好的技术:使用assertion(断言)明确标明这些假设。
assertion 是一个条件式,应该总是为真。如果它失败,表示程序员犯了错误。因此assertion的失败应该导致一个unchecked exception[7](不可控异常〕。Assertions 绝对不能被系统的其他部分使用。实际上程序最后成品往往将assertions 统统删除。因此,标记「某些东西是个assertion」是很重要的。
[7]译注:所谓unchecked exception 是指「未曾于函数签名式(signature)中列出」的异常。
Assertions 可以作为交流与调试的辅助。在交流(沟通〕的角度上,assertions 可以帮助程序阅读者理解代码所做的假设;在调试的角度上,assertions 可以在距离「臭虫」最近的地方抓住它们。当我编写自我测试代码的时候,我发现,assertions 在调试方面的帮助变得不那么重要了,但我仍然非常看重它们在交流方面的价值。
**作法(Mechanics)**
如果程序员不犯错,assertions 就应该不会对系统运行造成任何影响,所以加入assertions 永远不会影响程序的行为。
- 如果你发现代码「假设某个条件始终(必须)为真],就加入一个assertion 明确说明这种情况。
- 你可以新建一个Assert class,用于处理各种情况下的assertions 。
注意,不要滥用assertions 。请不要使用它来检查你「认为应该为真」的条件,请只使用它来检查「一定必须为真」的条件。滥用assertions 可能会造成难以维护的重复逻辑。在一段逻辑中加入assertions 是有好处的,因为它迫使你重新考虑这段代 码的约束条件。如果「不满足这些约朿条件,程序也可以正常运行」,assertions 就不会带给你任何帮助,只会把代码变得混乱,并且有可能妨碍以后的修改。
你应该常常问自己:如果assertions 所指示的约束条件不能满足,代码是否仍能正常运行?如果可以,就把assertions 拿掉。
另外,还需要注意assertions 中的重复代码。它们和其他任何地方的重复代码一样不好闻。你可以大胆使用Extract Method 去掉那些重复代码。
**范例:(Example)**
下面是一个简单例子:开支(经费)限制。后勤部门的员工每个月有固定的开支限额;业务部门的员工则按照项目的开支限额来控制自己的开支。一个员工可能没有开支额度可用,也可能没有参与项目,但两者总得要有一个(否则就没有经费可用 了)。在开支限额相关程序中,上述假设总是成立的,因此:
~~~
class Employee...
private static final double NULL_EXPENSE = -1.0;
private double _expenseLimit = NULL_EXPENSE;
private Project _primaryProject;
double getExpenseLimit() {
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
boolean withinLimit (double expenseAmount) {
return (expenseAmount <= getExpenseLimit());
}
~~~
这段代码包含了一个明显假设:任何员工要不就参与某个项目,要不就有个人开支限额。我们可以使用assertion 在代码中更明确地指出这一点:
~~~
double getExpenseLimit() {
Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
这条assertion 不会改变程序的任何行为。另一方面,如果assertion中的条件不为真,我就会收到一个运行期异常:也许是在withinLimit() 函数中抛出一个空指针(null pointer)异常,也许是在Assert.isTrue() 函数中抛出一个运行期异常。有时assertion 可以帮助程序员找到臭虫,因为它离出错地点很近。但是,更多时候,assertion 的价值在于:帮助程序员理解代码正确运行的必要条件。
我常对assertion 中的条件式使用Extract Method ,也许是为了将若干地方的重复码提炼到同一个函数中,也许只是为了更清楚说明条件式的用途。
在Java 中使用assertions 有点麻烦:没有一种简单机制可以协助我们插入这东西[8]。 assertions 可被轻松拿掉,所以它们不可能影响最终成品的性能。编写一个辅助类(例如Assert class)当然有所帮助,可惜的是assertions 参数中的任何表达式不论什么情况都一定会被执行一遍。阻止它的惟一办法就是使用类似下面的手法:
[8[译注:J2SE1.4已经支持assert语句。
~~~
double getExpenseLimit() {
Assert.isTrue (Assert.ON &&
(_expenseLimit != NULL_EXPENSE || _primaryProject != null));
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
或者是这种手法:
~~~
double getExpenseLimit() {
if (Assert.ON)
Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
如果Assert.ON 是个常量,编译器(译注:而非运行期间)就会对它进行检查; 如果它等于false ,就不再执行条件式后半段代码。但是,加上这条语句实在有点丑陋,所以很多程序员宁可仅仅使用Assert.isTrue() 函数,然后在项目结束前以过滤程序滤掉使用assertions 的每一行代码(可以使用Perl 之类的语言来编写这样 的过滤程序)。
Assert class应该有多个函数,函数名称应该帮助程序员理解其功用。除了isTrue() 之外,你还可以为它加上equals() 和shouldNeverReachHere() 等函数。
# 章节十 简化函数调用
在对象技术中,最重要的概念莫过于「接口」(interface)。容易被理解和被使用的接口,是开发良好面向对象软件的关键。本章将介绍「使接口变得更简洁易用」 的重构手法。
最简单也最重要的一件事就是修改函数名称。「名称」是程序写作者与阅读者交流的关键工具。只要你能理解一段程序的功能,就应该大胆地使用Rename Method 将你所知道的东西传达给其他人。另外,你也可以(并且应该)在适当时机修改变量名称和class 名称。不过,总体来说,「修改名称」只是相对比较简单 的文本替换功夫,所以我没有为它们提供单独的重构项目。
函数参数在「接口」之中扮演十分重要的角色。 Add Parameter 和Remove Parameter 都是很常见的重构手法。初始接触面向对象技术的程序员往往使用很长的参数列(parameter lists),这在其他开发环境中是很典型的方式。但是, 使用对象技术,你可以保持参数列的简短,以下有一些相关的重构可以帮助你缩短参数列。如果来自同一对象的数个值被当作参数传递,你可以运用 Preserve Whole Object 将它们替换为单一对象,从而缩短参数列。如果此前并不存在这样一个对象,你可以运用Introduce Parameter Object将它创建出来。如果函数参数来自该函数可取用的一个对象,则可以使用 Replace Parameter with Method 避免传递参数。如果某些参数被用来在条件式中做选择依据,你可以实施 Replace Parameter with Explicit Methods。另外,你还可以使用Parameterize Method 为数个相似函数添加参数,将它们合并到一起。
关于缩减参数列的重构手法,Doug Lea 对我提出了一个警告:并发编程(con-current programming)往往需要使用较长的参数列,因为这样你可以保证传递给函数的参数都是不可被修改的,就像内置型对象和value object 一定地不可变。通常,你可以使用不可变对象(immutable object)取代这样的长参数列,但另一方面你也必须对此类重构保持谨慎。
多年来我一直坚守一个很有价值的习惯:明确地将「修改对象状态」的函数(修改函数,modifiers)和「查询对象状态」的函数(查询函数,queries)分开设计。不知道多少次,我因为将这两种函数混在一起而麻烦缠身;不知道多少次,我看到别 人也因为同样的原因而遇到同样的麻烦。因此,如果我看到这两种函数混在一起, 我就使用 Separate Query from Modifier 将它们分开。
良好的接口只向用户展现必须展现的东西。如果一个接口暴露了过多细节,你可以将不必要暴露的东西隐藏起来,从而改进接口的质量。毫无疑问,所有数据都应该隐藏起来(希望你不需要我来告诉你这一点),同时,所有可以隐藏的函数都应该被隐藏起来。进行重构时,你往往需要暂时暴露某些东西,最后再以 Hide Method 和Remove Setting Method 将它们隐藏起来。
构造函数(constructors)是Java 和C++ 中特别麻烦的一个东西,因为它强迫你必须知道「待建对象」属于哪一个class ,而你往往并不需要知道这一点。你可以使用Replace Constructor with Factory Method 避免了解这「被迫了解的一点」。
转型(casting)是Java 程序员心中另一处永远的痛。你应该尽量使用Encapsulate Downcast 将「向下转型动作」封装隐藏起来,避免让class 用户做那种动作。
和许多现代编程语言一样,Java 也有异常处理(exception-handing)机制,这使得错误处理(error handling)相对容易一些。不习惯使用异常的程序员,往往会以错误代码(error code)表示程序遇到的麻烦。你可以使用Replace Error Code with Exception 来运用这些崭新的异常特性。但有时候异常也并不是最合适的选择,你应该实施Replace Exception with Test 先测试一番。
- 译序 by 侯捷
- 译序 by 熊节
- 序言
- 前言
- 章节一 重构,第一个案例
- 起点
- 重构的第一步
- 分解并重组statement()
- 运用多态(Polymorphism)取代与价格相关的条件逻辑
- 结语
- 章节二 重构原则
- 何谓重构
- 为何重构
- 「重构」助你找到臭虫(bugs)
- 何时重构
- 怎么对经理说?
- 重构的难题
- 重构与设计
- 重构与性能(Performance)
- 重构起源何处?
- 章节三 代码的坏味道
- Duplicated Code(重复的代码)
- Long Method(过长函数)
- Large Class(过大类)
- Long Parameter List(过长参数列)
- Divergent Change(发散式变化)
- Shotgun Surgery(散弹式修改)
- Feature Envy(依恋情结)
- Data Clumps(数据泥团)
- Primitive Obsession(基本型别偏执)
- Switch Statements(switch惊悚现身)
- Parallel Inheritance Hierarchies(平行继承体系)
- Lazy Class(冗赘类)
- Speculative Generality(夸夸其谈未来性)
- Temporary Field(令人迷惑的暂时值域)
- Message Chains(过度耦合的消息链)
- Middle Man(中间转手人)
- Inappropriate Intimacy(狎昵关系)
- Alternative Classes with Different Interfaces(异曲同工的类)
- Incomplete Library Class(不完美的程序库类)
- Data Class(纯稚的数据类)
- Refused Bequest(被拒绝的遗贈)
- Comments(过多的注释)
- 章节四 构筑测试体系
- 自我测试代码的价值
- JUnit测试框架
- 添加更多测试
- 章节五 重构名录
- 重构的记录格式
- 寻找引用点
- 这些重构准则有多成熟
- 章节六 重新组织你的函数
- Extract Method(提炼函数)
- Inline Method(将函数内联化)
- Inline Temp(将临时变量内联化)
- Replace Temp with Query(以查询取代临时变量)
- Introduce Explaining Variable(引入解释性变量)
- Split Temporary Variable(剖解临时变量)
- Remove Assignments to Parameters(移除对参数的赋值动作)
- Replace Method with Method Object(以函数对象取代函数)
- Substitute Algorithm(替换你的算法)
- 章节七 在对象之间搬移特性
- Move Method(搬移函数)
- Move Field(搬移值域)
- Extract Class(提炼类)
- Inline Class(将类内联化)
- Hide Delegate(隐藏「委托关系」)
- Remove Middle Man(移除中间人)
- Introduce Foreign Method(引入外加函数)
- Introduce Local Extension(引入本地扩展)
- 章节八 重新组织数据
- Self Encapsulate Field(自封装值域)
- Replace Data Value with Object(以对象取代数据值)
- Change Value to Reference(将实值对象改为引用对象)
- Replace Array with Object(以对象取代数组)
- Replace Array with Object(以对象取代数组)
- Duplicate Observed Data(复制「被监视数据」)
- Change Unidirectional Association to Bidirectional(将单向关联改为双向)
- Change Bidirectional Association to Unidirectional(将双向关联改为单向)
- Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)
- Encapsulate Field(封装值域)
- Encapsulate Collection(封装群集)
- Replace Record with Data Class(以数据类取代记录)
- Replace Type Code with Class(以类取代型别码)
- Replace Type Code with Subclasses(以子类取代型别码)
- Replace Type Code with State/Strategy(以State/strategy 取代型别码)
- Replace Subclass with Fields(以值域取代子类)
- 章节九 简化条件表达式
- Decompose Conditional(分解条件式)
- Consolidate Conditional Expression(合并条件式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)
- Replace Conditional with Polymorphism(以多态取代条件式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
- 章节十一 处理概括关系
- Pull Up Field(值域上移)
- Pull Up Method(函数上移)
- Pull Up Constructor Body(构造函数本体上移)
- Push Down Method(函数下移)
- Push Down Field(值域下移)
- Extract Subclass(提炼子类)
- Extract Superclass(提炼超类)
- Extract Interface(提炼接口)
- Collapse Hierarchy(折叠继承关系)
- Form Template Method(塑造模板函数)
- Replace Inheritance with Delegation(以委托取代继承)
- Replace Delegation with Inheritance(以继承取代委托)
- 章节十二 大型重构
- 这场游戏的本质
- Tease Apart Inheritance(梳理并分解继承体系)
- Convert Procedural Design to Objects(将过程化设计转化为对象设计)
- Separate Domain from Presentation(将领域和表述/显示分离)
- Extract Hierarchy(提炼继承体系)
- 章节十三 重构,复用与现实
- 现实的检验
- 为什么开发者不愿意重构他们的程序?
- 现实的检验(再论)
- 重构的资源和参考资料
- 从重构联想到软件复用和技术传播
- 结语
- 参考文献
- 章节十四 重构工具
- 使用工具进行重构
- 重构工具的技术标准(Technical Criteria )
- 重构工具的实用标准(Practical Criteria )
- 小结
- 章节十五 集成
- 参考书目