重构:改善既有代码的设计(第2版)-读书学习

2,387 阅读11分钟

书籍资源

www.aliyundrive.com/s/LoXwyqW78…

重构-代码精简

书中的例子是js,这里转换成java的代码了,意思相近:

private static String statement(Invoice invoice, List<Play> plays) throws Exception {
    double totalAmount = 0;
    double volumeCredits = 0;
    String result = "Statement for " + invoice.getCustomer();
    String format = "";
    Map<String, Play> collect = plays.stream().collect(Collectors.toMap(Play::getPlayID, Function.identity()));
    for (Invoice.performance performance : invoice.getPerformances()) {
        Play play = collect.get(performance.getPlayID());
        double thisAmount = 0;
        switch (play.getType()) {
            case "tragedy":
                thisAmount = 40000;
                if (performance.getAudience() > 30) {
                    thisAmount += 1000 * (performance.getAudience() - 30);
                }
                break;
            case "comedy":
                thisAmount = 30000;
                if (performance.getAudience() > 20) {
                    thisAmount += 10000 + 500 * (performance.getAudience() - 20);
                }
                thisAmount += 300 * performance.getAudience();
                break;
            default:
                throw new Exception("unknown type:" + play.getType());
        }
        // add volume credits
        volumeCredits += Math.max(performance.getAudience() - 30, 0);
        // add extra credit for every ten comedy attendees
        if ("comedy".equals(play.getType())) {
            volumeCredits += Math.floor(performance.getAudience() / 5);
        }
        // print line for this order
        result += "" + play.getName() + thisAmount / 100 + performance.getAudience();
        totalAmount += thisAmount;
    }
    result += "Amount owed is" + totalAmount / 100;
    result += "You earned " + volumeCredits;
    return result;
}
  1. 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力,杜绝重构之后功能不能使用 2.提炼函数,把计算逻辑提取成方法,按照他所做的事情给它命名,如例子中在计算一场戏剧演出的费用:amountFor
private static double amountFor(Invoice.performance performance, Play play) throws Exception {
    double thisAmount = 0;
    switch (play.getType()) {
        case "tragedy":
            thisAmount = 40000;
            if (performance.getAudience() > 30) {
                thisAmount += 1000 * (performance.getAudience() - 30);
            }
            break;
        case "comedy":
            thisAmount = 30000;
            if (performance.getAudience() > 20) {
                thisAmount += 10000 + 500 * (performance.getAudience() - 20);
            }
            thisAmount += 300 * performance.getAudience();
            break;
        default:
            throw new Exception("unknown type:" + play.getType());
    }
    return thisAmount;
}

3.顶层作用域(局部变量),消除无必须的局部变量,如例子中的amountFor中的play变量就可以中演出的变量查询出来,定义getPlay的方法

private static Play getPlay(Invoice.performance performance) {
    return collect.get(performance.getPlayID());
}

4.重构时要主要统一的编码风格,命名风格,一眼就能看出它的作用

5.内联变量,如例子中方法获取的play变量就可以通过getPlay的方法进行替换掉

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

7.对于有些共同的方法可以提取到工具类中(util),书中的例子是分离到文件中去,导入进来引用(js)

8.使用多态计算器来提供数据,书中的例子play代表的话剧,书中的意思是剧团有很多种类,所以创建了一个工厂函数(工厂模式)来创建话剧,透镜中也是用到了工程模式的(chartFactor),还有对应的工厂处理类

@Component
@RequiredArgsConstructor
@Log4j2
public class ChartFactory {
    ```
/**
 * 根据plugin获取对应的handler
 *
 * @param plugin
 * @return
 */
public static IChartHandler getChartHandler(String plugin) {
    plugin = StringUtils.isEmpty(plugin) ? "base" : plugin;
    Class clazz = clazzMap.get(plugin);
    String simpleName = clazz.getSimpleName();
    String first = simpleName.substring(0, 1);
    try {
        return SpringUtil.getBean(simpleName.replaceFirst(first, first.toLowerCase()) + "Handler", AbstractChartHandler.class);
    } catch (NoSuchBeanDefinitionException e) {
        log.warn("组件对应handler未查询到,确认是否存在该handler,如确实不存在,请忽略。");
        return null;
    }
}

重构原则

  1. 何谓重构: 重构:改善既有代码的设计(第2版)(异步图书)
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

重构:改善既有代码的设计(第2版)(异步图书)
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,
调整其结构。
  1. 重构两项帽子
添加新功能:添加新功能时,我不应该 修改既有代码,只管添加新功能。

重构: 重构时我就不能再添加功能,只管调整代码的结构。
  1. 为何重构
重构改进软件的设计:程序的内部设计(或者叫架构)会逐渐腐败变质

重构使软件更容易理解:重构前,代码可以正常运行,但结构不够理想,让代码更好地表达自己的意图

重构帮助找到bug:重构,我就可以深入理解代码的所作所为,并立即把新的理解反映在代码当中

重构提高编程速度:随着开发的时间增加,累积的功能的效率会逐渐降低,所以通过重构可以容易找到哪里修改,如何修改,引入的bug的可能性也会变小
  1. 何时重构

有计划的重构和见机行事的重构

## 见机行事的重构

事不过三,三则重构:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无 论如何还是可以去做;第三次再做类似的事,你就应该重构。

预备性重构:让添加新功能更容易:在动手添加新功能之前,我会看看现 有的代码库,此时经常会发现:如果对代码结构做一点微调,我的工作会容易得多。

帮助理解的重构:使代码更易懂:看代码时,先理解代码在做什么,通过重构可以把脑子中的理解转移到代码本身

捡垃圾式重构:通过理解代码,发现代码中存在缺陷,做的不够好,这时候可以把比较复杂的问题先记下来,做完当下任务再回来重构,比较简单的就可以顺手重构掉
## 有计划的重构
专门安排一段时间来重构,而是在添加功能或修复bug的同时顺 便重构,但是项目计划上没有专门留给重构的时间,绝大多数重 构都在我做其他事的过程中自然发生。

## 长期重构

大多数重构可以在几分钟——最多几小时——内完成,有些大型的项目甚至需要花几个星期,但是重构的好处
绝对是非常高的,开发更快,少bug,更容易阅读,更容易理解,即便这样也很少会有让一个团队去专门做
重构,比较好的策略是,每当有人靠近重构去的代码就把代码往想要改进的地方推进一点。

复审代码时重构
项目每次上线前都会进行常规的代码复审(code review),通过团队成员一起审查,理解代码,找出不合理的代码,对老旧,不合理的代码进行重构,加入这一次迭代中,是个非常好的时间点

5 何时不应该重构哦

没有重复理解工作原理时,不应该进行重构

重写比重构简单就不用重构了

  1. 重构的挑战
延缓新功能开发: 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
 
代码所有权: 所重构的代码不仅影响一个模块内部,还和系统的其他部分有息息相关的关联,包括一起调用的api,方法等,一旦自助主张的修改,就一定会破坏使用者的程序

分支管理:代码重构时涉及多团队,多人员对同一块代码进行修改,这时候就需要好的分支管理,基本上会让团队人员各自创建一条分支,待开发完成之后,才会把各自修改合并到主线分支中

测试:每一次的重构都需要测试,需要对改动涉及的功能点跑之前测试的测试用例,防止重构导致功能被破坏。

遗留代码:历史遗留的代码在重构时也是一大难点,包括不规范的命名,没有注释,不统一的编码风格,导致很难理解代码的工作原理,让重构人员望而却步,困难重重。

数据库:数据库迁移,改动都需要对代码进行大量的修改,不管是字段,表名,索引等变动,对应到代码上以及查询逻辑就需要改动很多。

7.重构,架构

与其猜测未来需要哪些机制提供灵活性,根据当前的需求来构造软件,同时把设计质量做的很高,不仅需要
对代码重构,甚至需要对架构进行升级,重构的代码需要更好适配架构。在软件开发的过程中,自测试代 
码、持续集成、重构——彼此之间有着很强的协同效应。

8.重构与性能

代码重构很容易对程序的性能造成影响,所以需要测试用例以及压测等测试手段。为了保证性能可以采取一下方法

时间预算法:这通常只用 于性能要求极高的实时系统。如果使用这种方法,分解你的设计时就要做好预 
算,给每个组件预先分配一定资源,包括时间和空间占用。

持续关注法:这种方法要求任何程序员在任何时间做任何事时,都要设法保持系统的高性能。

统计数据法: 采用这种方法时,我编写 构造良好的程序,不对性能投以特别的关注,直至进入性能优化阶段——那通常 是在开发后期。在性能优化阶段,我首先应该用一个度量工具来监控程序的运行,让它告诉 我程序中哪些地方大量消耗时间和空间。然后集中关注这些性能热点。

9.自动化重构

自动化重构需要依赖工具或脚本(Refactoring Browser,IntelliJ IDEA或者Eclipse),不同的工具所支持的程度也不太一样。

重构工具不仅需要理解和修改语法树,还要知道如何把修改后的代码写回编辑器视图。总而言之,实现一个
体面的自动化重构手法,是一个很有挑战的编程任务。

代码坏味道

  1. 神秘命名:类一般采用大驼峰命名,方法和局部变量使用小驼峰命名,而大写下划线命名通常是常量和枚举中使用。

2.重复代码:如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。

3.过长函数:函数越长,就越难理解,应该限制一个方法中代码的行数。

  1. 过长的参数列表:过长的参数列表本身也经常令人迷惑,如果可以通过参数查询出第二个参数的值,就可以去掉第二个参数。

  2. 全局数据:全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机 制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了那些诡异的bug。

6.可变数据:对数据的修改经常导致出乎意料的结果和难以发现的bug。我在一处更新数 据,却没有意识到软件中的另一处期望着完全不同的数据。目前普遍都是支持数据可变的,所以只能通过对约束对数据的更新,降低其风险。

  1. 霰弹式修改:我们希望软件更容易被修改,但是却发现添加的功能需要在不同的地方修改才能达到目的,这种情况很容易漏掉部分,导致功能有bug产生,所以针对发散式变化,应该规范代码结构,让修改统一到一个方法,一个api,一个类中。

  2. 发散式变化:我们希望软件功能更容易修改,一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。该系统的点相当于多个不同的上下文,可以支持对不同功能的修改,这就是发散式变化。

9.依恋情结:一个函数跟另一个模块中的函数或者数据交流格 外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况,也就是我们常说的耦合性太强了

  1. 数据泥团:在项目中经常可以看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些 总是绑在一起出现的数据真应该拥有属于它们自己的对象。

11.基本类型偏执:指的是项目中对于变量类型修饰不太符合:如钱、坐标、范围等,是否等状态

  1. 重复的switch:重构:任何switch语句都应该用以多态取代条件表达式消除掉。

  2. 循环语句:循环语句太多,导致遍历次数太多,导致时间复杂度太高

14.冗赘的元素:程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者 哪怕只是提供更好的名字也好,但有时我们真的不需要这层额外的结构。

  1. 过大的类:如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。

  2. 注释:注释可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。

  3. 使用插件检测项目坏味道:项目中使用的是SonarLint

image.png