软件构造学习笔记——重构
重构
什么是重构?
问题:比特衰减(Bit rot)
在几个月后或者几个新版本后,许多代码库(databases)达到下面一种状态:
重写:什么都没有从原始代码中留下来
遗弃:原始代码被扔掉了,被从头重写
为什么呢?
系统进化去满足新的需求和添加新的功能
如果代码的结构不也进化,它将会“腐烂”
即使代码起初在核对的时候就被评审并且很好的设计这也有时发生。
代码维护:
维护:软件产品交付后的修改或修复。
目的:修复bugs,提高性能,提高设计,添加功能
有学术表明百分之八十的维护都是因为无关bug(non-bugfix-related)的活动,像是增加功能((Pigosky
1997)
维护很难,维护代码比重新写个新代码更难,难在:
- “纸牌屋“现象(别接触它)
- 必须理解另外一个开发者写的代码,或者是你拥有不同精神状态另外一个时间点写的代码
维护就是开发者如何利用他们的时间,维护要求:
-
致力于良好设计软件和提前计划,为了让之后的维护减少痛苦
-
未来改变的能力必须被意料到
什么是“重构”?
Chikosfky and Cross说:“在同一相对抽象层次上从一个表示转换到另一个表示,同时保留项目系统的外部行为(功能和语义)。
Griswold:“源级结构转换应被保证保证保留程序的含义”
Opdyke:“程序重组保留程序的行为的操作”
重构是改变一个软件系统的过程,它并不改变代码的外部行为但是改善代码的内部结构。
重构是:
-
重组(重排)代码…
-
在一系列小的,保护语义的变形(代码保持工作),为了让代码更容易去维护和修改
重构并不仅仅是任何一个旧的重组
-
你需要保持代码工作
-
你需要一些小的步骤去保护语义
-
你需要有单元测试去保证代码工作
重构改进了软件的非功能性属性,优点包括提升了代码的可读性和降低了复杂性,这些可以提升源代码的可维护性和创建一个更加具有表现力的内部结构或者对象模型去提升可延展性
重构和增加功能,debug代码,重写代码并不是相同的
重构是“行为保护”
一个重构是一个参数化的行为保护程序变形。
行为保护的方法可以静态或动态地执行检查。
为什么重构呢?
为什么修复你系统并不残破的一部分呢?
你系统里的代码地每一部分都有三个目标:
-
执行它的功能
-
允许改变
-
和读它的开发者能好好交流
如果代码做不到上面一个或者更多,它就是残缺的。
代码重构的目的:
-
让它更容易维护
-
让它更容易理解
-
模块化的分解——分解模块(不是局部的)
-
痛苦地并且枯燥地去写(添加修复bug或增添功能的代码)
-
推广或保证程序面向对象
-
设计一致性
-
性能
什么时候去重构呢?
对一个团队来说什么是重构代码的最佳时机呢?作为流程的一部分,最好不断完成(如测试)。这很难在项目后期做得很好(比如测试),或者不如说,最佳时机是任何一次你发现有一个更好的方式去实现的时候。
当你认证你的系统中一个区域如下情况时重构:
-
没有被良好设计
-
没有被严格地测试,但是看上去至今还能工作
-
需要添加新的功能
-
发现一个异味(“bad
smell”)
以下情况,你不需要重构:
-
稳定的代码(不需要更改的代码)
-
别人家的代码(除非你继承它并且它现在是你的了)
例子1:
switch 语句在恰当设计的面向对象的代码中十分罕见
其次,switch语句是一个简单并且很容易被发现的“异味“
当然,不是所有的switch的应用都是糟糕的
一个switch语句不应该被应用在辨别不同种类的目标
针对这种情况有几种被很好设计的重构:
用多态来替换条件句
动机:你有一个一句不同对象类型来选择不同行为的选择句。
技术:移动选择句的每一个分支去一个子类中的一个复写的方法。让原有的方法抽象化。
例子:
// exp1
class Animal {
final int MAMMAL = 0, BIRD = 1, REPTILE = 2;
int myKind; // set in constructor ...
String getSkin() {
switch (myKind) {
case MAMMAL: return "hair";
case BIRD: return "feathers";
case REPTILE: return "scales";
default: return "integument";
}
}
}
class Animal {
String getSkin() {
return "integument";
}
}
class Mammal extends Animal {
String getSkin() {
return "hair";
}
}
class Bird extends Animal {
String getSkin() {
return "feathers";
}
}
class Reptile extends Animal {
String getSkin() {
return "scales";
}
}
这是怎么提升的?
添加一个新的动物类型,比如两栖类,不需要校正和重新辨析存在的代码,哺乳动物,鸟类和爬行动物可能在其他方面有所不同,我们已将它们分开(因此我们不需要更多的switch语句)
我们已经抛弃了我们需要从一类动物告诉另一类动物的flag,基本上来说,我们正在按照对象的使用方法来使用对象
Junit test
就像我们重构,我们需要去运行Junit test 去保证我们不会引进错误
例子2:
移动一个方法:
动机:一个方法是,将是使用或者被另外一个类的更多的功能使用而不是它被定义的那个类。
技巧:在最常用的类中创建一个具有类似主体的新方法。要么将旧方法转变成一个简单的委任,要么是将它全部移除。
例子3:
引入空对象;
动机:你有许多空检查
技巧:用一个空的对象替换一个空的值
通过用空对象替换空检查执行正确操作彻底消除if语句
//exp2
Customer c = findCustomer(…); ...
if (customer == null) {
name = “occupant”;
}
else {
name = customer.getName();
}
if (customer == null) { …
public class NullCustomer extends Customer {
public String getName() {
return “occupant”;
}
-------------------------------------------
Customer c = findCustomer(…); name = c.getName();
例子4
//exp4
public static int dayOfYear(int month,int dayOfMonth,int year){
if (month == 2) {
dayOfMonth += 31;
} else if (month == 3) {
dayOfMonth += 59;
} else if (month == 4) {
dayOfMonth += 90;
} else if (month == 5) {
dayOfMonth += 31 + 28 + 31 + 30;
} else if (month == 6) {
dayOfMonth += 31 + 28 + 31 + 30 + 31;
} else if (month == 7) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30;
} else if (month == 8) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31;
} else if (month == 9) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31;
} else if (month == 10) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30;
} else if (month == 11) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31;
} else if (month == 12) {
dayOfMonth += 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 31;
}
return dayOfMonth;
}
不要重复
复制代码是一个安全威胁。如果你在两个地方有完全相同或者十分相似的代码,那么基本的威胁时两份中都有bug,并且一些维护者在一个地方修复bug却没有在另一处修复。
赋值和粘贴是一个非常引诱力的编程工具,并且每次使用它你需要感觉到一股危险的颤栗从你的脊梁上滑下来。你复制的代码块越长它越危险。
不要重复,或者只重复很短的,已经成为一个程序员的颂歌!
Fail fast(快速错误)
Fail fast意味着代码可能尽早地显露出bug
问题尽早被发现,就越容易发现和修复
静态检查错误比动态检查更快,并且动态检查失败的速度快于产生可能破坏后续计算的错误答案。
Dayofcheck函数不会fail fast,如果你将它的参数以错误的顺序传递,他会悄悄地返回错误答案。
它需要更多的检查,无论时静态或者动态检查.
避免魔法数字(Magic Numbers)
实际上只有两个常数被计算机科学家认为是有效的:0,1,也许2。
剩下所有常数都被成为魔法,因为它们无中生有地出现并且没有任何解释。
解释数字的一个方法是使用注释,或者是将数字声明为拥有良好、清晰名的常熟。
2……,12作为FEBRUARY,…, DECEMBER具有更好的可读性
30,31,28在像array,List或者map数据结构里将具有更好的可读性。MONTH_LENGTH[month].
难以解释的数字59和90是魔法数字(magic numbers)特别有害的例子,不仅仅是他们难以注释和未记录的,他们实际上是程序员用手完成计算的结果。显示的计算如31+28更清晰的显明了这些难以解释的数字的出处。
概述
每个变量的同一个目的:
不要重复利用参数,也不要重复利用变量:
-
变量不是编程的恐怖之源
-
*的引进他们,赋予他们名字,并且仅仅在你不再需要他们的时候停止使用他们
-
如果一个曾经意味着一件事的变量突然开始意味着不同的几行,
那么你使你的读者困惑,方法参数通常应该保持不变
-
为每个方法的参数尽可能多的使用final是一个好主意。
-
final关键字说明变量不应该会被重新赋值,并且java编译器将会静态地检查它。
但是,重构是危险的;
重构有时会引进错误,因为任何时候你想修改你的软件你都可能引进bug
管理层会因此说:
-
重构增添危险;
-
这是高代价的 - 我们花时间在开发上,但没有“看到”任何外部差异? 我们还要重新测试吗?我们为什么要做这个?
谁开创了重构?
Ward Cunningham and Kent Beck influential people in Smalltalk
Kent Beck – responsible for Extreme Programming
Ralph Johnson a professor at U of Illinois and part of “Gang of
Four”
Bill Opdyke – Ralph’s Doctoral Student
Martin Fowler -
http://www.refactoring.com
Refactoring : Improving the Design of Existing Code
上一篇: 软件构造相关二
下一篇: 软件构造体会(二)复习笔记之再谈泛型