《重构》---- 读书笔记

《重构》---- 读书笔记

三月 10, 2021 本文共计: 3.4k 字 预计阅读时长: 11分钟

读《重构》(第二版)有感

开篇一些好的语录

  • 软件不死,重构不歇。----—余晟,《代码整洁之道:程序员的职业素养》译者
  • 重构的最终目的始终是为了减少重构。
  • 重构的先决条件是:测试先行。

第一章手札

  • 重构手法
我觉的最有感慨的还是对于多态取代条件表达式;
先来补一补,什么是多态
多态:一个事物的多种形态,例如动物中,小猫可以“喵喵喵”,小狗可以“汪汪汪”。
为什么需要多态?
究其最终原因还是为了实现代码重用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  class Movie {
constructor(name, price) {
this.name = name;
this.price = price;
}

get movieName() {
return this.name;
}

get moviePrice() {
return this.price;
}
}

/**
* @desc 恐怖电影
*/
class HorrorMovie extends Movie{
get movieName() {
return `恐怖片片名:${super.movieName}`
}
}

class ComedyMovie extends Movie{
get movieName() {
return `喜剧片片名:${super.movieName}`
}
}

const horrorMovie = new HorrorMovie('惊悚乐园', 18);
const comedyMovie = new ComedyMovie('小鬼当家', 27);
console.log(horrorMovie.movieName, comedyMovie.movieName)

以上就是多态的展现手法之一,重写。

当然。还可以通过方法重载去实现,不过js通常需要借助arguements去实现,利用typescript去强化实现则更佳。

个人观点:我觉的重构是什么?当读完第一章后,我觉的跟我之前理解的重构相差不大。

我们在每一次迭代中,尽量不去动那些运转正常的代码,当迭代或者需求变更时,那些“不该动”的代码,不得不动的时候,这个时候就需要警惕,我们需要重构了。

然而,重构需要遵循以下原则:

  • 不破坏以往的需求点
  • 重构之前有完整的测试用例支撑现有功能点正常运行,保证我们写的重构代码是“可以被信任的”,可以用于“生产环境的”。
  • 重构的目的是更好的维护与添加现有功能,而不是仅仅是当前迭代的重构,重构应该本着不需要再次重构的方向去靠拢。

第二章手札

带着下面的疑问,我们继续探索书籍的第二章:

  • 什么时候应该重构,什么时候不应该重构?
  • 重构势必会增加工作量,我们应该如何应对?
  • 好的代码需要被重构吗?

好的设计方法:

  • 简单设计
  • 增量式设计
  • YAGNI[mf-yagni] ——“你不会需要它”(you arenʼt going to need it)的缩写

重构与性能

克莱斯勒综合薪资系统的支付过程太慢了。虽然我们的开发还没结束,这个问题却已经开始困扰
我们,因为它已经拖累了测试速度。

Kent Beck、Martin Fowler和我决定解决这个问题。等待大伙儿会合的时间里,凭着对这个系统的
全盘了解,我开始推测:到底是什么让系统变慢了?我想到数种可能,然后和伙伴们谈了几种可能的
修改方案。最后,我们就“如何让这个系统运行更快”,提出了一些真正的好点子。
然后,我们拿Kent的工具度量了系统性能。我一开始所想的可能性竟然全都不是问题肇因。我们
发现:系统把一半时间用来创建“日期”实例(instance)。更有趣的是,所有这些实例都有相同的几个
值。

于是我们观察日期对象的创建逻辑,发现有机会将它优化。这些日期对象在创建时都经过了一个
字符串转换过程,然而这里并没有任何外部数据输入。之所以使用字符串转换方式,完全只是因为代
码写起来简单。好,也许我们可以优化它。

然后,我们观察这些日期对象是如何被使用的。我们发现,很多日期对象都被用来产生“日期区
间”实例——由一个起始日期和一个结束日期组成的对象。仔细追踪下去,我们发现绝大多数日期区间
是空的!

处理日期区间时我们遵循这样一个规则:如果结束日期在起始日期之前,这个日期区间就该是空
的。这是一条很好的规则,完全符合这个类的需要。采用此规则后不久,我们意识到,创建一个“起始
日期在结束日期之后”的日期区间,仍然不算是清晰的代码,于是我们把这个行为提炼成一个工厂函
数,由它专门创建“空的日期区间”。

我们做了上述修改,使代码更加清晰,也意外得到了一个惊喜:可以创建一个固定不变的“空日期
区间”对象,并让上述调整后的工厂函数始终返回该对象,而不再每次都创建新对象。这一修改把系统
速度提升了几乎一倍,足以让测试速度达到可接受的程度。这只花了我们大约五分钟。
我和团队成员(Kent和Martin谢绝参加)认真推测过:我们了若指掌的这个程序中可能有什么错
误?我们甚至凭空做了些改进设计,却没有先对系统的真实情况进行度量。

我们完全错了。除了一场很有趣的交谈,我们什么好事都没做。

教训是:哪怕你完全了解系统,也请实际度量它的性能,不要臆测。臆测会让你学到一些东西,
但十有八九你是错的。

                                       ——Ron Jeffries

上面的故事,其实也从反面思维告诉了我们一个道理,写好每一行代码是要义,还记得以前看过一句话一个优秀的程序员并不是满天飞的架构,而是他写的每一行代码

看完了一整章的内容,有必要回答刚开始的三个问题:

  • 什么时候应该重构,什么时候不应该重构?
    • 当一大堆“丑陋”的代码工作正常,但是不影响你正常开发下,我们并不需要重构它,因为可能会显式的增加你的工作量。那么,什么时候应该重构呢?当你的需求要求你不得不改动
      原有的“丑陋的代码”的时候,这个时候你就需要考虑重构了。
  • 重构势必会增加工作量,我们应该如何应对?
    • 提前做好预算是必须的,特别是时间预算,务必准备好你的“健全”的测试用例。
  • 好的代码需要被重构吗?
    • 答案是很显著的,好的代码的也需要被重构,随着每次迭代的进行,好的代码终将会有“不好”的那一面,始终记得我们一开始说过的“软件不死,重构不歇”。

第三章 手札

无论是重构还是设计代码,我们的起点始终是写好每一行代码。

好的代码的基本素养:

  • 好的命名:函数声明(124)(用于给函数改名)、变
    量改名(137)、字段改名(244)等,其实有经验的程序员都明白一个东西就是,好的命名是不需要加注释的。
  • 避免重复代码:简洁代码的要义的“抽象”,抽象一切可复用逻辑;
  • 过长函数的优化:短小精悍的函数,更容易阐释其意思。(这里的过长函数指的是函数体内容过长,而不是函数命名);
  • 过长参数列表的优化:对象序列化参数;
  • 抽离全局变量,添加作用域,避免污染全局;
  • 注释,一些注释是必须的,但是在写注释之前,请先考虑重构是否可以解决添加注释的必要;

当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。

小结:其实这一章节的内容主要还是阐述了如何去写好每一行代码,如何约束程序员去书写健壮的代码;’

第四章 手札

这一章节,其实重点的笔墨应该在如何准备“健全”的测试用例上。

编写未臻完善的测试并经常运行,好过对完美测试的无尽等待。

不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕
捉到大多数bug。

每当你收到bug报告,请先写一个单元测试来暴露这个bug。

其实,我个人属于敏捷开发爱好者,也就是TDD驱动者,不过常常因为工时制约了TDD的路途,
因为TDD的要义是:测试先行,由红变绿

至于应该进行什么测试黑盒测试或者白盒测试,取决于你的场景。

适当的补全一些BDD行为测试的用例,方便产品测试或者其他人员,像读短文一样读你的测试用例,在读懂测试用例的前提下,理解既有需求。

其实我觉得白盒测试更加适用于TDD敏捷开发思维,适用于单元测试层面。而黑盒测试更加适用于BDD思维,以行为驱动测试,以行为阐释需求。

End:关于解释“黑匣子”和“白匣子”,我觉得这里有一篇qs问答说的比较准确,也值得推敲。https://qastack.cn/software/27491/black-box-or-white-box-testing-which-do-you-do-first

web端qsstack传送门

第五章 手札

重构手法之核心要义

  • 首先是名称(name)。要建造一个重构词汇表,名称是很重要的。
  • 名称之后是一个简单的速写(sketch)。这部分可以帮助你更快找到你所需要
    的重构手法。
  • 动机(motivation)为你介绍“为什么需要做这个重构”和“什么情况下不该做这
    个重构”。
  • 做法(mechanics)简明扼要地一步一步介绍如何进行此重构。
  • 范例(examples)以一个十分简单的例子说明此重构手法如何运作。

貌似第五章仅仅是作者的介绍重构的前戏,欲听后戏如何,请见下章内容。

第六章 重构名录

低层级代码重构的精髓—形成函数并给函数命名。

提炼函数法则

alt

alt

核心观点阐释了:应该将“意图与实现分开”,“函数不要过长”;

做法:

  • 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而
    不是以它“怎样做”命名)
  • 将待提炼的代码从源函数复制到新建的目标函数中。
  • 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的
    新函数中访问不到的变量。若是,以参数的形式将它们传递给新函数。
  • 所有变量都处理完之后,编译。
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
  • 测试。
  • 查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以
    函数调用取代内联代码(222)令其调用提炼出的新函数。

alt

内联函数

alt

ALT

核心观点:属于函数内部的代码就尽量去内敛它。

做法:

  • 检查函数,确定它不具多态性。
  • 找出这个函数的所有调用点。
  • 将这个函数的所有调用点都替换为函数本体。
  • 每次替换之后,执行测试。
  • 删除该函数的定义。

提炼变量

提炼变量的法则其实很简单,就是将返回变量作为行内表达式,避免多声明一堆不必要的变量。

  • 检查确认变量赋值语句的右侧表达式没有副作用。
  • 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。

    这是为了确保该变量只被赋值一次。

  • 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
    测试。
  • 重复前面两步,逐一替换其他所有使用该变量的地方。
  • 删除该变量的声明点和赋值语句。
  • 测试。

摘录来自: 马丁·福勒(Martin Fowler). “重构:改善既有代码的设计(第2版)。” Apple Books.

改变函数声明

通常的手法就是给函数换一个名字,或者去除不必要的函数,以这个函数做了什么去描述,而不是如何去做命名。

封装变量

动机:数据不可变性

做法:提供 get | set方法给使用者使用。

变量改名

好的变量是代码阅读的基石;

函数组合成类

当一组函数形影不离的操作一段数据,那么就可以利用函数组合成类;

代码拆分

将耦合代码拆分;

第七章 封装

封装是面向对象的编程思想,好的封装手法可以更好的杜绝一些不必要的失误,也是扩展性的阐释。