什么是重构?

按书中 P45 中的说法,重构这个概念被分成了动词和名词的方面被分别阐述:

  • 重构(名词): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • 重构(动词): 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

坏代码张什么样?

灰色:没看懂;
红色:争议;
绿色:废话。

  • 神秘命名
  • 重复代码
  • 过长函数
  • 过长参数列表
  • 全局数据: 全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了一些诡异的 BUG,而问题的根源却在遥远的别处。
  • 可变数据: 对数据的修改经常导致出乎意料的结果和难以发现的 BUG。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据。https://eslint.bootcss.com/docs/rules/no-param-reassign
    • js更倾向于函数式编程,在众说纷纭的计算机编程语言圈子里,每个人对函数式编程的理解不尽想同,但其核心是:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
  • 发散式变化: 模块经常因为不同的原因在不同的方向上发生变化。
  • 散弹式修改: 每遇到某种变化,你都必须在许多不同的类内做出许多小修改。
  • 依恋情结: 所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。
  • 数据泥团: 你经常在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。
  • 基本类型偏执: 很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。
  • 重复的 switch: 在不同的地方反复使用相同的 switch 逻辑。问题在于:每当你想增加一个选择分支时,必须找到所有的 switch,并逐一更新。
  • 循环语句: 我们发现,管道操作(如 filter 和 map)可以帮助我们更快地看清被处理的元素一级处理它们的动作。
    • 也不能固执于管道操作
  • 冗余的元素
  • 夸夸其谈通用性: 函数或类的唯一用户是测试用例。
  • 临时字段: 有时你会看到这样的类:其内部某个字段仅为某种特定情况而定。这样的代码让人不理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
  • 过长的消息链
  • 中间人: 过度运用委托。
  • 内幕交易: 软件开发者喜欢在模块之间筑起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。
  • 过大的类
  • 异曲同工的类
  • 纯数据类: 所谓纯数据类是指:他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
  • 被拒绝的遗赠: 拒绝继承超类的实现,我们不介意:但如果拒绝支持超类的接口,这就难以接受了。
  • 注释: 当你感觉需要纂写注释时,请先尝试重构,试着让所有注释都变得多余。

重构的一些方法

结构化代码

结构化的代码更加便于我们阅读和理解,例如最常使用的重构方法:提炼函数

  • 把意图和实现分开
    1. void printOwing(double amount) {
    2. printBanner();
    3. //print details
    4. System.out.println ("name:" + _name);
    5. System.out.println ("amount" + amount);
    6. }
    7. // ====>
    8. void printOwing(double amount) {
    9. printBanner();
    10. printDetails(amount);
    11. }
    12. void printDetails (double amount) {
    13. System.out.println ("name:" + _name);
    14. System.out.println ("amount" + amount);
    15. }

    更清楚的表达用意

    要保持软件的 KISS 原则是不容易的,但是也有一些方法可以借鉴,例如:引入解释性变量
    动机:用一个良好命名的临时变量来解释对应条件子句的意义,使语义更加清晰。 ```java if ( (platform.toUpperCase().indexOf(“MAC”) > -1) && (browser.toUpperCase().indexOf(“IE”) > -1) &&
    1. wasInitialized() && resize > 0 ){
    // do something }

// ====>

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 }

  1. 另外由于 lambda 表达式的盛行,我们现在有一些更加优雅易读的方法使我们的代码保持可读:**以管道取代循环**就是这样一种方法。
  2. ```javascript
  3. const names = [];
  4. for (const i of input) {
  5. if (i.job === "programer")
  6. names.push(i.name);
  7. }
  8. // ====>
  9. const names = input
  10. .filter(i => i.job === "programer")
  11. .map(i => i.name);

简化条件表达式

分解条件式: 我们能通过提炼代码,把一段 「复杂的条件逻辑」 分解成多个独立的函数,这样就能更加清楚地表达自己的意图。

  1. if (date.before (SUMMER_START) || date.after(SUMMER_END)){
  2. charge = quantity * _winterRate + _winterServiceCharge;
  3. } else {
  4. charge = quantity * _summerRate;
  5. }
  6. // ====>
  7. if (notSummer(date)){
  8. charge = winterCharge(quantity);
  9. } else {
  10. charge = summerCharge (quantity);
  11. }

另外一个比较受用的一条建议就是:以卫语句取代嵌套条件式。根据经验,条件式通常有两种呈现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件式提供的答案中只有一种是正常行为,其他都是不常见的情况。
精髓是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支和 else 分支的重视是同等的。 这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句(guard clauses)就不同了,它告诉阅读者:「这种情况很罕见,如果它真的发生了,请做 一些必要的整理工作,然后退出。」
「每个函数只能有一个入口和一个出口」的观念,根深蒂固于某些程序员的脑海里。 我发现,当我处理他们编写的代码时,我经常需要使用 Replace Nested Conditional with Guard Clauses。现今的编程语言都会强制保证每个函数只有一个入口, 至于「单一出口」规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:如果「单一出口」能使这个函数更清楚易读,那么就使用单一出口;否则就不必这么做。

  1. double getPayAmount() {
  2. double result;
  3. if (_isDead) result = deadAmount();
  4. else {
  5. if (_isSeparated) result = separatedAmount();
  6. else {
  7. if (_isRetired) result = retiredAmount();
  8. else result = normalPayAmount();
  9. };
  10. }
  11. return result;
  12. };
  13. // =====>
  14. double getPayAmount() {
  15. if (_isDead) return deadAmount();
  16. if (_isSeparated) return separatedAmount();
  17. if (_isRetired) return retiredAmount();
  18. return normalPayAmount();
  19. };

原文
https://zhuanlan.zhihu.com/p/68385955

https://zh-hans.reactjs.org/docs/how-to-contribute.html#request-for-comments-rfc