重构(第二版) 读书笔记 第一章(从一个小程序入手,开始我们的第一次重构)

本文是《重构》一书第一章的读书笔记,通过重构一个JavaScript小程序,探讨重构的意义和步骤。首先介绍了重构的背景,强调代码维护的重要性。接着详细描述了重构的准备工作,包括设置JavaScript运行环境和调试。然后逐步分解代码,提炼函数,优化逻辑,最后通过分离计算和输出阶段,展示了如何优雅地添加新功能。重构的目标是提高代码可读性和维护性,为应对需求变化做好准备。
摘要由CSDN通过智能技术生成


《重构》 从一个小程序入手,开始我们的第一次重构

"如果它还可以运行,就不要动它。 " 如果一段代码能正常工作,并且不会再被修改,或许我们应该遵循这句古老的工程谚语,毕竟机器并不在乎你的代码结构是否丑陋。但代码需要维护,需求总在变动。如果有人看你写的代码时觉得很费劲,特别是那个人是一个月后的你自己时,那么,是时候改进它了。

脑袋能力有限,代码需要不断改进;记忆并不可靠,知识也需要时常温习。

时时勤拂拭,勿使惹尘埃


一、准备工作

我也想立刻开始我的第一次重构,但打开书我就被拦住了。书中的示例代码是Javascript,而我没有接触过这门语言。为了后续学习的流畅,必须得稍微准备下。

1.JavaScript运行环境与debug

毕竟不是文章主题,在这里只贴上我遇到的问题和解决的链接:

  1. vscode运行调试javascript代码
  2. SyntaxError: cannot use import statement outside a module
    解决办法:在package.json文件中设置"type": “module”
    npm Docs Creating a package.json file
    Solved: Uncaught SyntaxError: cannot use import statement outside a module

语言都是相通的,示例代码比较简单,大概也都能看懂,解决了运行环境和debug问题,那我们就开始重构之旅。

二、重构步骤

1.起步(先理解场景和代码功能,代码我做了必要的注释,方便理解)

场景:一个戏剧演出团,根据观众人数剧目类型来向客户收费
示例代码功能:1.根据客户观看信息,计算应收取的费用。 2.计算获取的积分(看剧获得积分,某种促销活动。相应规则我注释在代码里)

直接上代码

//演出ID(索引),演出名、类型
const playsData = {
   
    "hamlet": {
    "name": "Hamlet", "type": "tragedy" },
    "as-like": {
    "name": "As You Like It", "type": "comedy" },
    "othello": {
    "name": "Othello", "type": "tragedy" }
}
//记录顾客观看的演出ID和对应的人数
const invoicesData = {
   
    "customer": "BigCo",
    "performances": [
        {
   
            "playID": "hamlet",  			//ID
            "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) {
   
    	//通过演出ID拿到演出名和类型
        const play = plays[perf.playID];
        let thisAmount = 0;

        switch (play.type) {
   
        	//悲剧收费规则
            case "tragedy":
                thisAmount = 40000;
                if (perf.audience > 30) {
   
                    thisAmount += 1000 * (perf.audience - 30);//人数超过30,额外收费
                }						//人越多,对单个人收费越高?(太黑了吧,狗都不看)
                break;
            //喜剧收费规则
            case "comedy":
                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}`);
        }

        // 积分计算规则
        volumeCredits += Math.max(perf.audience - 30, 0);
        // 看喜剧额外加积分    
        if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);

        // 拼接最后要打出的字符串
        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;
}

console.log(statement(invoicesData,playsData));//调用函数并输出
//输出
Statement for BigCo
 Hamlet: $650.00 (55 seats)
 As You Like It: $580.00 (35 seats)
 Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits

代码组织不甚清晰,但这还在可忍受的限度内。书的作者觉得复杂的示例会让读者不忍卒读,所以设计了这一小段代码。这样短小的程序或许确实不需要进行深入的设计,但请想象它处于一个更大规模的项目中。

现在需求变动来了:1.希望也支持以HTML格式输出详单 ; 2.演员尝试更多的戏剧类型,对于新增的戏剧类型有新的计分方式。
读者可以思考下自己会怎样完成新功能,对问题1你会复制一份代码并修改其中字符串拼接的部分,以保证两种输出格式都支持? 对问题2是否会要增加switch里的分支并写下新的计费逻辑?

我再强调一次,是需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它。能改进之当然很好,但若没人需要去理解它,它就不会真正妨碍什么。

2.代码块分解(分解statement函数)

2.1提取switch语句,提炼函数(106)

先将这块代码抽取成一个独立的函数,按它所干的事情给它命名。作者在重构时会遵顼一些标准流程,他把这一步称作“提炼函数(106)”

每当看到这样长长的函数,我便下意识地想从整个函数中分离出不同的关注点。

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 = amountFor(play, perf);//---------------------这里 
        volumeCredits += Math.max(perf.audience - 30, 0);

        if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);

        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;
//-------------------------------------------------------------这里
//javascript支持函数写在函数内部
    function amountFor(play, aPerformance) {
   
        let result = 0;
        switch (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: ${
     play.type}`);
        }
        return result;
    }
}

console.log(statement(invoicesData,playsData));

做完这个改动后,我会马上编译并执行一遍测试,看看有无破坏了其他东西。无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。犯错误是很容易的——至少我知道我是很容易犯错的。

!!!重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。

这里有一些小tips,ide一般都对重构有支持,可以选择对应代码块后点击灯泡,按提示操作;提炼出来的函数,对于其用到的不会被改变的变量可以当作参数传入,会变动的变量(thisAmount)可以作为返回值; 对被提炼出的函数的函数名做合适的修改,让其能表达它的功能,对其中变量名修改,使其更简洁明确,比如返回值改为result。

立刻编译运行输出,检查功能是否被破坏。

2.2 对amountFor进行一些改进

paly可以由aPerformance计算得到,可以尝试删除它。

我喜欢将play这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。这里我要使用的重构手法是以查询取代临时变量(178)

  • 首先play赋值的表达式可以提取为
function playFor(perf) {
   
        return plays[perf.playID];
    }
  • 使用内联变量(123)手法内联play变量和amount变量(就是把play和amount替换为对应函数)
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) {
   

        volumeCredits += Math.max(perf.audience - 30, 0);

        if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);

        result += ` ${
     playFor(perf).name}: ${
     format(amountFor(playFor(perf), perf) / 100)} (${
     perf.audience} seats)\n`;
        totalAmount += amountFor(playFor(perf), perf);
    }
    result += `Amount owed is ${
     format(totalAmount / 100)}\n`;
    result += `You earned ${
     volumeCredits} credits\n`;
    return result;
  • 对amountFor函数应用改变函数声明(124)。 去除play参数。
  • 修改参数名,最终结构如下
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) {
   

        volumeCredits += Math.max
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值