第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;
}
思考
- 结果生成html
- 添加剧类型,改变计费、积分方式
重构
第一阶段:提取函数
- 提取计算金额方法
- 规范属性命名
- 去掉可省略参数
- 提取计算积分方法
- 提取format函数
- 分离volumeCredits\分离totalAmount
目的和原则
- 要有完备的测试用例
- 结构清晰化
- 命名规范化
- 可以先忽略性能,重构后在优化
重构后代码
//主方法
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展示
封装底层数据,展示方式不同是只改展示代码就可以了
- 将代码分层,展示层、业务层对于当前的需求,展示层是经常变化的。分层后只修改或新增展示层的方法就可以了
- 第一层,数据层,将获取数据的方法都封装到一个文件内,产出一个数据对象供下一层使用
- 第二层,展示层,拿到数据对象,根据自己的个性化订制结果
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);
}
第三阶段:支持多类型戏剧
多类型是不可预见的
- 利用多态的特性
- 不同的类型实现各自的子类
- 当新增类型时新增子类就可以了
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章 重构的原则
- 重构的关键在于运用大 量微小且保持软件行为的步骤,一步步达成大规模的修改。
- 每个单独的重构要么很小,要么由若干小步骤组合而成。
- 即便重构没有完成,也可以在任何时刻停下来
- 重构与性能优化:重构是为了让代码“更容易理解,更易于修改”。这可能使程序运行得更快,也可能使程序运行得更慢。在性能优化时,只关心让程序运行得更快,最终得到的代码有可能更难理解和维护。
- 分清新功能和重构,一会添加新功能一会重构可能后造成程序难以理解
为何重构
- 重构改进软件的设计:随着代码量增加,和为短期目的修改代码,导致代码质量低
- 重构使软件更容易理解:深刻理解代码
- 重构帮助找bug
- 重构提高编码速度
- 随着系统的演变,不断地重构才能够更好的提高开发效率,提高代码质量
何时重构:事不过三,三则重构
- 预备性重构:让添加新功能更容易。在开始开发前先看代码的结构整体和新功能是否能匹配,是否简单重构后就能完美结合。
- 帮助理解的重构:使代码更易懂
- 捡垃圾式重构,遇到不好的就要重构,循序渐进
- 有计划的重构和见机行事的重构:一般都是见机行事,但长时间未重构也可以计划
- 长期重构:不推荐,应该慢慢推进
- 复审代码时重构
- 何时不应该重构:1、不需要修改的。2、重写比重构容易
重构的挑战
- 延缓新功能开发:不会延缓开发,重构是为了提速
- 代码所有权:在“强代码所有制”和“混乱修 改”两个极端之间权衡
- 分支:持续集成CI
- 测试:自测试代码要完备
- 遗留代码:缺乏测试
- 数据库:新增字段情况,多步发布:
1、新增字段。
2、改代码同时写两个字段。
3、迁移数据。
4、没问题再去掉老字段,代码只保存新字段
重构、架构和YAGNI
有了重构技术,我就可以采取不同的策略。与其猜测未来需要哪些灵活性、 需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软 件的设计质量做得很高。随着对用户需求的理解加深,我会对架构进行重构,使 其能够应对新的需要。
YAGNI
适可而止:You Ain’t Gonna Need It
YAGNI原则指的是只需要将应用程序必需的功能包含进来,而不要试图添加任何其他你认为可能需要的功能。
重构与软件开发过程
- 三大实践——自测试代 码、持续集成、重构
- 重构不能影响到别人的工作
- 就算有影响也要及时发现
重构与性能
重构很大程度上是不会影响性能的,会使性能优化更容易
编写快速软件的方法
- 时间预算法:用于性能要求极高的实时系统。实现做好预算,严格按预算进行。
- 持续关注法:随时保证系统的性能
- 性能提升法:开发的后段性能优化阶段度量工具监控热点,对热点进行优化
对于性能的优化不要臆测,要实际的度量
自动化重构
强大的IDE
第3章 代码的坏味道
代码重构的时机,在于坏味道
- 神秘命名:见名知意
- 重复代码
- 过长函数:注释、条件表达式、循环都可能是拆分的信号
- 过长参数列表:封装对象
- 全局数据:封装起来,控制作用域
- 可变数据:控制修改入口,避免多处引用修改
- 发散式变化:模块独立,引入一次变化只修改对应模块,不能发散到其他地方
- 霰弹式修改:某个变化,引起好多地方的修改
- 依恋情结:一个函数与另一个模块有大量交互,搬移函数。策略 (Strategy)模式和访问者(Visitor)模式
- 数据泥团:不同的类有相同的字段和类似行为,要统一封装类、实体
- 重复的switch:避免多处重复的switch
- 循环语句:管道操作 filter map
- 冗赘的元素:内联
- 夸夸其谈通用性:提前搞一些根本用不到的装置,只有测试用例调用的方法
- 临时字段:提取特定字段及对应的函数
- 过长的消息链:调用链很长有很多对象,应该使用隐藏委托关系
- 中间人:过度委托
- 内幕交易:模块间的耦合,委托代替继承。
- 过大的类:
- 异曲同工的类:可以提炼超类
- 纯数据类:封装控制作用域,相关操作函数都封装起来
- 被拒绝的遗赠:子类不愿意支持超类的接口,应该运用以委托取代子类或者以委托取代超类彻底划清界限
- 注释:当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。