第6章 第一组重构

news2024/12/19 19:53:28

        最常用到的重构就是用提炼函数(106)将代码提炼到函数中,或者用提炼变量(119)来提炼变量。既然重构的作用就是应对变化,你应该不会感到惊讶,我也经常使用这两个重构的反向重构——内联函数(115)和内联变量(123)。
        提炼的关键就在于命名,随着理解的加深,我经常需要改名。改变函数声明(124)可以用于修改函数的名字,也可以用于添加或删减参数。变量也可以用变量改名(137)来改名,不过需要先做封装变量(132)。在给函数的形式参数改名时,不妨先用引入参数对象(140)把常在一起出没的参数组合成一个对象。
        形成函数并给函数命名,这是低层级重构的精髓。有了函数以后,就需要把它们组合成更高层级的模块。我会使用函数组合成类(144),把函数和它们操作的数据一起组合成类。另一条路径是用函数组合成变换(149)将函数组合成变换式(transform),这对于处理只读数据尤为利。再往前一步,常常可以用拆分阶段(154)将这些模块组成界限分明的处理阶段。

6.1 提炼函数

何时应该把代码放进独立的函数?

   “将意图与实现分开”:如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。

    创造一个新函数,根据这个函数的意图来对它命名(以它做什么来命名,而不是以它怎样做命名)。

    一旦接受了这个原则,我就逐渐养成一个习惯:写非常小的函数——通常只有几行的长度。在我看来,一个函数一旦超过6行,就开始散发臭味

    有些人担心短函数会造成大量函数调用,因而影响性能。在我尚且年轻时,有时确实会有这个问题;但如今“由于函数调用影响性能”的情况已经非常罕见了。短函数常常能让编译器的优化功能运转更良好,因为短函数可以更容易地被缓存。所以,应该始终遵循性能优化的一般指导方针,不用过早担心性能问题。

6.1.1 范例:无局部变量

function printOwing(invoice) {

 let outstanding = 0;
 console.log("***********************");
 console.log("**** Customer Owes ****");
 console.log("***********************");

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 // record due date
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

 //print details
 console.log(`name: ${invoice.customer}`);
 console.log(`amount: ${outstanding}`);
 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);

}

我们可以轻松提炼出“打印横幅”的代码。

function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 // record due date
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

 //print details
 console.log(`name: ${invoice.customer}`);
 console.log(`amount: ${outstanding}`);
 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}


function printBanner() {
 console.log("***********************");
 console.log("**** Customer Owes ****");
 console.log("***********************");
}

同样,我还可以把“打印详细信息”部分也提炼出来:

function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 // record due date
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

 printDetails();


 function printDetails()
 {
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

6.1.2 范例:有局部变量

局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况下我可以简单地将它们当作参数传给目标函数。所以,如果我面对下列函数:

function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding

 for (const o of invoice.orders) {

  outstanding += o.amount;

 }

 // record due date

 const today = Clock.today;

 invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

 //print details

 console.log(`name: ${invoice.customer}`);

 console.log(`amount: ${outstanding}`);

 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);

}

就可以将“打印详细信息”这一部分提炼为带两个参数的函数:

function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding

 for (const o of invoice.orders) {

  outstanding += o.amount;

 }

 // record due date

 const today = Clock.today;

 invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

 printDetails(invoice, outstanding);

}

function printDetails(invoice, outstanding) {

 console.log(`name: ${invoice.customer}`);

 console.log(`amount: ${outstanding}`);

 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);

}

如果局部变量是一个数据结构(例如数组、记录或者对象),而被提炼代码段又修改了这个结构中的数据,也可以如法炮制。所以,“设置到期日”的逻辑也可以用同样的方式提炼出来:

function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);
 printDetails(invoice, outstanding);
}

function recordDueDate(invoice) {
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

}

6.1.3 范例:对局部变量再赋值

function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);

 printDetails(invoice, outstanding);
}

首先,把变量声明移动到使用处之前。

使用 移动语句 的手法

function printOwing(invoice) {

 printBanner();

 // calculate outstanding
 let outstanding = 0;
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);

 printDetails(invoice, outstanding);

}

然后把想要提炼的代码复制到目标函数中。

function printOwing(invoice) {

 printBanner();

 // calculate outstanding
 let outstanding = 0;
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);

 printDetails(invoice, outstanding);
}

function calculateOutstanding(invoice) {
 let outstanding = 0;

 for (const o of invoice.orders) {
    outstanding += o.amount;
 }

 return outstanding;
}

由于outstanding变量的声明已经被搬移到提炼出的新函数中,就不需要再将其作为参数传入了。outstanding是提炼代码段中唯一被重新赋值的变量,所以我可以直接返回它。

下一件事是修改原来的代码,令其调用新函数。新函数返回了修改后的outstanding变量值,我需要将其存入原来的变量中。

function printOwing(invoice) {

 printBanner();

 let outstanding = calculateOutstanding(invoice);

 recordDueDate(invoice);

 printDetails(invoice, outstanding);

}



function calculateOutstanding(invoice) {

 let outstanding = 0;

 for (const o of invoice.orders) {

  outstanding += o.amount;

 }

 return outstanding;

}

在收工之前,我还要修改返回值的名字,使其符合我一贯的编码风格。

function printOwing(invoice) {

 printBanner();

 const outstanding = calculateOutstanding(invoice);

 recordDueDate(invoice);

 printDetails(invoice, outstanding);

}

 function calculateOutstanding(invoice) {
 let result = 0;

 for (const o of invoice.orders) {
  result += o.amount;
 }

 return result;
}

6.2 内联函数

function getRating(driver)
{
 return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver)
{
 return driver.numberOfLateDeliveries > 5;
}

function getRating(driver)

{

 return (driver.numberOfLateDeliveries > 5) ? 2 : 1;

}

      函数内部代码和函数名称同样清晰易读

    本书经常以简短的函数表现动作意图,这样会使代码更清晰易读。但有时候你会遇到某些函数,其内部代码和函数名称同样清晰易读。也可能你重构了该函数的内部实现,使其内容和其名称变得同样清晰。若果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。

    另一种需要使用内联函数的情况是:我手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以我喜欢的方式重新提炼出小函数。

    如果代码中有太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用内联函数。当然,间接层有其价值,但不是所有间接层都有价值。通过内联手法,我可以找出那些有用的间接层,同时将无用的间接层去除。

6.3 提炼变量(Extract Variable)

反向重构:内联变量(123)

 动机
        表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。这样的变量在调试时也很方便,它们给调试器和打印语句提供了便利的抓手。

做法
a. 确认要提炼的表达式没有副作用。

b. 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。

c. 用这个新变量取代原来的表达式。
d. 测试。
如果该表达式出现了多次,请用这个新变量逐一替换,每次替换之后都要执行测试。

范例:在一个类中

下面是同样的代码,但这次它位于一个类中:

我要提炼的还是同样的变量,但我意识到:这些变量名所代表的概念,适用于整个Order类,而不仅仅是“计算价格”的上下文。既然如此,我更愿意将它们提炼成方法,而不是变量。

        这是对象带来的一大好处:它们提供了合适的上下文,方便分享相关的逻辑和数据。在如此简单的情况下,这方面的好处还不太明显;但在一个更大的类当中,如果能找出可以共用的行为,赋予它独立的概念抽象,给它起一个好名字,对于使用对象的人会很有帮助。

6.4 内联变量(Inline Variable)

反向重构:提炼变量(119)

动机
        在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。

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

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

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

6.5 改变函数声明

auto circum(double radius){ … }

Auto circumference(double radius){ … }

        一个好名字能让我一眼看出函数的用途,而不必查看其实现代码。但起一个好名字并不容易,有一个改进函数名字的好办法:先写一句注释描述这个函数的用途,再把这句注释变成函数的名字。

        对于函数的参数,道理也是一样。函数的参数列表阐述了函数如何与外部世界共处。函数的参数设置了一个上下文,只有在这个上下文中,我才能使用这个函数。假如有一个函数的用途是把某人的电话号码转换成特定的格式,并且该函数的参数是一个人(person),那么我就没法用这个函数来处理公司(company)的电话号码。如果我把函数接受的参数由“人”改成“电话号码”,这段处理电话号码格式的代码就能被更广泛地使用。

        修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件,从而去除不必要的耦合。

6.6 封装变量[封装到函数,通过函数操作]

重构的作用就是调整程序中的元素。函数相对容易调整一些,因为函数只有一种用法,就是调用。在改名或搬移函数的过程中,总是可以比较容易地保留旧函数作为转发函数(即旧代码调用旧函数,旧函数再调用新函数)。这样的转发函数通常不会存在太久,但的确能够简化重构过程。

     数据就要麻烦得多,因为没办法设计这样的转发机制。如果我把数据搬走,就必须同时修改所有引用该数据的代码,否则程序就不能运行。如果数据的可访问范围很小,比如一个小函数内部的临时变量,那还不成问题。但如果可访问范围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。

    所以,如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。这样,我就能把“重新组织数据”的困难任务转化为“重新组织函数”这个相对简单的任务。

    封装数据的价值还不止于此。封装能提供一个清晰的观测点,可以由此监控数据的变化和使用情况;我还可以轻松地添加数据被修改时的验证或后续逻辑。我的习惯是:对于所有可变的数据,只要它的作用域超出单个函数,我就会将其封装起来,只允许通过函数访问。数据的作用域越大,封装就越重要。

范例

下面这个全局变量中保存了一些有用的数据:

let defaultOwner = {firstName: "Martin", lastName: "Fowler"};

使用它的代码平淡无奇:

spaceship.owner = defaultOwner;

更新这段数据的代码是这样:

defaultOwner = {firstName: "Rebecca", lastName: "Parsons"};

首先我要定义读取和写入这段数据的函数,给它做个基础的封装。

function getDefaultOwner() {return defaultOwner;}

function setDefaultOwner(arg) {defaultOwner = arg;}

然后就开始处理使用defaultOwner的代码。每看见一处引用该数据的代码,就将其改为调用取值函数。

spaceship.owner = getDefaultOwner();

每看见一处给变量赋值的代码,就将其改为调用设

值函数。

setDefaultOwner({firstName: "Rebecca", lastName: "Parsons"});

6.7 变量改名

 

        好的命名是整洁编程的核心。变量可以很好地解释一段程序在干什么——如果变量名起得好的。使用范围越广,名字的好坏就越重要。 

6.8 引入参数对象

 我常会看见,一组数据项总是结伴同行出没于一个又一个函数。这样一组数据就是所谓的数据泥团。

    将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰。使用新的数据结构,参数的参数列表也能缩短。并且经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。

    可能只是一组共用的函数,也可能用一个类把数据结构与使用数据的函数组合起来。

 6.9 函数组合成类

        类,在大多数现代编程语言中都是基本的构造。它们把数据与函数捆绑到同一个环境中,将一部分数据与函数暴露给其他程序元素以便协作。它们是面向对象语言的首要构造,在其他程序设计方法中也同样有用。

        如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。

6.10 函数组合成变换

        函数组合成变换的替代方案是函数组合成类(144),后者的做法是先用源数据创建一个类,再把相关的计算逻辑搬移到类中。这两个重构手法都很有用,我常会根据代码库中已有的编程风格来选择使用其中哪一个。不过,两者有一个重要的区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。

 6.11 拆分阶段

        每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块,因为这样到了需要修改的时候,我就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。如果运气够好的话,我可能只需要修改其中一个模块,完全不用回忆起另一个模块的诸般细节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2262341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于python对网页进行爬虫简单教程

python对网页进行爬虫 基于BeautifulSoup的爬虫—源码 """ 基于BeautifulSoup的爬虫### 一、BeautifulSoup简介1. Beautiful Soup提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为用户提供需要…

C语言中文件是什么?文件文本和二进制文件的区别

1、C语言中文件是什么? 我们对文件的概念已经非常熟悉了,比如常见的 Word 文档、txt 文件、源文件等。文件是数据源的一种,最主要的作用是保存数据。 在操作系统中,为了统一对各种硬件的操作,简化接口,不同…

vmware workstation pro上创建虚拟机

vmware workstation pro上创建虚拟机 下载vmware workstation pro软件安装后并运行点击主页,选择创建虚拟机 创建虚拟机成功后会出现如下界面 可以点击设置按钮删除不需要的硬件,也可以添加新的硬件设备,最终硬件信息如下图 至此虚拟机…

【数学建模】利用Matlab绘图(2)

一、Matlab中plot函数的基本用法 在matlab中,函数的基本用法主要包括以下几种 第一类: plot(X,Y,LineSpec) 第二类: plot(tbl,xvar,yvar) 1.1 第一类 1.1.1x-y坐标 x和y的选择取决于绘图所需的数据类型以及图像的类型。下表列出了几种…

ASP.NET Core - 依赖注入 自动批量注入

依赖注入配置变形 随着业务的增长,我们项目工作中的类型、服务越来越多,而每一个服务的依赖注入关系都需要在入口文件通过Service.Add{}方法去进行注册,这将是非常麻烦的,入口文件需要频繁改动,而且代码组织管理也会变…

Oracle 适配 OpenGauss 数据库差异语法汇总

背景 国产化进程中,需要将某项目的数据库从 Oracle 转为 OpenGauss ,项目初期也是规划了适配不同数据库的,MyBatis 配置加载路径设计的是根据数据库类型加载指定文件夹的 xml 文件。 后面由于固定了数据库类型为 Oracle 后,只写…

Kubeadm+Containerd部署k8s(v1.28.2)集群(非高可用版)

Kubeadm+Containerd部署k8s(v1.28.2)集群(非高可用版) 文章目录 Kubeadm+Containerd部署k8s(v1.28.2)集群(非高可用版)一.环境准备1.服务器准备2.环境配置3.设置主机名4.修改国内镜像源地址5.配置时间同步6.配置内核转发及网桥过滤二.容器运行时Containerd安装(所有节点)…

[LeetCode-Python版]21. 合并两个有序链表(迭代+递归两种解法)

题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1: 输入:l1 [1,2,4], l2 [1,3,4] 输出:[1,1,2,3,4,4] 示例 2: 输入:l1 [], l2 [] 输出&#x…

MATLAB引用矩阵元素的几种方法

引用矩阵元素可以通过索引,也可以通过逻辑值 索引 通过引用元素在矩阵中的位置来提取元素,例如: - 逻辑值 通过某种逻辑运算来使得要提取的值变为逻辑 1 1 1,用 A ( ) A() A()提取即可, A A A为原矩阵的名称。 例如&…

sql 批量修改字段 的默认值

SELECT COLUMN_NAME, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA financeproject AND TABLE_NAME finance_balance AND DATA_TYPE decimal; 查出的字段 excel 拼接 修改语句 ALTER TABLE finance_income MODIFY COLUMN yy…

CVE-2023-0562【春秋云镜】

目录 CVE-2023-0562漏洞概述漏洞利用方式影响范围修复建议安全编码示例靶标介绍 CVE-2023-0562 CVE-2023-0562 是一个针对银行储物柜管理系统的SQL注入漏洞。该漏洞影响了储物柜管理系统中处理用户输入的部分,攻击者可以利用此漏洞未经授权地访问数据库中的敏感信息…

vue el-dialog实现可拖拉

el-dialog实现拖拉&#xff0c;每次点击度居中显示&#xff0c;以下贴出代码具体实现&#xff0c;我是可以正常拖拉并且每次度显示在中间&#xff0c;效果还可以&#xff0c;需要的可以丢上去跑跑 组件部分&#xff1a; <el-dialog:visible.sync"dialogVisible"…

MySQL:库和表的操作

目录 一. 查看数据库 二. 创建数据库 三. 字符集和校验规则 四. 修改和删除数据库 4.1 数据库修改 4.2 数据库删除 五. 备份与恢复 5.1 备份 5.2 还原 5.3 注意事项 5.4 查看连接情况 六. 创建表 七. 查看表结构 八. 修改表 九. …

gitlab初始化+API批量操作

几年没接触gitlab了&#xff0c;新版本装完以后代码提交到默认的main分支&#xff0c;master不再是主分支 项目有几十个仓库&#xff0c;研发提交代码后仓库地址和之前的发生了变化 有几个点 需要注意 1、修改全局默认分支 2、关闭分支保护 上面修改了全局配置不会影响已经创…

Java集合(完整版)

集合框架 Collection集合 概念&#xff1a;对象的容器&#xff0c;定义了对多个对象进行操作的常用方法。可以实现数组的功能 和数组的区别&#xff1a; 数组的长度固定&#xff0c;集合长度不固定数组可以存储基本类型和引用类型&#xff0c;集合只能存储引用类型 Collec…

常耀斌:深度学习和大模型原理与实战(深度好文)

目录 机器学习 深度学习 Transformer大模型架构 人工神经元网络 卷积神经网络 深度学习是革命性的技术成果&#xff0c;有利推动了计算机视觉、自然语言处理、语音识别、强化学习和统计建模的快速发展。 深度学习在计算机视觉领域上&#xff0c;发展突飞猛进&#xff0c;…

不能通过 ip 直接访问 共享盘 解决方法

from base_config.config import OpenSMB, SMB import os, time, calendar, requests, decimal, platform, fs.smbfsinfo_dict SMB.EPDI_dict info_dict[host] (FS03,10.6.12.182) info_dict[direct_tcp] True# smb OpenSMB(info_dict)print(ok)# 根据 ip 查询电脑名 impor…

Mapbox-GL 的源码解读的一般步骤

Mapbox-GL 是一个非常优秀的二三维地理引擎&#xff0c;随着智能驾驶时代的到来&#xff0c;应用也会越来越广泛&#xff0c;关于mapbox-gl和其他地理引擎的详细对比&#xff08;比如CesiumJS&#xff09;&#xff0c;后续有时间会加更。地理首先理解 Mapbox-GL 的源码是一项复…

HIVE4.0.1在Hadoop HA部署hiveserver2模式

本文基于CENTOS7&#xff0c;在Hadoop3.4.0版本vm虚拟机3节点HA集群的基础上进行的搭建。 一、前置条件 本文使用MySQL8.0.26作为HIVE数据库&#xff0c;不使用hive自带的derby数据库&#xff0c;因为其不支持多客户端访问&#xff0c;也不方便查询。 所以必须先安装MySQL。版本…

Visual Studio 使用 GitHub Copilot 协助调试

&#x1f380;&#x1f380;&#x1f380;【AI辅助编程系列】&#x1f380;&#x1f380;&#x1f380; Visual Studio 使用 GitHub Copilot 与 IntelliCode 辅助编码Visual Studio 安装和管理 GitHub CopilotVisual Studio 使用 GitHub Copilot 扩展Visual Studio 使用 GitHu…