重构

198 阅读7分钟

第1章 重构,第一个示例

题目

有一个戏剧团,提供一些剧目,剧团根据观众人数及剧目类型来 向客户收费。计算给客户的账单和积分。下面提供了一些数据及规则。

{
  "hamlet": {"name": "Hamlet", "type": "tragedy"},//悲剧
  "as-like": {"name": "As You Like It", "type": "comedy"},
  "othello": {"name": "Othello", "type": "tragedy"}
}


[
  {
    "customer": "BigCo",
    "performances": [
      {
          "playID": "hamlet",
          //观众
          "audience": 55
      }, 
      {
          "playID": "as-like",
          "audience": 35
      },
      {
          "playID": "othello",
          "audience": 40
      } 
    ]
  }
]

function statement (invoice, plays) {
  // 账单总金额
  let totalAmount = 0;
  // 总积分
  let volumeCredits = 0;
  // 结果字符串
  let result = `Statement for ${invoice.customer}\n`;
  // 计算舍入规则
  const format = new Intl.NumberFormat("en-US",
                        { style: "currency", currency: "USD",
                          minimumFractionDigits: 2 }).format;
  //遍历剧目金额信息
  for (let perf of invoice.performances) {
    //拿到剧目基本信息
    const play = plays[perf.playID];
    //当前剧目金额
    let thisAmount = 0;
    //判断剧目类型
    switch (play.type) {
    //悲剧 默认40000 当观众>30每超过一个人加1000
    case "tragedy":
      thisAmount = 40000;
      if (perf.audience > 30) {
        thisAmount += 1000 * (perf.audience - 30);
      }
      break;
    case "comedy":
    //喜剧 默认30000 当观众>20每超过一个人加500整体再加10000,最后每个观众加300
      thisAmount = 30000;
      if (perf.audience > 20) {
        thisAmount += 10000 + 500 * (perf.audience - 20);
      }
      thisAmount += 300 * perf.audience;
      break;
    default:
        throw new Error(`unknown type: ${play.type}`);
    }
    // add volume credits
    //计算积分,超过30个观众积1分
    volumeCredits += Math.max(perf.audience - 30, 0);
    // add extra credit for every ten comedy attendees
    //如果是喜剧额外加观众数除以5向下取整
    if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
    // print line for this order
    //单位转化将分化成元
    result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
    totalAmount += thisAmount;
  }
  result += `Amount owed is ${format(totalAmount/100)}\n`;
  result += `You earned ${volumeCredits} credits\n`;
  return result;
}

思考

  1. 结果生成html
  2. 添加剧类型,改变计费、积分方式

重构

第一阶段:提取函数

  1. 提取计算金额方法
  2. 规范属性命名
  3. 去掉可省略参数
  4. 提取计算积分方法
  5. 提取format函数
  6. 分离volumeCredits\分离totalAmount

目的和原则

  1. 要有完备的测试用例
  2. 结构清晰化
  3. 命名规范化
  4. 可以先忽略性能,重构后在优化

重构后代码

//主方法
function statement (invoice, plays) {
    let result = `Statement for ${invoice.customer}\n`;
    for (let perf of invoice.performances) {
        result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
    }
    result += `Amount owed is ${usd(totalAmount())}\n`;
    result += `You earned ${totalVolumeCredits()} credits\n`;
    return result;
}
//计算总金额
function totalAmount() {
    let result = 0;
    for (let perf of invoice.performances) {
      result += amountFor(perf);
    }
    return result;
}
//计算总积分
function totalVolumeCredits() {
    let result = 0;
    for (let perf of invoice.performances) {
      result += volumeCreditsFor(perf);
    }
    return result;
}
//金额单位转化
function usd(aNumber) {
    return new Intl.NumberFormat("en-US",
                    { style: "currency", currency: "USD",
                      minimumFractionDigits: 2 }).format(aNumber/100);
}
//计算单个剧目产生积分
function volumeCreditsFor(aPerformance) {
    let result = 0;
    result += Math.max(aPerformance.audience - 30, 0);
    if ("comedy" === playFor(aPerformance).type) result += Math.floor(aPerformance.audience /
    5);
    return result;
}
//获取剧目信息
function playFor(aPerformance) {
    return plays[aPerformance.playID];
}
//获取剧目金额
function amountFor(aPerformance) {
    let result = 0;
    switch (playFor(aPerformance).type) {
        case "tragedy":
          result = 40000;
          if (aPerformance.audience > 30) {
            result += 1000 * (aPerformance.audience - 30);
          }
          break;
        case "comedy":
          result = 30000;
          if (aPerformance.audience > 20) {
            result += 10000 + 500 * (aPerformance.audience - 20);
          }
          result += 300 * aPerformance.audience;
          break;
        default:
          throw new Error(`unknown type: ${playFor(aPerformance).type}`);
    }
    return result;
}

第二阶段:支持html展示

封装底层数据,展示方式不同是只改展示代码就可以了

  1. 将代码分层,展示层、业务层对于当前的需求,展示层是经常变化的。分层后只修改或新增展示层的方法就可以了
  2. 第一层,数据层,将获取数据的方法都封装到一个文件内,产出一个数据对象供下一层使用
  3. 第二层,展示层,拿到数据对象,根据自己的个性化订制结果
import createStatementData from './createStatementData.js';
//文本入口
function statement (invoice, plays) {
  return renderPlainText(createStatementData(invoice, plays));
}
//生成文本信息
function renderPlainText(data, plays) {
  let result = `Statement for ${data.customer}\n`;
  for (let perf of data.performances) {
    result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`;
  }
  result += `Amount owed is ${usd(data.totalAmount)}\n`;
  result += `You earned ${data.totalVolumeCredits} credits\n`;
  return result;
}
//html入口
function htmlStatement (invoice, plays) {
  return renderHtml(createStatementData(invoice, plays));
}
//生成html
function renderHtml (data) {
  let result = `<h1>Statement for ${data.customer}</h1>\n`;
  result += "<table>\n";
  result += "<tr><th>play</th><th>seats</th><th>cost</th></tr>";
  for (let perf of data.performances) {
    result += ` <tr><td>${perf.play.name}</td><td>${perf.audience}</td>`;
    result += `<td>${usd(perf.amount)}</td></tr>\n`;
  }
  result += "</table>\n";
  result += `<p>Amount owed is <em>${usd(data.totalAmount)}</em></p>\n`;
  result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\n`;
  return result;
}
function usd(aNumber) {
  return new Intl.NumberFormat("en-US",{ style: "currency", currency: "USD",minimumFractionDigits:2}).format(aNumber/100);
}

//封装所有数据到一个对象中
export default function createStatementData(invoice, plays) {
   const result = {};
   result.customer = invoice.customer;
   result.performances = invoice.performances.map(enrichPerformance);
   result.totalAmount = totalAmount(result);
   result.totalVolumeCredits = totalVolumeCredits(result);
   return result;
    //将所有数据封装到一起
    function enrichPerformance(aPerformance) {
         const result = Object.assign({}, aPerformance);
         result.play = playFor(result);
         result.amount = amountFor(result);
         result.volumeCredits = volumeCreditsFor(result);
         return result;
    }
    //获取play
    function playFor(aPerformance) {
        return plays[aPerformance.playID]
    }
    //计算金额
    function amountFor(aPerformance) {
     let result = 0;
     switch (aPerformance.play.type) {
     case "tragedy":
       result = 40000;
       if (aPerformance.audience > 30) {
         result += 1000 * (aPerformance.audience - 30);
       }
       break;
     case "comedy":
       result = 30000;
       if (aPerformance.audience > 20) {
         result += 10000 + 500 * (aPerformance.audience - 20);
       }
       result += 300 * aPerformance.audience;
       break;
     default:
         throw new Error(`unknown type: ${aPerformance.play.type}`);
     }
     return result;
    }
    //计算积分
    function volumeCreditsFor(aPerformance) {
     let result = 0;
     result += Math.max(aPerformance.audience - 30, 0);
     if ("comedy" === aPerformance.play.type) result += Math.floor(aPerformance.audience / 5);
     return result;
    }
    //计算总金额
    function totalAmount(data) {
     return data.performances
       .reduce((total, p) => total + p.amount, 0);
    }
    //计算总积分
    function totalVolumeCredits(data) {
     return data.performances
       .reduce((total, p) => total + p.volumeCredits, 0);
    }

第三阶段:支持多类型戏剧

多类型是不可预见的

  1. 利用多态的特性
  2. 不同的类型实现各自的子类
  3. 当新增类型时新增子类就可以了
export default function createStatementData(invoice, plays) {
   const result = {};
   result.customer = invoice.customer;
   result.performances = invoice.performances.map(enrichPerformance);
   result.totalAmount = totalAmount(result);
   result.totalVolumeCredits = totalVolumeCredits(result);
   return result;
   function enrichPerformance(aPerformance) {
     const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));
     const result = Object.assign({}, aPerformance);
     result.play = calculator.play;
     result.amount = calculator.amount;
     result.volumeCredits = calculator.volumeCredits;
     return result;
   }
   function playFor(aPerformance) {
     return plays[aPerformance.playID]
   }
   function totalAmount(data) {
     return data.performances
       .reduce((total, p) => total + p.amount, 0);
   }
   function totalVolumeCredits(data) {
     return data.performances
       .reduce((total, p) => total + p.volumeCredits, 0);
   }
 }
 //创建计算类方法
 function createPerformanceCalculator(aPerformance, aPlay) {
     switch(aPlay.type) {
     case "tragedy": return new TragedyCalculator(aPerformance, aPlay);
     case "comedy" : return new ComedyCalculator(aPerformance, aPlay);
     default:
         throw new Error(`unknown type: ${aPlay.type}`);
     }
 }
 //金额、积分计算父类
 class PerformanceCalculator {
   constructor(aPerformance, aPlay) {
     this.performance = aPerformance;
     this.play = aPlay;
   }
   get amount() {
     throw new Error('subclass responsibility');
   }
   get volumeCredits() {
     return Math.max(this.performance.audience - 30, 0);
} }
//悲剧金额、积分计算方法
 class TragedyCalculator extends PerformanceCalculator {
  get amount() {
     let result = 40000;
     if (this.performance.audience > 30) {
       result += 1000 * (this.performance.audience - 30);
     }
     return result;
   }
 }
 //喜剧金额、积分计算方法
 class ComedyCalculator extends PerformanceCalculator {
   get amount() {
     let result = 30000;
     if (this.performance.audience > 20) {
       result += 10000 + 500 * (this.performance.audience - 20);
     }
     result += 300 * this.performance.audience;
     return result;
   }
   get volumeCredits() {
     return super.volumeCredits + Math.floor(this.performance.audience / 5);
} }

第2章 重构的原则

  1. 重构的关键在于运用大 量微小且保持软件行为的步骤,一步步达成大规模的修改。
  2. 每个单独的重构要么很小,要么由若干小步骤组合而成。
  3. 即便重构没有完成,也可以在任何时刻停下来
  4. 重构与性能优化:重构是为了让代码“更容易理解,更易于修改”。这可能使程序运行得更快,也可能使程序运行得更慢。在性能优化时,只关心让程序运行得更快,最终得到的代码有可能更难理解和维护。
  5. 分清新功能和重构,一会添加新功能一会重构可能后造成程序难以理解

为何重构

  1. 重构改进软件的设计:随着代码量增加,和为短期目的修改代码,导致代码质量低
  2. 重构使软件更容易理解:深刻理解代码
  3. 重构帮助找bug
  4. 重构提高编码速度
  5. 随着系统的演变,不断地重构才能够更好的提高开发效率,提高代码质量

何时重构:事不过三,三则重构

  1. 预备性重构:让添加新功能更容易。在开始开发前先看代码的结构整体和新功能是否能匹配,是否简单重构后就能完美结合。
  2. 帮助理解的重构:使代码更易懂
  3. 捡垃圾式重构,遇到不好的就要重构,循序渐进
  4. 有计划的重构和见机行事的重构:一般都是见机行事,但长时间未重构也可以计划
  5. 长期重构:不推荐,应该慢慢推进
  6. 复审代码时重构
  7. 何时不应该重构:1、不需要修改的。2、重写比重构容易

重构的挑战

  1. 延缓新功能开发:不会延缓开发,重构是为了提速
  2. 代码所有权:在“强代码所有制”和“混乱修 改”两个极端之间权衡
  3. 分支:持续集成CI
  4. 测试:自测试代码要完备
  5. 遗留代码:缺乏测试
  6. 数据库:新增字段情况,多步发布:
    1、新增字段。
    2、改代码同时写两个字段。
    3、迁移数据。
    4、没问题再去掉老字段,代码只保存新字段

重构、架构和YAGNI

有了重构技术,我就可以采取不同的策略。与其猜测未来需要哪些灵活性、 需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软 件的设计质量做得很高。随着对用户需求的理解加深,我会对架构进行重构,使 其能够应对新的需要。

YAGNI

适可而止:You Ain’t Gonna Need It
YAGNI原则指的是只需要将应用程序必需的功能包含进来,而不要试图添加任何其他你认为可能需要的功能。

重构与软件开发过程

  1. 三大实践——自测试代 码、持续集成、重构
  2. 重构不能影响到别人的工作
  3. 就算有影响也要及时发现

重构与性能

重构很大程度上是不会影响性能的,会使性能优化更容易

编写快速软件的方法

  1. 时间预算法:用于性能要求极高的实时系统。实现做好预算,严格按预算进行。
  2. 持续关注法:随时保证系统的性能
  3. 性能提升法:开发的后段性能优化阶段度量工具监控热点,对热点进行优化

对于性能的优化不要臆测,要实际的度量

自动化重构

强大的IDE

第3章 代码的坏味道

代码重构的时机,在于坏味道

  1. 神秘命名:见名知意
  2. 重复代码
  3. 过长函数:注释、条件表达式、循环都可能是拆分的信号
  4. 过长参数列表:封装对象
  5. 全局数据:封装起来,控制作用域
  6. 可变数据:控制修改入口,避免多处引用修改
  7. 发散式变化:模块独立,引入一次变化只修改对应模块,不能发散到其他地方
  8. 霰弹式修改:某个变化,引起好多地方的修改
  9. 依恋情结:一个函数与另一个模块有大量交互,搬移函数。策略 (Strategy)模式和访问者(Visitor)模式
  10. 数据泥团:不同的类有相同的字段和类似行为,要统一封装类、实体
  11. 重复的switch:避免多处重复的switch
  12. 循环语句:管道操作 filter map
  13. 冗赘的元素:内联
  14. 夸夸其谈通用性:提前搞一些根本用不到的装置,只有测试用例调用的方法
  15. 临时字段:提取特定字段及对应的函数
  16. 过长的消息链:调用链很长有很多对象,应该使用隐藏委托关系
  17. 中间人:过度委托
  18. 内幕交易:模块间的耦合,委托代替继承。
  19. 过大的类:
  20. 异曲同工的类:可以提炼超类
  21. 纯数据类:封装控制作用域,相关操作函数都封装起来
  22. 被拒绝的遗赠:子类不愿意支持超类的接口,应该运用以委托取代子类或者以委托取代超类彻底划清界限
  23. 注释:当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。