封装变量(Encapsulate Variable | 132)
曾用名:自封装字段(Self-Encapsulate Field)
曾用名:封装字段(Encapsulate Field)
let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
let defaultOwnerData = {firstName: "Martin", lastName: "Fowler"};
export function defaultOwner() {return defaultOwnerData;}
export function setDefaultOwner(arg) {defaultOwnerData = arg;}
动机
重构的作用就是调整程序中的元素。函数相对容易调整一些,因为函数只有一种用法,就是调用。在改名或搬移函数的过程中,总是可以比较容易地保留旧函数作为转发函数(即旧代码调用旧函数,旧函数再调用新函数)。这样的转发函数通常不会存在太久,但的确能够简化重构过程。
如果数据的可访问范围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。
如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问。
封装能提供一个清晰的观测点,可以由此监控数据的变化和使用情况,还可以轻松地添加数据被修改时的验证或后续逻辑。数据的作用域越大,封装就越重要。处理遗留代码时,一旦需要修改或增加使用可变数据的代码,可以借机把这份数据封装起来,从而避免继续加重耦合一份已经广泛使用的数据。
面向对象方法如此强调对象的数据应该保持私有(private),背后也是同样的原理。每当看见一个公开(public)的字段时,考虑使用封装变量来缩小其可见范围。
封装数据很重要,不过,不可变数据更重要。如果数据不能修改,就根本不需要数据更新前的验证或者其他逻辑钩子。
做法
- 创建封装函数,在其中访问和更新变量值。
- 执行静态检查。
- 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试。
- 限制变量的可见性。
- 测试。
- 如果变量的值是一个记录,考虑使用封装记录(162)。
封装值
对数据结构的引用做封装,能控制对该数据结构的访问和重新赋值;但并不能控制对结构内部数据项的修改。
控制结构内部数据的修改方法:
-
取值函数中返回数据的一份副本(保护源数据,防止源数据变化导致的意外事故)
-
阻止对数据的修改(禁止对数据结构内部的数值做任何修改)
数据封装很有价值,但往往并不简单。到底应该封装什么,以及如何封装,取决于数据被使用的方式,以及想要修改数据的方式。
变量改名(Rename Variable | 137)
let a = height * width;
let area = height * width;
动机
好的命名是整洁编程的核心。变量名起得好,变量可以很好地解释一段程序在干什么。
变量使用范围越广,名字的好坏就越重要。
机制
- 如果变量被广泛使用,考虑运用封装变量(132)将其封装起来。
- 找出所有使用该变量的代码,逐一修改。(已发布变量,不能进行这个重构)
- 测试。
引入参数对象(Introduce Parameter Object | 140)
function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}
动机
一组数据项(作为函数参数)总是同时出现(这样一组数据就是所谓的数据泥团)。
将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰。使用新的数据结构,参数的参数列表也能缩短。
做法
- 如果暂时还没有一个合适的数据结构,就创建一个
- 测试
- 使用改变函数声明(124)给原来的函数新增一个参数,类型是新建的数据结构
- 测试
- 调整所以调用者,传入新数据结构的适当实列。修改每一处,执行测试
- 用新数据结构中的每一项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。测试
函数组合成类(Combine Functions into class | 144)
function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}
class Reading {
base() {...}
taxableCharge() {...}
calculateBaseCharge() {...}
}
动机
类,在大多数现代编程语言中都是基本的构造。它们把数据与函数捆绑到同一个环境中,将一部分数据与函数暴露给其他程序元素以便协作。
如果发现一组函数相互操作同一块数据(通常是将这块数据作为参数传递给函数),可以将这几个函数足见2一个类。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用。
做法
- 运用封装记录(162)对多个函数共用的数据记录加以封装。
- 对于使用该记录结构的每个函数,运用搬移函数(198)将其移入新类。(如果函数调用时传入的参数已经是新类的成员,则从参数列表中去除之。)
- 用以处理该数据记录的逻辑可以用提炼函数(106)提炼出来,并移入新类。
函数组合成变换(Combine Functions into Transfrom | 149)
动机
在软件中经常有这类操作:一个数据项在一个函数中处理加工后,有进入到其他函数中继续处理加工,经过几番处理,才得到最终需要的结果(可能我们的初衷不这样,但由于后期新需求、功能的添加才形成这种现象)。
一个方式是采用数据变换(transform)函数:这种函数接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。
函数组合成变换的替代方案是函数组合成类(144),后者的做法是先用源数据创建一个类,再把相关的计算逻辑搬移到类中。
做法
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
- 挑选一块逻辑,将其主体一如变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段
- 测试
- 针对其他相关的计算逻辑,重复上述步骤
拆分阶段(Split Phase | 154)
const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);
function parseOrder(aString) {
const values = aString.split(/\s+/);
return ({
productID: values[0].split("-")[1],
quantity: parseInt(values[1]),
});
}
function price(order, priceList) {
return order.quantity * priceList[order.productID];
}
动机
一段代码在同时处理两件不同的事。
最简洁的拆分方法之一,就是把一大段行为分成顺序执行的两个阶段。
做法
- 将第二阶段的代码提炼成独立的函数
- 测试
- 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数中
- 测试
- 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,将其移入中转数据结构。每次搬移之后都要执行测试。
- 对第一阶段的代码运用提炼函数(106),让提炼出的函数返回中转数据结构。