首页 > 资讯 > 综合 > 正文
2024-03-10 07:32

《重构——改进现有代码的设计》

如果函数的名称不能表达它在做什么,则需要对其进行重命名,以使代码更清晰、更容易理解。 两种方法:

1.直接更改名称,全局搜索替换

2. 保留旧函数进行转发(如果外部用户调用您的API)

封装变量

如果一个变量在很多地方改变了它的值,那么就需要通过函数来​​封装该变量。 益处:

1.方便调试和关闭,知道谁改变了变量的值

2. 如果该变量需要与其他变量联动,直接在方法内部进行联动即可。 不需要大规模改动其他地方的代码。

变量重命名

良好的命名是编程的核心

引入参数对象

即将函数的输入参数改为对象形式。 如果一组数据总是在一起,出现在一个又一个函数中,这就是所谓的数据泥,需要将这些数据封装成一个类。 这实现了数据的所有本地访问标准。

function bookHotel(startData,endData)
function unBook(startData,endData)
//应该改成
function bookHotel(bookData)
function unBook(bookData)

函数组合成类

如果几个函数总是密不可分地操作相同的数据,并且A的返回是B的输入参数,那么将A和B合并到同一个类中。 益处:

1.减少参数传递,参数传递可以通过类的上下文来实现

2、方便继续抽象,更合理地设计这些方法。

函数组合成转换

将多个函数组合成一个大函数与组合多个函数类似,但并不是什么新鲜事。

分阶段

如果一段代码同时处理两件事,请将它们分成各自的模块。 实现了这种分层之后,我们在改变其中一个模块时就不用考虑其他模块的细节了。 方法:将一大块行为分解为两个阶段顺序执行。 一个典型的例子是编译器:文本被转换为 AST,AST 生成目标代码。 这些阶段非常清晰。

第 7 章:封装

封装的意义就是隐藏内部的实现细节不暴露给外界,外界可以直接使用暴露的能力。

封装记录

通常,数据库中存储的记录是一张一张的POJO。

封装集合

开发者在进行封装时,常常忘记屏蔽集合对象本身不暴露给外界。

class Persion{
    getList(){return this._list}
  setList(list){this._list = list}
}
// 上面的封装会把集合直接暴露出去,并没有起到封装的效果
class Persion{
  add(item){
    this._list.add(item)
  }
}

用对象替换基本类型

比如对于区号这样的服务,很多程序员通常都想用或者Int来表示。 我们完全可以封装一个区号对象来表达区号。 类似的还有金额(货币种类、数量)等。

用查询替换临时变量

如果临时变量只分配一次,则无需使用该变量。 您可以直接通过该方法获取变量的值。 PS:这种方法调试起来比较困难。 我个人认为这个东西没什么用。

精致类

如果一个类同时做很多事情,那么它的职责就不够单一,改变一个地方很容易导致其他地方的bug。 信号:

class Person{
  get officeAreaCode() {return this._officeAreaCode}
  get officeNumber(){return this._officeNumber}
}
// 需要改成
class Person{
  get officeAreaCode() {return this._telephoneNumber.officeAreaCode}
  get officeNumber(){return this._telephoneNumber.officeNumber}
}
class TelephoneNumber{
  get areaCode() {return this._areaCode}
  get number(){return this._number}
}

内联类

与3.7.5的方法相反,如果一个类不足以承担责任,那么通过内联方法将其塞到另一个类中。本质上仍然是单一责任原则。 如果他不能承担相应的责任,那就开除他。

隐藏委托关系

通过封装,对外暴露的只是能力,用户不需要知道具体的实现细节。好处:如果调用关系发生变化,用户不需要注意到,被调用者只需要做桥接即可。

去掉中间商

与3.7.7方法相反,3.7.7方法的优点是调用者感知不到具体细节。 缺点是每次被调用者增加一个新的能力供别人调用时,中间人都需要增加一层转发功能。 如果中间人需要转发太多,系统又不会频繁调用关系变更,那么直接去掉中间人就可以了。

替换算法

经过不断的优化,用简短、高效、清晰的算法来描述你的业务逻辑中的狗屎代码。

第 8 章:移动特性

上面介绍的方法都是通过新建、删除、重命名等方式进行重构。本章主要讲的是在不同上下文之间移动元素进行重构。

移动功能

将函数移动到更需要它的模块。原因:如果一个函数经常使用其他上下文中的元素,而对自己上下文中的元素知之甚少,请将其移动到它关心的上下文中。

移动字段

如果你发现在调用某些函数时,经常需要传入多个字段,而这些字段又不在同一个地方,那就将这些字段移到同一个模块中进行维护,这样在制作时只需要改一个地方变化。 。 扩展:设计好你的数据结构。 数据结构是健壮程序的基础。 适应问题领域的良好数据结构可以使行为代码更加简单,而糟糕的数据结构会导致大量无用的代码。

将语句移至函数

本质是消除重复。 如果许多调用者在调用函数之前都做了同样的事情,那么将这些相同的事情直接放入函数中,这样调用者就不必做同样的事情了。 代码重复也将被消除

function callerA(){
  // check sth
  print();
}

function callerB(){
  // check sth
  print();
}
function print(){
  do sth
}

// 应该改成 
function print(){
  // check sth
  do sth
}

将语句移至调用者

如果许多调用同一个函数的调用者在调用点前面经常表现不同,那么将这些不同的行为从函数中分离出来,并将它们放在调用者内部。 不要使用 from 之类的字段来区分内部函数。

用函数调用替换内联代码

如果一个大函数中包含实现特定功能的代码,则将代码分离成一个单独的函数并命名好。好处:后续的维护者看到这个小函数名就可以知道这段代码是做什么的,复杂的代码会让后续的人感到困惑人们。

function getNameById(id){
  let result = allPersion.find((item) => {return item.id === id})
  return result.name
}
// 改成
function getNameById(id){
  let persion = getPersionById(id)
  return persion.name
}
function getPersonById(id){
  return allPersion.find((item) => {return item.id === id})
}

移动声明

作者认为,操作同一个变量的语句应尽可能放在一起,以方便查找和阅读。 PS:我个人认为是否放在一起的决定应该根据业务逻辑的耦合程度来决定,而不是单纯是不是一个变量。

分割循环

如果你在同一个循环中做两件事,那么当你分别改变这两件事时,你必须改变循环体,这可能会互相影响,违反单一责任原则。 笔者认为此时应该打破循环。 即使循环多次,也要保证循环中只做一件事情。 PS:这会影响性能,谨慎使用

管道代替循环

使用管道运算符,例如 .map 。 而不是循环 PS:花哨但没用

删除死代码

如果没有人调用该代码,就将其删除。 不要注释掉它。 你可以通过git记录找到这些代码。 不要将这些死代码放入文件中。

第 9 章:重新组织数据分割变量

每个变量应该只表达一件事。 如果同一个变量扮演多个角色,则需要创建一个新变量来使用它(循环体中的临时变量是另一回事(let i=0;i

let temp = persion.name;
console.log(temp)
temp = persion.sex
console.log(temp)
// 上面的代码应该拆成两个变量  name  sex

字段重命名

如果是程序中广泛使用的数据结构,字段的命名就非常重要,这对于读者阅读代码很有帮助。 通常你可以使用IDE的自动分析工具来帮助你更改名称,而不必全局搜索代码。

字段对应的概念_比较两个字段是否一致_内联字段比较

用查询替换派生变量

作者认为,可变数据结构是软件中最大的错误来源之一,因此尝试将可变数据结构限制在一个范围内可以最大程度地避免这些错误。 因此,如果一个变量很简单,在使用时可以通过调用函数来获取。 无需设置单独的临时变量存储。 PS:个人认为没啥用

将引用对象转换为值对象

本质上是维持对象的不变性或者消除可变数据源。 但如果需要多处修改对象,这种值对象就非常麻烦(需要来回复制),而引用对象仍然需要保留。

将值对象转换为引用对象

与上面的方法相反,如果多个地方需要共享一个对象,那么它们都共享该对象的引用,直接操作该对象的属性。

第 10 章:简化条件逻辑分解条件表达式

这只是一个细化功能的应用场景。 目的是将大量的计算代码从if else语句中分离出来,以便维护者能够快速了解​​程序的走向。 提取函数的两个步骤:

1. 将大函数分解为小函数

2. 将小函数与适当的名称相匹配

if(a){
  // 1
  // 2 
  // 3
}else{
  // 4
  // 5
  // 6
}

// 替换成
if(a){
  callA();
}else{
  callB();
}

function callA(){
  // 1
  // 2 
  // 3
}

合并条件表达式

如果检查条件不同但最终行为相同,可以通过组合条件表达式来实现。 PS:这个事情有待讨论。 如果以下行为不一致,则必须进行拆分。

if(a) return 0
if(b) return 0
if(c) return 0
// 替换成
if(a || b || c){
  return 0
}

用保护语句替换嵌套条件表达式

Guard语句:如果判断条件为真,笔者认为没必要一直让程序只有一个入口和出口。 保持代码清晰是最关键的。

if(a) return 0
if(b) return 0
if(c) return 0

用多态性替换条件表达式

用多态能力一致性来替代大量案例,这已经是老生常谈了。 如果是比较简单的判断语句,那么就没有必要费力的使用多态了。 如果判断逻辑特别复杂,引入多态可以有效解决复杂性问题。

介绍特殊情况

书上说的太复杂了。 保持简单:如果在很多地方检查一个值的合法性,只需使用单独的函数来消除重复即可。

引入断言

每种语言都不同,这取决于。

第 11 章:重构 API 以将查询函数与修改函数分开

有副作用的函数(可以设置值)和没有副作用的函数(只能)应该分开,这样后续维护者就不会担心没有副作用的函数在调试时偷偷改变某些值。

函数参数化

如果你的代码中有很多函数做了80%相似的事情和20%不同的事情,那么你应该考虑将20%的内容以参数的形式传递到函数中,然后消除这些函数的重复部分。

删除标记参数

如果一个函数通过某些输入参数执行不同的逻辑,最好直接拆分成两个函数,这样更清晰。 当然,这种方法也要视情况而定。 如果有多个标记参数,就无法拆掉了~

function setWidthOrHeight(size,isWidth){
  if(isWidth){
    width = size
  }else{
    height = size
  }
}
// 直接拆成两个函数就可以了
function setWidth(width)
function setHeight(height)

保持物体完好无损

如果在调用函数之前导出了几个值,然后将这些值传递给函数,那么对象就直接传递给函数并在函数内部解构。

const {w,h} = square;
function area(w,h){return w*h}
// 直接改成 
function area(square){
  const {w,h} = square;
  return w * h;  
}

用查询替换参数

如果一个函数有很多输入参数,并且某些输入参数可以从其他输入参数推导出来,那么就去掉这些可推导的输入参数,并通过推导来替换它们。 好处:保持耦合最小化 另外,保持函数幂等性也很重要,这样的函数很容易测试

用参数替换查询

上述方法的逆操作方法是,如果你的函数引用了一些全局变量,则将这些参数提取到函数的输入参数中,通过调用传入。 好处:保持函数幂等!

删除设置器功能

这种做法有点极端。 本质上,作者希望保持字段不可变。如果您不希望其他人更改某个字段,则杀死 函数并保持该字段对外部不可见。

用工厂函数替换构造函数

我没有看到任何好处。 盲猜的好处是构造对象的地方封闭在同一个地方。

用命令替换函数

与普通函数调用相比,命令对象提供了更大的控制灵活性和更强的表达能力。 除了函数调用本身之外,命令对象还可以支持其他操作,例如撤消操作。 (当然,太灵活也不是好事,很难调试和控制。)笔者也认为,在95%的场景下,他不会选择使用命令而不是函数。

用函数替换命令

就像上面提到的命令对象的缺点一样,如果你的业务场景不复杂,直接使用函数调用就可以了,不用使用命令。

上移继承功能

如果很多子类中都需要同一个函数,那么直接将该函数的代码移到父类中

向上移动字段

如果子类有很多重复的特性,则将其重复的字段和对这些字段进行操作的函数同时移至父类。 本质上,它消除了重复。

构造函数主体向上移动

如果很多子类在构造函数中都有相同的行为,那么将这些相同的行为上移到父类中,子类就可以在构造函数中调用super(xxx)。

函数向下移动

如果超类中的某个(或几个)函数只与某个子类相关,则将这些东西从超类中移走

向下移动字段

同上,如果一个字段及其处理函数只被子类引用,那么就把这些东西从超类中移走

用子类替换类型代码

之前没用过,示例代码如下

function createEmployee(name,type){
  return new Employee(nanme,type)
}
// 改成
function createEmployee(name,type){
  switch (name){
    case 'man' return createManEmplo(name);
    case 'woman' return createWomanEmplo(name)
  }
}

删除子类

随着软件的发展,子类支持的功能可能会被转移到其他地方。 如果子类的功能太少,就去掉这个子类。 子类的存在是有成本的,人们需要了解它的用途。

精炼超类

合理的继承关系是在进化过程中获得的,并不是一开始就设计好的。 我们需要在继承和委托之间进行选择,目的是将共同元素提取到同一个地方。

用委托替换子类化

继承的优点是消除重复代码,但其缺点也很明显:

1.传承只能玩一次。 如果后续子类向不同方向演化,继承显然是不合适的。

2、继承引入了类之间非常重的耦合关系。 对超类所做的任何修改都会影响子类。

上述两个问题可以通过委托来解决。 不同的调用点通过委托调用同一个类中的方法。 这就是组合优于继承的表现。

用委托替换超类

与上面相同