什么是重构?
按书中 P45 中的说法,重构这个概念被分成了动词和名词的方面被分别阐述:
- 重构(名词): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 重构(动词): 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
坏代码张什么样?
灰色:没看懂;
红色:争议;
绿色:废话。
- 神秘命名
- 重复代码
- 过长函数
- 过长参数列表
- 全局数据: 全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了一些诡异的 BUG,而问题的根源却在遥远的别处。
- 可变数据: 对数据的修改经常导致出乎意料的结果和难以发现的 BUG。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据。https://eslint.bootcss.com/docs/rules/no-param-reassign
- js更倾向于函数式编程,在众说纷纭的计算机编程语言圈子里,每个人对函数式编程的理解不尽想同,但其核心是:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
- 发散式变化: 模块经常因为不同的原因在不同的方向上发生变化。
- 散弹式修改: 每遇到某种变化,你都必须在许多不同的类内做出许多小修改。
- 依恋情结: 所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。
- 数据泥团: 你经常在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。
- 基本类型偏执: 很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。
- 重复的 switch: 在不同的地方反复使用相同的 switch 逻辑。问题在于:每当你想增加一个选择分支时,必须找到所有的 switch,并逐一更新。
- 循环语句: 我们发现,管道操作(如 filter 和 map)可以帮助我们更快地看清被处理的元素一级处理它们的动作。
- 也不能固执于管道操作
- 冗余的元素
- 夸夸其谈通用性: 函数或类的唯一用户是测试用例。
- 临时字段: 有时你会看到这样的类:其内部某个字段仅为某种特定情况而定。这样的代码让人不理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
- 过长的消息链
- 中间人: 过度运用委托。
- 内幕交易: 软件开发者喜欢在模块之间筑起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。
- 过大的类
- 异曲同工的类
- 纯数据类: 所谓纯数据类是指:他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
- 被拒绝的遗赠: 拒绝继承超类的实现,我们不介意:但如果拒绝支持超类的接口,这就难以接受了。
- 注释: 当你感觉需要纂写注释时,请先尝试重构,试着让所有注释都变得多余。
重构的一些方法
结构化代码
结构化的代码更加便于我们阅读和理解,例如最常使用的重构方法:提炼函数
- 把意图和实现分开
void printOwing(double amount) {
printBanner();
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
// ====>
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
更清楚的表达用意
要保持软件的 KISS 原则是不容易的,但是也有一些方法可以借鉴,例如:引入解释性变量
动机:用一个良好命名的临时变量来解释对应条件子句的意义,使语义更加清晰。 ```java if ( (platform.toUpperCase().indexOf(“MAC”) > -1) && (browser.toUpperCase().indexOf(“IE”) > -1) &&
// do something }wasInitialized() && resize > 0 ){
// ====>
final boolean isMacOs = platform.toUpperCase().indexOf(“MAC”) > -1; final boolean isIEBrowser = browser.toUpperCase().indexOf(“IE”) > -1; final boolean wasResized = resize > 0; if (isMacOs && isIEBrowser && wasInitialized() && wasResized) { // do something }
另外由于 lambda 表达式的盛行,我们现在有一些更加优雅易读的方法使我们的代码保持可读:**以管道取代循环**就是这样一种方法。
```javascript
const names = [];
for (const i of input) {
if (i.job === "programer")
names.push(i.name);
}
// ====>
const names = input
.filter(i => i.job === "programer")
.map(i => i.name);
简化条件表达式
分解条件式: 我们能通过提炼代码,把一段 「复杂的条件逻辑」 分解成多个独立的函数,这样就能更加清楚地表达自己的意图。
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);
}
另外一个比较受用的一条建议就是:以卫语句取代嵌套条件式。根据经验,条件式通常有两种呈现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件式提供的答案中只有一种是正常行为,其他都是不常见的情况。
精髓是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支和 else 分支的重视是同等的。 这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句(guard clauses)就不同了,它告诉阅读者:「这种情况很罕见,如果它真的发生了,请做 一些必要的整理工作,然后退出。」
「每个函数只能有一个入口和一个出口」的观念,根深蒂固于某些程序员的脑海里。 我发现,当我处理他们编写的代码时,我经常需要使用 Replace Nested Conditional with Guard Clauses。现今的编程语言都会强制保证每个函数只有一个入口, 至于「单一出口」规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:如果「单一出口」能使这个函数更清楚易读,那么就使用单一出口;否则就不必这么做。
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
};
// =====>
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
};
原文
https://zhuanlan.zhihu.com/p/68385955
https://zh-hans.reactjs.org/docs/how-to-contribute.html#request-for-comments-rfc