重构,改善既有代码的设计读后感-拆分计算阶段与格式化阶段
假如,需要增加一个功能:目前仅仅有文本详单,需要增加一个HTML详单。如果仅仅是重构到当前步骤,还需要将函数拷贝到另一个函数中。虽然,条例也算清晰,但是如果我们实现的更好,能将所有需要的数据放到一个数据结构,HTML详单可以调用一个函数获取所有数据,在进行HTML编码,效果会更好吧。
拆分阶段(154)
要实现服用由多种方法,本书推荐的技术是拆分阶段。本例中第一阶段为产生数据,第二阶段为渲染数据。要开始拆分阶段,应该先对第二阶段的代码应用提炼函数。在这个例子中,这部分代码就是打印详单的代码,即statement函数的全部内容(七行代码)。要把他们与所有嵌套函数一起抽象到一个新的顶层函数中。然后创建一个对象,作为两个阶段间传递的中转数据结构。
function statement(invoice,plays) {
const statementData = {};
return renderPlainText(statementData,invoice,plays);//第二阶段
}
//拆分阶段 render 呈现 plaintext纯文本
function renderPlainText (data, invoice,plays ) {
let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串
for(let aPerformance of invoice.performances){
//print line for this order
result += ` ${playFor(aPerformance).name}:${usd(amountFor(aPerformance)/100)} (${aPerformance.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
//计算总的账目
function totalAmount() {
let totalAmount = 0 ; // 账单总额
for(let aPerformance of invoice.performances){
totalAmount += amountFor(aPerformance);
}
return totalAmount;
}
//计算观众积分 add volume credits
function totalVolumeCredits() {
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
for(let aPerformance of invoice.performances){
//计算观众积分 add volume credits
volumeCredits += volumeCreditsFor(aPerformance);
}
return volumeCredits;
}
//这一轮循环增加的量
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance - 30 , 0);
if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5);
return result;
}
//格式化数字,显示为货币
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
//获取某一剧目
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;
}
}
现在检查一下renderPlaintext其他参数,我希望将参数都挪到中转参数中,让renderPlainText只操作data传过来的数据。
那么就先invoice的两个属性到data
function statement(invoice,plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances;
return renderPlainText(statementData,plays);
}
//拆分阶段 render 呈现 plaintext纯文本
function renderPlainText (data,plays ) {
let result = `Statement for ${data.customer}\n`; //用于打印的字符串
for(let aPerformance of data.performances){
//print line for this order
result += ` ${playFor(aPerformance).name}:${usd(amountFor(aPerformance)/100)} (${aPerformance.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
//计算总的账目
function totalAmount() {
let totalAmount = 0 ; // 账单总额
for(let aPerformance of data.performances){
totalAmount += amountFor(aPerformance);
}
return totalAmount;
}
//计算观众积分 add volume credits
function totalVolumeCredits() {
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
for(let aPerformance of data.performances){
//计算观众积分 add volume credits
volumeCredits += volumeCreditsFor(aPerformance);
}
return volumeCredits;
}
//这一轮循环增加的量
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance - 30 , 0);
if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5);
return result;
}
//格式化数字,显示为货币
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
//获取某一剧目
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;
}
}
另外,如果希望“剧目名称”数据也从中转数据得来,就需要使用play中的数据填充aPerformance对象。
同样的手法处理amountFor
接下来就搬移观众量积分,然后将两个计算总数的搬移到statement函数中,同时将usd移动到顶层,以便于 其他渲染方式调用,结果如下:
function statement(invoice,plays) {
const statementData = {};
statementData.customer = invoice.customer;
//这里是一个知识点,类似与Java的新循环
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return renderPlainText(statementData,plays);
function enrichPerformance(aPerformance) {
const result = Object.assign({},aPerformance);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
//获取某一剧目
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 - 30 , 0);
if ("comedy" == aPerformance.play.type) result += Math.floor(aPerformance.audience / 5);
return result;
}
//计算总的账目
function totalAmount(data) {
let totalAmount = 0 ; // 账单总额
for(let aPerformance of data.performances){
totalAmount += aPerformance.amount;
}
return totalAmount;
}
//计算观众积分 add volume credits
function totalVolumeCredits(data) {
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
for(let aPerformance of data.performances){
//计算观众积分 add volume credits
volumeCredits += aPerformance.volumeCredits;
}
return volumeCredits;
}
}
//拆分阶段 render 呈现 plaintext纯文本
function renderPlainText (data,plays ) {
let result = `Statement for ${data.customer}\n`; //用于打印的字符串
for(let aPerformance of data.performances){
//print line for this order
result += ` ${aPerformance.play.name}:${usd(aPerformance.amount/100)} (${aPerformance.audience} seats)\n`;
}
result += `Amount owed is ${usd(data.totalAmount)}\n`;
result += `You earned ${data.totalVolumeCredits} credits\n`;
return result;
}
//提到顶层,以供其他函数使用
//格式化数字,显示为货币
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
以管道取代循环(231)
接下来以管道取代循环(231),就可以将第一阶段的代码提取到独立的函数中了。同时,再去实现html版本就非常容易了,也很好的实现了代码的复用。
function statement() {
return renderPlainText(createStatementData(invoices,plays))
}
function createStatementData(invoice,plays) {
const statementData = {};
statementData.customer = invoice.customer;
//这里是一个知识点,类似与Java的新循环
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return statementData;
function enrichPerformance(aPerformance) {
const result = Object.assign({},aPerformance);
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
//获取某一剧目
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 - 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);
}
//计算观众积分 add volume credits
function totalVolumeCredits(data) {
return data.performances.reduce((total,p) =>total+p.volumeCredits,0)
}
}
//拆分阶段 render 呈现 plaintext纯文本
function renderPlainText (data,plays ) {
let result = `Statement for ${data.customer}\n`; //用于打印的字符串
for(let aPerformance of data.performances){
//print line for this order
result += ` ${aPerformance.play.name}:${usd(aPerformance.amount/100)} (${aPerformance.audience} seats)\n`;
}
result += `Amount owed is ${usd(data.totalAmount)}\n`;
result += `You earned ${data.totalVolumeCredits} credits\n`;
return result;
}
//提到顶层,以供其他函数使用
//格式化数字,显示为货币
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
function htmlStatement(invoices,plays) {
return rederHtml(createStatementData(invoices,plays));
}
function rederHtml(data) {
//...
}
同时,生成中转参数的函数可以提取到单独的文件。
也许,有的代码还能够优化,但是我们经常需要在重构与添加新特性之间寻找平衡。当我们面临选择时,应当尽可能的遵循营地法则:保证你离开时的代码库一定比来时更健康。
欢迎大家留言,以便于后面的人更快解决问题!另外亦欢迎大家可以关注我的微信公众号,方便利用零碎时间互相交流。共勉!
------愿来生只做陌上的看花人,无须入尘缘,仅行于陌上,看一川风花,无爱无伤-----