title: 重构读书笔记
date: 2019/11/05
重构:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构的前提是单元测试。
六、重新组织函数
6.1 提炼函数
你有一段代码可以被组织在一起并独立出来 => 将这段代码放进一个独立的函数中,并让函数名称解释该函数的用途。
void printOwing(double amount) {
printBanner();
sout("name:" + _name);
sout("amount" + _amount);
}
=>
void printOwing(double amount) {
printBanner();
printDetails();
}
void printDetails() {
sout("name:" + _name);
sout("amount" + _amount);
}
何时使用?
- 过长的函数
- 需要注释才能让人理解用途的代码
好处?
- 函数的粒度很小,函数复用的机会就会更大
- 如果函数命名良好,就会使高层函数读起来就像一系列注释
一个函数多长合适?
长度不是问题,关键在于函数名称和函数本体之间的语义距离。
做法?
- 创建一个新函数,根据这个函数的意图为他起一个合适的名字(以“做什么”而不是“怎样做”来命名)。
即使想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能以更好的表达代码的意图,就应该提炼他。
如果想不出一个更有意义的名称,就别动
-
仔细检查“提炼代码段”,看看是否引用了“作用域仅在源函数”的变量(包括局部变量和源函数参数)。
-
检查是否有任何仅用于“提炼代码段”的局部变量。如果有,在“提炼代码段”声明为局部变量。
-
检查“提炼代码段”,看看是否有任何局部变量被他改变。如果一个临时变量值被修改了,看看是否可以将“提炼代码段”处理为一个查询,并将结果赋给相关变量。
-
传入“提炼代码段”需要的局部变量。
如果需要返回多个变量怎么办?
分为多个函数,每个返回一个变量。
6.2 内联函数
一个函数的本体与名称一样通俗易懂,而且比较简单 => 将函数中的代码copy到调用函数中,删除该函数
int getRating() {
return (moreThanFiveLateDeliveries) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
=>
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
此重构方法同样适用于,使用了太多间接层的代码。
6.3 内联临时变量
一个临时变量,只被一个简单的表达式赋值了一次,而它妨碍了其它重构方法 => 将所有对改变了的引用动作,替换为对它赋值的那个表达式自身。
double basePrice = anOrder.basePrice();
return basePrice > 1000;
=>
return (anOrder.basePrice()) > 1000;
6.4 以查询取代临时变量
以一个临时变量保存某一表达式的运算结果 => 将这个表达式提炼到一个独立函数中。将这个临时变量的的所有引用点替换为对新函数的引用。
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000) {
...
}
=>
if (getBasePrice() > 1000) {
...
}
double getBasePrice() {
return _quantity * _itemPrice;
}
其实我觉得复杂且可重用的表达式,可以提取出来,但是简单的就没必要了。
6.5 引入解释变量
一个复杂的表达式 => 将该复杂的表达式(或其中一部分)的结果放进一个临时变量,以此变量名解释表达式用途。
if ((platform.toUpperCase().indexOf("MAC")) > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInit() && resize > 0) {
...
}
final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIE = browser.toUpperCase().indexOf("IE") > -1
final boolean wasResized = resize > 0
if (isMacOS && isIE && wasResized && wasInit()) {
...
}
如果这个复杂的算法其它方法也能用,就独立成方法,如果只有本方法再用,采用局部变量
6.6 分解临时变量
一个临时变量被赋值超过一次(他不是循环变量,也不用于收集计算结果) => 针对每次赋值,创建一个独立对应的临时变量
double temp = 2 * (_height + _width);
sout(temp);
temp = _height * _width
sout(temp);
=>
double 周长 = 2 * (_height + _width);
sout(周长);
double 面积 = _height * _width
sout(面积);
单一职责
6.7 移除对参数的赋值
代码对一个参数进行赋值 => 以临时变量取代参数的位置
降低的代码的清晰度。
6.8 以函数对象取代函数
一个大型函数,其中有很多局部变量,导致无法使用【提取方法】重构手段 => 将这个函数放入一个单独的对象中,如此一来局部变量就成为了对象中的字段,然后就可以在这个类中,把大型函数分解为多个小型函数
7、在对象间搬移特性
7.1 搬移函数
程序中,有个函数与其所驻类之外的另一个类进行更多交流(调用后者,或被后者调用) => 在另一个类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或者直接把旧函数删除。
7.2 搬移字段
某个字段被其所驻类之外的另一个类更多的用到 => 在目标类新建一个字段,修改源字段的所有用户,令他使用新字段
7.3 提炼类
某个类做了两个类做的事 => 建立一个新类,将相关字段和函数移到新类
一个类应该是一个清楚的抽象,处理一些明确的责任。
但是在工作中,类会不断的成长扩展。不断的为它添加责任,从而导致这个类过于复杂。
这样的类往往有大量的函数和数据;例如Person类,其中有大量的和电话号码相关的字段(区号、电话、手机等),那么就可以将其移除出去,让他和Person类建立关系。
7.4 将类内联化
某个类没有做太多事情 => 将这个类的所有特性搬移到另一个类中,然后移除原类
如果一个类不再承担足够的责任,我们就可以使用这一重构手法。
7.5 隐藏委托关系
客户通过委托类来调用另一个对象 => 在服务类上建立客户所需的所有函数,用以隐藏委托关系
class Person {
Department _department;
public Department get_department() {
return _department;
}
public void set_department(Department _department) {
this._department = _department;
}
}
// 部门
class Department {
private String _chargeCode;
private Person _manager;
public String get_chargeCode() {
return _chargeCode;
}
public void set_chargeCode(String _chargeCode) {
this._chargeCode = _chargeCode;
}
public Person get_manager() {
return _manager;
}
public void set_manager(Person _manager) {
this._manager = _manager;
}
}
// 客户想知道部门经理是谁;这种写法就向用户揭露了Department的工作原理
person.get_department().get_manager();
=>
class Person {
Department _department;
public Person get_manager() {
return _department.get_manager();
}
...
7.6 移除中间人
某个类做了过多的简单委托 => 让客户直接调用受托类
以上面的例子来说,如果Department类的方法全部都需要Person类进行委托,那么就应该让客户端直接调用Department类。
7.7 引入外加函数
要为提供服务的类增加一个函数,但无法修改服务类 => 在客户类中新建一个函数,并把服务类实例作为第一个参数传入。
Date newStart = new Date(previousEnds.getYear(), previousEnds.getMonth(), previousEnds.getDate() + 1);
=>
Date newStart = nextDay(previousEnds);
private static Date nextDay(Date date) {
return new Date(date.getYear(), date.getMonth(), date.getDate() + 1);
}
7.8 引入本地扩展
要为提供服务的类增加一个函数,但无法修改服务类 => 建立一个新类,是它包含这些额外的函数,让这个扩展品成为源类的子类或包装类
8、重新组织数据
8.1 自封装字段
直接访问一个字段,但与字段之间的耦合关系变得笨拙 => 为这个字段建立get/set方法,并且只以这些函数来访问字段。
private int _low, _hight;
boolean includes(int arg) {
return arg >= _low && arg <= _hight;
}
=>
private int _low, _hight;
boolean includes(int arg) {
return arg >= get_low() && arg <= get_hight();
}
get/set方法略
8.2 以对象取代数据值
有一个数据项,必须要与其它数据和行为一起使用才有意义 => 将数据项变成对象。
开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。
比如说,一开始你可能会用一个字符串来表示“电话号码”概念,但是随后你就会发现,电话号码需要“格式化”、“抽取区号”之类的特殊行为。当这种字段越来越多的时候,你就应该将数据值变成对象。
8.7 将单项关联改为双向关联
两个类都需要使用对方特性,但其中只有一条单向连接 => 添加一个反向指针,并使修改函数能够同时更新两条连接
8.9 以字面常量取代魔法数
一个字面数值,代表特殊含义 => 创造一个常量,根据其意义为他命名
double potentialEnergy(double mass, double height) {
return mass * 9.8 * height;
}
=>
static final double 重力加速度 = 9.8;
double potentialEnergy(double mass, double height) {
return mass * 重力加速度 * height;
}
8.11 封装集合
函数返回一个集合 => 函数返回一个只读副本,并在该类中提供添加/移除集合中元素的函数
9、简化条件表达式
9.1 分解条件表达式
有一个复杂的条件语句 => 从if、then、else三个段落分别提炼出独立函数。
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * _winterRate + _winterServiceCharge;
} else {
charge = quantity + _summerRate;
}
=>
if (notSummer(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}
9.2 合并条件表达式
有一系列条件测试,都得到相同结果 => 将其合并成为一个条件表达式,并提炼为一个函数。
double disabilityAmount() {
if (_seniority < 2) return 0;
if (_monthsDisableed > 12) return 0;
if (_isPartTime) return 0;
}
=>
double disabilityAmount() {
if (this.isNotEligible4Disability()) return 0;
}
好处:
合并后的条件代码会告诉你,“实际只有一次条件检查,只不过有多个并列条件需要检查而已”。
将检查条件独立成为一个函数对应代码阅读很有好处,因为他把描述“做什么”的语句换成了”为啥这样做“
9.3 合并重复的条件片段
在条件表达式的每一个分支上有着一段相同的代码 => 将这段相同的代码移到条件表达式之外
if (flag) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
=>
if (flag) {
total = price * 0.95;
} else {
total = price * 0.98;
}
send();
9.4 移除控制标记
boolean flag = true;
while (flag) {
if (doSomething()) {
flag = false;
}
}
=>
while (true) {
if (doSomething()) {
break;
}
}
9.5 以卫语句取代嵌套条件表达式
result = xxx;
if (xxx) {
if (yyy) {
result = yyy;
} else {
result = zzz;
}
} else {
result = www;
}
return result;
=>
if (!xxx) return www;
...
卫语句要不就从函数中返回,要不就抛出一个异常。
9.6 以多态取代条件表达式
工厂模式
9.7 引入空对象
https://blog.csdn.net/qiumengchen12/article/details/44923139
9.8 引入断言
某一段代码需要对程序状态做出某种假设 => 以断言明确表现这种假设
如果断言失败,代表程序员犯了错误。
10、简化函数调用
10.1 函数改名
函数的名称没有揭示函数的用途 => 修改函数名称
函数的名称应该准确的表达它的用途,函数的内容一定不要离函数名太远。
10.2 添加参数
某个函数需要从调用端得到更多的信息 => 为此函数添加一个对象参数,让该对象带进函数所需信息
注:如果有其它可能(例如,通过已有参数可以计算出来),不要添加参数,实在不行再使用本项重构。
10.3 移除参数
函数本体不再需要某个参数 => 将其去除
10.4 将查询函数和修改函数分离
10.5 令函数携带参数
若干函数做了类似的工作,但在函数本体中却包含了不同的值 => 建立单一函数,以参数表达那些不同的值
多个函数干的活类似,只是部分参数不同,将那部分参数作为入参传入。
10.6 以明确函数取代参数
一个函数,完全取决于参数值而采取不同行为 => 针对参数的每一个可能值,建立一个独立函数
void setValue(String name, int value) {
if (name.equals("height")) {
_height = value;
return;
}
if (name.equals("weight")) {
_weight = value;
return;
}
}
=>
void setHeight(int height) {
_height = height;
}
void setWeight(int weight) {
_weight = weight;
}
10.7 保持对象完成
从某个对象中取出若干值,将他们作为某一次函数调用时的参数 => 改为传递整个字段
参数列表要尽可能短,最好可以通过很少的参数获取到所需的信息。
注:如果会是你的依赖结构恶化,那么就不要使用这个重构方法。
10.8 以函数取代参数
对象调用某个函数,并将所得结果作为参数传给另一个函数。而且接受该函数的函数本身也能调用前一个函数 => 让参数接受者去除该项参数,并直接调用前一个函数
如果函数可以通过其它途径获得参数值,那么他就不该通过参数获取该值,过长的参数值会增加阅读者的理解难度。
10.9 引入参数对象
某些参数总是很自然的同时出现 => 以一个对象取代这些参数
10.12 以工厂函数取代构造函数
创建对象时不仅仅是做简单的建构动作 => 将构造函数改为静态工厂方法
10.13 封装向下转型
某个函数返回的对象,需要由函数调用者执行向下转型 => 将向下转型动作转移到函数中。
方法的返回值最好返回客户端需要的类型。
11、处理概况关系
11.8 提炼接口
若干客户使用类/接口中的同一子集 => 将相同的子集提炼到一个独立的接口中
例如 Tomcat的HttpServletRequestWrapper类,实现了Lifecyle接口和HttpServletRequest接口,假设这2个接口是一个接口,但是客户使用时其实只需要用到HttpServletRequest接口的部分,所以将其独立成为一个接口。
如果某个类在不同环境下扮演截然不同的角色,使用接口就是一个好主意。
todo: 我现在还是不会使用接口,需要读源码了啊,但现在能力不足,唉。
11.9 以委托取代继承
某个子类只使用超类中的一部分,或者根本不需要继承而来的数据 => 在子类中新建一个字段用以保存超类;调整子类函数,令他改为委托超类;然后去掉2者之间的关系