CH11_重构API
将查询函数和修改函数分离(Separate Query from Modifier)
function getTotalOutstandingAndSendBill() {const result = customer.invoices.reduce((total, each) => each.amount + total, 0);sendBill();return result;
}
function totalOutstanding() {return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {emailGateway.send(formatBill(customer));
}
动机
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。这种函数的测试也更容易。
明确表现出“有副作用”与“无副作用”两种函数之间的差异,是个很好的想法。下面是一条好规则:任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离(Command-Query Separation)[mf-cqs]。
做法
- 复制整个函数,将其作为一个查询来命名。
- 从新建的查询函数中去掉所有造成副作用的语句。
- 执行静态检查。
- 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试。
- 从原函数中去掉返回值。
- 测试。
完成重构之后,查询函数与原函数之间常会有重复代码,可以做必要的清理。
函数参数化(Parameterize Function)
曾用名:令函数携带参数(Parameterize Method)
function tenPercentRaise(aPerson) {aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {aPerson.salary = aPerson.salary.multiply(1.05);
}
function raise(aPerson, factor) {aPerson.salary = aPerson.salary.multiply(1 + factor);
}
动机
如果发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。
做法
- 从一组相似的函数中选择一个。
- 运用改变函数声明(124),把需要作为参数传入的字面量添加到参数列表中。
- 修改该函数所有的调用处,使其在调用时传入该字面量值。
- 测试。
- 修改函数体,令其使用新传入的参数。每使用一个新参数都要测试。
- 对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试。
移除标记参数(Remove Flag Argument)
曾用名:已明确函数取代参数(Replace Parameter with Explicit Methods)
function setDimension(name, value) {if (name === "height") {this._height = value;return;}if (name === "width") {this._width = value;return;}
}
function setHeight(value) {this._height = value;}
function setWidth (value) {this._width = value;}
动机
“标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。这类参数让人难以理解到底有哪些函数可以调用、应该怎么调用。
如果明确用一个函数来完成一项单独的任务,其含义会清晰得多。
标记参数区分:如果调用者传入的是程序中流动的数据,这样的参数不算标记参数;只有调用者直接传入字面量值,这才是标记参数。在函数实现内部,如果参数值只是作为数据传给其他函数,这就不是标记参数;只有参数值影响了函数内部的控制流,这才是标记参数。
做法
-
针对参数的每一种可能值,新建一个明确函数。
如果主函数有清晰的条件分发逻辑,可以用分解条件表达式(260)创建明确函数;否则,可以在原函数之上创建包装函数。
-
对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。
保持对象完整(Preserve Whole Object)
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high))
if (aPlan.withinRange(aRoom.daysTempRange))
动机
“传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,就不用为此修改参数列表。如果有很多函数都在使用记录中的同一组数据,处理这部分数据的逻辑常会重复,此时可以把这些处理逻辑搬移到完整对象中去。
做法
- 新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)。
- 在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据)。
- 执行静态检查。
- 逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
- 所有调用处都修改过来之后,使用内联函数(115)把旧函数内联到新函数体内。
- 给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同时修改所有调用处。
以查询取代参数(Replace parameter with Query)
曾用名:以函数取代参数(Replace parameter with Method)
反向重构:以参数取代查询(Replace Query with Parameter)
availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {// calculate vacation...
}
availableVacation(anEmployee)
function availableVacation(anEmployee) {const grade = anEmployee.grade;// calculate vacation...
}
动机
函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异的主要方式。和任何代码中的语句一样,参数列表应该尽量避免重复,并且参数列表越短就越容易理解。
如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。
不使用以查询取代参数最常见的原因是,移除参数可能会给函数体增加不必要的依赖关系。
如果想要去除的参数值只需要向另一个参数查询就能得到,这是使用以查询取代参数最安全的场景。如果可以从一个参数推导出另一个参数,那么几乎没有任何理由要同时传递这两个参数。
做法
- 如果有必要,使用提炼函数(106)将参数的计算过程提炼到一个独立的函数中。
- 将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试。
- 全部替换完成后,使用改变函数声明(124)将该参数去掉。
以参数取代查询(Replace Query with Parameter)
反向重构:以查询取代参数(Replace parameter with Query)
targetTemperature(aPlan)function targetTemperature(aPlan) {currentTemperature = thermostat.currentTemperature;// rest of function...
}
targetTemperature(aPlan, thermostat.currentTemperature)function targetTemperature(aPlan, currentTemperature) {// rest of function...
}
动机
需要使用本重构的情况大多源于想要改变代码的依赖关系——为了让目标函数不再依赖于某个元素,把这个元素的值以参数形式传递给该函数。这里需要注意权衡:如果把所有依赖关系都变成参数,会导致参数列表冗长重复;如果作用域之间的共享太多,又会导致函数间依赖过度。
以参数取代查询并非只有好处。把查询变成参数以后,就迫使调用者必须弄清如何提供正确的参数值,这会增加函数调用者的复杂度。
做法
- 对执行查询操作的代码使用提炼变量(119),将其从函数体中分离出来。
- 现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对这部分代码使用提炼函数(106)。
- 使用内联变量(123),消除刚才提炼出来的变量。
- 对原来的函数使用内联函数(115)。
- 对新函数改名,改回原来函数的名字。
移除设置函数(Remove Setting Method)
class Person {get name() {/*...*/}set name(aString) {/*...*/}
}
class Person {get name() {/*...*/}
}
动机
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变。如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数。
做法
-
如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明(124)将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值。
如果想移除多个设值函数,可以一次性把它们的值都传入构造函数,这能简化后续步骤。
-
移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试。
-
如果不能把“调用设值函数”替换为“创建一个新对象”(例如需要更新一个多处共享引用的对象),请放弃本重构。
-
使用内联函数(115)消去设值函数。如果可能的话,把字段声明为不可变。
-
测试。
以工厂函数取代构造函数(Replace Constructor with Factory Function)
曾用名:以工厂函数取代构造函数(Replace Constructor with Factory Method)
leadEngineer = new Employee(document.leadEngineer, 'E');
leadEngineer = createEngineer(document.leadEngineer);
动机
很多面向对象语言都有特别的构造函数,专门用于对象的初始化。需要新建一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常有一些丑陋的局限性。
工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。
做法
- 新建一个工厂函数,让它调用现有的构造函数。
- 将调用构造函数的代码改为调用工厂函数。
- 每修改一处,就执行测试。
- 尽量缩小构造函数的可见范围。
以命令取代函数(Replace Function with Command)
曾用名:以函数对象取代函数(Replace Method with Method Object)
反向重构:以函数取代命令(Replace Command with Function)
function score(candidate, medicalExam, scoringGuide) {let result = 0;let healthLevel = 0;// long body code
}
class Scorer {constructor(candidate, medicalExam, scoringGuide) {this._candidate = candidate;this._medicalExam = medicalExam;this._scoringGuide = scoringGuide;}execute() {this._result = 0;this._healthLevel = 0;// long body code}
}
function score(candidate, medicalExam, scoringGuide) {return new Scorer().execute(candidate, medicalExam, scoringGuide);
}
动机
命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。
做法
-
为想要包装的函数创建一个空的类,根据该函数的名字为其命名。
-
使用搬移函数(198)把函数移到空的类里。
遵循编程语言的命名规范来给命令对象起名。如果没有合适的命名规范,就给命令对象中负责实际执行命令的函数起一个通用的名字,例如“execute”或者“call”。
-
可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数。
以函数取代命令(Replace Command with Function)
反向重构:以命令取代函数(Replace Function with Command)
class ChargeCalculator {constructor (customer, usage){this._customer = customer;this._usage = usage;}execute() {return this._customer.rate * this._usage;}
}
function charge(customer, usage) {return customer.rate * usage;
}
动机
大多数时候,只是想调用一个函数,让它完成自己的工作就好。如果这个函数不是太复杂,那么命令对象可能显得费而不惠,就应该考虑将其变回普通的函数。
做法
- 运用提炼函数(106),把“创建并执行命令对象”的代码单独提炼到一个函数中。
- 对命令对象在执行阶段用到的函数,逐一使用内联函数(115)。
- 使用改变函数声明(124),把构造函数的参数转移到执行函数。
- 对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次修改后都要测试。
- 把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换命令对象的那个函数)。
- 测试。
- 用移除死代码(237)把命令类消去。
相关文章:

CH11_重构API
将查询函数和修改函数分离(Separate Query from Modifier) function getTotalOutstandingAndSendBill() {const result customer.invoices.reduce((total, each) > each.amount total, 0);sendBill();return result; }function totalOutstanding() …...

UPLOAD-LABS1
less1 (js验证) 我们上传PHP的发现不可以,只能是jpg,png,gif(白名单限制了) 我们可以直接去修改限制 在查看器中看到使用了onsubmit这个函数,触发了鼠标的单击事件,在表单提交后马上调用了re…...
WordPress相关文章推荐
首先 WordPress 本身并没有相关文章的推荐功能,网站之所以需要这样的功能出于两个原因,一方面是推荐相关的内容越优质,访客的留存和继续阅读将会增强,同样从优化角度来说会更加有利于搜索引擎抓取时对页面质量的提升,毕…...

【QML】Qt和QML获取操作系统类型
1. Qt获取系统类型 //方法 QSysInfo::productType()//举例: if(QSysInfo::productType() "windows") {qDebug() << "windows system"; }官方说明: [static] QString QSysInfo::productType() Returns the product name of …...

CSS 显示、定位、布局、浮动
一、CSS 显示: CSS display属性设置元素应如何显示;CSS visibility属性指定元素应可见还是隐藏。隐藏元素可以通过display属性设置为“none”,也可以通过visibility属性设置为“hidden”。两者的区别:visibility:hidden可以隐藏某…...
Java 学习笔记
文章目录 一、集合1.1 List1.1.1 ArrayList1.1.2 Vector1.1.3 LinkedList 1.2 Deque1.3 Set1.4 Map1.4.1 HashMap1.4.2 LinkedHashMap 1.5 注意事项 二、函数式接口和 Lambda 表达式三、方法引用3.1 静态方法引用3.2 实例方法引用3.2 特定类型的方法引用3.4 构造器引用 四、Str…...

项目实战:优化Servlet,把所有围绕Fruit操作的Servlet封装成一个Servlet
1、FruitServlet 这些Servlet都是围绕着Fruit进行的把所有对水果增删改查的Servlet放到一个Servlet里面,让tomcat实例化一个Servlet对象 package com.csdn.fruit.servlet; import com.csdn.fruit.dto.PageInfo; import com.csdn.fruit.dto.PageQueryParam; import c…...
Go语言函数参数
文章目录 Go语言函数参数1. **函数参数的定义**:2. **参数的数量**:3. **参数的数据类型**:4. **参数的命名**:5. **参数的传递**:6. **参数的传递方式**:7. **空白标识符**: Go语言函数参数 在…...

【遍历二叉树的非递归算法,二叉树的层次遍历】
文章目录 遍历二叉树的非递归算法二叉树的层次遍历 遍历二叉树的非递归算法 先序遍历序列建立二叉树的二叉链表 中序遍历非递归算法 二叉树中序遍历的非递归算法的关键:在中序遍历过某个结点的整个左子树后,如何找到该结点的根以及右子树。 基本思想&a…...

数模之线性规划
线性规划 优化类问题:有限的资源,最大的收益 例子: 华强去水果摊找茬,水果摊上共3个瓜,华强总共有40点体力值,每劈一个瓜能带来40点挑衅值,每挑一个瓜问“你这瓜保熟吗”能带来30点挑衅值,劈瓜消耗20点体力值,问话消耗…...

【C++】AVL树的4中旋转调整
文章目录 前提一、AVL树的结构定义二、AVL的插入(重点)1. 插入的结点在较高左子树的左侧(右单旋)2. 新节点插入较高右子树的右侧(左单旋)3.新结点插入较高右子树的左侧(先右单旋再左单旋&#x…...

【MATLAB源码-第69期】基于matlab的LDPC码,turbo码,卷积码误码率对比,码率均为1/3,BPSK调制。
操作环境: MATLAB 2022a 1、算法描述 本文章介绍了卷积码、Turbo码和LDPC码。以相同的码率仿真这三种编码,并对比其误码率性能 信源输出的数据符号(二进制)是相互独立和等概率的; 信道是加性白高斯噪声信道&#…...
Java获取时间戳、字符串和Date对象的相互转换、日期时间格式化、获取年月日
获取时间戳(自1970年1月1日经历的毫秒数值) package org.example;import java.util.Date;public class Main {public static void main(String[] args) {Date date1 new Date(1699540662210L);System.out.println(date1.getTime());Date date2 new Dat…...
用c语言实现矩阵转置
下面是在 C 语言中实现矩阵转置的示例代码: #include <stdio.h> #define ROWS 3 #define COLS 3 void transpose(int matrix[ROWS][COLS]) { int temp; for(int i0; i<ROWS; i) { for(int j0; j<i; j) { temp matrix[i][j]; matrix[i][j] matrix[j]…...
蓝桥杯官网练习题(移动距离)
题目描述 X 星球居民小区的楼房全是一样的,并且按矩阵样式排列。其楼房的编号为 1,2,3, 当排满一行时,从下一行相邻的楼往反方向排号。 比如:当小区排号宽度为 6 时,开始情形如下: 1 2 3 4 5 6 12 …...

不止于“初见成效”,阿斯利康要让数据流转,以 AI 带动决策智能
“阿斯利康数字化成果在进博会上引人注目,令我感到非常高兴。”这是阿斯利康代表的感慨。 数字化建设目标是利用先进技术来提高企业运营效率,降低成本。在第六届进博会的7.2 B2-01展区,阿斯利康不仅展示了全球领先的生物医药和医疗器械成果&a…...

nav2 调节纯追踪算法
纯追踪算法 纯追踪基础 The core idea is to find a point on the path in front of the robot and find the linear and angular velocity to help drive towards it. 核心思想是在机器人前方的路径上找到一个点,并找到一个合适的线速度和角速度,以驱…...

安装RabbitMQ
安装RabbitMQ 下载需要的两个包 # 这直接就可以安装了,下面 ‘上传对应的rmp包’ 操作 [rootrabbitmq-1 ~]# curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash [rootrabbitmq-1 ~]# yum install erlang-21.3.8.2…...

Spring基础(1):两个概念
最近看了点Spring的源码,于是来稍微扯一扯,希望能帮一部分培训班出身的朋友撕开一道口子,透透气。 广义上的Spring指的是Spring整个项目,包含SpringBoot、SpringCloud、SpringFramework、SpringData等等, 本系列文章…...

国产化精密划片机已得到国内更多厂家青睐
国产化精密划片机在近年来得到了国内许多厂家的青睐,这是因为精密划片机在工业生产中有着重要作用。这种设备主要用于高精密切割加工,适用于多种材料,包括硅、石英、氧化铝、氧化铁等。 以精密晶圆划片机为例,这种设备采用了自主研…...
.NET 事件模式举例介绍
.NET 事件模式:实现对象间松耦合通信 在软件开发中,对象之间的通信是一个常见且重要的问题。.NET 框架提供了一种标准化的事件模式,用于解决对象间的通信问题,实现松耦合的交互方式。今天,我们就通过一个简单的例子来…...
在C++中,头文件(.h或.hpp)的标准写法
目录 1.头文件保护(Include Guards)2.包含必要的标准库头文件3.前向声明(Forward Declarations)4.命名空间5.注释示例1:基础头文件示例2:包含模板和内联函数的头文件示例3:C11风格的枚举类头文件…...
在WPF项目中集成Python:Python.NET深度实战指南
随着Python在数据分析、机器学习、自动化等领域的广泛应用,越来越多的.NET开发者希望在WPF桌面应用中调用Python代码,实现两者优势互补。Python.NET(pythonnet)作为连接.NET与Python的桥梁,提供了强大的跨语言调用能力…...

什么是预训练?深入解读大模型AI的“高考集训”
1. 预训练的通俗理解:AI的“高考集训” 我们可以将预训练(Pre-training) 形象地理解为大模型AI的“高考集训”。就像学霸在高考前需要刷五年高考三年模拟一样,大模型在正式诞生前,也要经历一场声势浩大的“题海战术”…...

使用osqp求解简单二次规划问题
文章目录 一、问题描述二、数学推导1. 目标函数处理2. 约束条件处理 三、代码编写 一、问题描述 已知: m i n ( x 1 − 1 ) 2 ( x 2 − 2 ) 2 s . t . 0 ⩽ x 1 ⩽ 1.5 , 1 ⩽ x 2 ⩽ 2.5 min(x_1-1)^2(x_2-2)^2 \qquad s.t. \ \ 0 \leqslant x_1 \leqslant 1.5,…...
东芝Toshiba e-STUDIO2110AC打印机信息
基本信息 产品类型:数码复合机颜色类型:彩色涵盖功能:复印、打印、扫描接口类型:标配为 Ethernet(RJ45)10/100/1000BASE - T、USB2.0 高速;选配为 Wireless Lan、IEEE802.11b/g/n、blueteeth。中…...

手机号在网状态查询接口如何用PHP实现调用?
一、什么是手机号在网状态查询接口 通过精准探测手机号的状态,帮助平台减少此类问题的发生,提供更个性化的服务或进行地域性营销 二、应用场景 1. 金融风控 通过运营商在网态查询接口,金融机构可以核验贷款申请人的手机状态,拦…...
Oracle 用户名大小写控制
Oracle 用户名大小写控制 在 Oracle 数据库中,用户名的默认大小写行为和精确控制方法如下: 一 默认用户名大小写行为 不引用的用户名:自动转换为大写 CREATE USER white IDENTIFIED BY oracle123; -- 实际创建的用户名是 "WHITE"…...
Spring中循环依赖问题的解决机制总结
一、解决机制 1. 什么是循环依赖 循环依赖是指两个或多个Bean之间相互依赖对方,形成一个闭环的依赖关系。最常见的情况是当Bean A依赖Bean B,而Bean B又依赖Bean A时,就形成了循环依赖。在Spring容器初始化过程中,如果不加以特殊…...

Ubuntu20.04基础配置安装——系统安装(一)
引言: 工作需要,Ubuntu的各类环境配置,从23年开始使用Ubuntu20.04之后,尽管能力在不断提升,但是依旧会遇到Ubuntu系统崩掉的情况,为了方便后续系统出现问题及时替换,减少从网上搜索资源进行基础…...