写在前面
在前面两篇文章《重构,有品位的代码 01——走上重构之道》和《重构,有品位的代码 02 ── 构建测试系统》中分别介绍了重构的基本概念和测试重构性能的测试体系,而本篇文章将致力于介绍11中常见的重构方法。
在重构实践中,我们常用的重构方法有提炼函数和变量,就是通过代码提炼到函数和变量,最关键的就是命名。当然,我们还用到了两个重构的反向重构——内联函数和内联变量,是不是很惊讶怎么自己不知道。其实还有更多的重构方法,可能已经在悄无声息在我们的代码中使用。
重构名录
对于重构可以采用标准格式,每个重构手法都包含以下部分:
- 名称(name):建造常用的重构词汇表,在进行重构的时候可以用于变量和函数命名
- 速写(sketch):能够帮助快速找到所需要的重构手法
- 动机(motivation):介绍“为什么要做这个重构”和“什么情况下不该做这个重构”
- 做法(mechanics):简明扼要地介绍如何进行此重构
- 范例(examples):以简单清晰的范例说明此重构手法是如何操作的
重构的奇技淫巧
对于有经验的开发者而言,重构的技巧甚多,针对代码重构的方式也很多,具体操作因人而异。在进行重构后的思考,对常用的技巧总结如下:
- 提炼函数
- 内联函数
- 提炼变量
- 内联变量
- 改变函数声明
- 封装变量
- 变量改名
- 引入参数对象
- 函数组合成类
- 函数组合成变换
- 拆分阶段
1. 提炼函数
原始函数:
function extractFunc(arg){
printBanner();
let outstanding = calculateOutstanding();
// 打印细节
console.log(`name:${arg.customer}`);
console.log(`amount:${outstanding}`);
}
提炼函数:
function extractFunc(arg){
printBanner();
let outstanding = calculateOutstanding();
printDetails(outstanding);
// 打印细节
function printDetails(outstanding){
console.log(`name:${arg.customer}`);
console.log(`amount:${outstanding}`);
}
}
其实,上面代码中没有多长,在做函数提炼的时候也没做多大改变,只是根据代码的作用将其提炼到单独函数中,并根据代码的用途对函数命名。
动机
提炼函数的最恰当时机不是代码不能进行一屏幕显示时,也不是对重复性高代码进行简单抽取,而是将意图和实现进行分开。就是当你浏览代码时需要花费时间去理解弄清到底是做什么的,那么此时就需要对其进行提炼到用途函数中,并根据用途命名。
做法
我们看到,如果采用此方法对代码进行提炼函数,那么是否会造成大量短函数调用问题,影响运行性能。其实,你大可不必担心,因为短函数可以更容易地被缓存,所以当你始终使得短函数遵循性能优化的指导方针,那么就不必太过于担心性能问题。在进行函数命名时,要注意简洁明了、言语达意,其实就可以使用你做的注释翻译成英文进行命名。
简而言之,就是创造新函数,根据函数意图进行命名。注意检查提炼的代码中注意变量的作用域引用,而后进行处理测试。
针对变量的作用域处理:
-
无局部变量时,直接对代码进行提炼
-
有局部变量时,可以将这些局部变量的值作为参数传递给目标函数
-
对局部变量再赋值,如源函数的参数被赋值,应当进行拆分变量为临时变量。两种情况:
(1)被赋值的变量只在被提炼代码中使用,此时只需要将临时变量的声明移动到被提炼代码段中;
(2)被提炼代码之外的代码也在使用此变量,此时只需返回修改后的值即可。
范例
未重构前的完整代码:
function extractFunc(arg){
let outstanding = 0;
console.log("************************");
console.log("*****Customer Owns******");
console.log("************************");
// 计算outstanding
for(const o of arg.orders){
outstanding += o.amount;
}
// 记录
const today = Clock.today;
arg.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
// 打印细节
console.log(`name:${arg.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${arg.dueDate.toLocaleDateSString()}`);
}
重构后的完整代码:
function extractFunc(arg){
// 提炼打印提示的函数
printBanner();
// 计算outstanding
const outstanding = calculateOutstanding(arg);
// 记录
recordDueDate(arg);
// 打印详情
printDetails(arg,outstanding);
}
function printBanner(){
console.log("************************");
console.log("*****Customer Owns******");
console.log("************************");
}
function recordDueDate(arg){
const today = Clock.today;
arg.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
function printDetails(arg,outstanding){
console.log(`name:${arg.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${arg.dueDate.toLocaleDateSString()}`);
}
function calculateOutstanding(){
let result = 0;
// 计算outstanding
for(const o of arg.orders){
result += o.amount;
}
return result;
}
2. 内联函数
原始函数:
function whoAreYou(user){
return whatAreYou(user) ? "成年" : "未成年";
}
function whatAreYou(user){
return user.age > 18;
}
重构后的函数:
function whoAreYou(user){
return user.age > 5 ? "成年" : "未成年";
}
当你看到这段代码时,你是不是会想谁会写上面那段那么傻的代码,很明显越简单越好呀。的确,简短的函数能够使得代码清晰易读,能够更明确函数表达的意图。
动机
通过内联手法,可以找到函数间的联系,找到间接函数并将无用函数去除。可以对组织不合理的函数内联到个大型函数,而后重新进行提炼函数。
做法
对于内联函数的实际操作手法具体如下:
- 检查函数,确定其不具有多态性。如果该函数属于类,其有子类继承了此函数,那么就无法内联
- 找出该函数的所有调用点,并将所有调用点替换成函数本体
- 在每次替换后测试。不必一次性完成整个内联操作,有些调用点难以内联,可以考虑时机成熟后处理
- 删除该函数的定义
范例
未进行内联操作的函数:
function inlineFunc(student){
const students = [];
addStudent(students, student);
return students;
}
function addStudent(students, student){
students.push(["name",student.name]);
students.push(["address",student.address]);
}
进行内联操作后的函数:
function inlineFunc(student){
const students = [];
students.push(["name",student.name]);
students.push(["address",student.address]);
return students;
}
3. 提炼变量
原始函数:
function price(order){
return order.quantity * order.itemPrice -
Max.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Max.min(order.quantity * order.itemPrice * 0.1, 100)
}
重构函数:
function(order){
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(order.quantity * order.itemPrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
事实上,表达式有时候会很冗长,使得变得复杂且难以阅读。而在进行重构时,可以使用局部变量来分解表达式,使得其逻辑更加清晰、便于管理,且在进行变量调试时也很便利。
同时,当给提炼变量后的表达式命名也要考虑其所处的上下文。当变量仅在当前函数内有意义,那么可以提炼变量;当变量在更宽的上下文中有意义,那么可以考虑将其通过函数形式进行暴露。
具体的,在确认要提炼的表达式没有副作用时,声明一个不可修改的变量(常量),将要提炼的表达式复制一份,且将该表达式的结果赋值给此变量,从而实现提炼变量的目的。
范例
未进行重构的代码:
class Order{
constructor(record){
this._data = record;
}
get quantity(){
return this._data.quantity;
}
get itemPrice(){
return this._data.itemPrice;
}
get price(){
return this.quantity * this.itemPrice -
Math.max(0, this.quantity - 500 ) * this.itemPrice * 0.05 +
Math.min(this.quantity * this.itemPrice * 0.1, 100)
}
}
进行重构的代码:
class Order{
constructor(record){
this._data = record;
}
get quantity(){
return this._data.quantity;
}
get itemPrice(){
return this._data.itemPrice;
}
get price(){
return this.basePrice - this.quantityDiscount + this.shipping;
}
get basePrice(){
return this.quantity * this.itemPrice;
}
get quantityDiscount(){
return Math.max(0, this.quantity - 500 ) * this.itemPrice * 0.05;
}
get shipping(){
return Math.min(this.quantity * this.itemPrice * 0.1, 100)
}
}
4. 内联变量
在函数内部,变量能给表达式提供有意义的名字,但有时候名字却并不能带给比表达式本身更有表现力,甚至科能妨碍重构附近的代码,这样就需要进行内联来消除变量。
具体的,先对变量赋值语句的右侧表达式继续检查是否有副作用,若变量没有被声明为不可修改,则先将其变成不可修改再进行测试。找到首次使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式并进行测试。而后,对所有使用此变量的地方进行修改替换,删除该变量的声明点和赋值语句。
简而言之,就是直接使用表达式。
function inlineVariable(order){
let basePrice = order.basePrice;
return basePrice > 1000;
}
重构后:
function inlineVariable(order){
return order.basePrice > 1000;
}
5. 改变函数声明
函数是将程序拆分成代码段的主要方式,能够很清晰将各个功能代码进行划分,使得能够清楚了解软件系统的关节。
对于函数重构而言,好的名字和有序的参数列表尤为关键。常见的手法是书写一段清晰达意的注释描述函数用途,再将其进行修改为函数名称;修改参数列表能够增加函数的应用范围,还能改变模块所需要的条件,去除不必要的耦合。
例如:
function cirum(radius){}
//改成
function cirumFerence(radius){}
class Book{
constructor(){
this._reservations = [];
}
addReservation(customer){
this._reservations.push(customer)
}
}
//我们为其添加了新功能是否进行优先预订,修改为
class Book{
constructor(){
this._reservations = [];
}
addReservation(customer){
this.zz_addReservation(customer,false);
}
zz_addReservation(customer,isPriority){
assert(isPriority === true || isPriority === false);
this._reservations.push(customer);
}
}
6. 封装变量
在重构中其实就是调整程序中的元素,函数是相对容易调整的,只需要改变调用、进行改名和修改参数。而对于数据而言就要麻烦些,需要考虑到数据的可访问范围,通常使用函数形式对数据访问,这样就能重新组织数据。对于此方法称作为“封装数据”。
封装数据的作用有:可以为监控数据的变化和使用情况清晰的观测点;可以轻松添加数据被修改时的验证或后续逻辑。
对于所有可变数据,只要其作用域超过单个函数,就可将其进行封装为通过函数访问的数据。那是因为数据的作用域越大,在进行数据修改时影响更大,容易出现耦合现象。而对于不可变数据而言,就不需要这样的数据更新验证等操作,可以简单进行复制使用。
通常,进行创建封装函数进行访问和更新其中的变量值,再执行静态检查,逐一修改使用该变量的代码并将其修改为调用合适的封装函数。每次替换后均需要测试。通过数据封装来限制变量的可见性。
未进行封装前的代码:
let defaultOwner = {
firstName:"Wen",
lastName:"Bo"
}
// 如果我们要进行查看和修改数据的代码,是这样的
spaceship.owner = defaultOwner;
defaultOwner = {
firstName:"Liu",
lastName:"Yichuan"
}
let defaultOwnerData = {
firstName:"Wen",
lastName:"Bo"
}
进行数据封装后的代码:
let defaultOwnerData = {
firstName:"Wen",
lastName:"Bo"
}
// 如果我们要实现多次操作和使用相同操作
export function defaultOwner(){
return new Person(defaultOwnerData);
}
export function setDefaultOwner(arg){
defaultOwnerData = arg;
}
class Person{
constructor(data){
this._firstName = data.firstName;
this._lastName = data.lastName;
}
get firstName(){
return this._firstName;
}
get lastName(){
return this._lastName;
}
}
7. 变量改名
很简单,就是要取有意义的变量名称:
let a = height * width;
//改成
let area = height * width;
8. 引入参数对象
其实,就是把一长串的散列的参数,替换成具有数据结构的参数对象。如下:
function carculateVolume(length, width, height){
return length * width * height;
}
可修改成:
const cuboid = {
width:10,
height:20,
length:30
}
function carculateVolume(cuboid){
return cuboid.length * cuboid.width * cuboid.height;
}
9. 函数组合成类
正如你所知道,类是多数现代编程语言的基本构造,能够将数据和函数进行捆绑在一个环境,将部分数据和函数进行暴露使用。在实践中,当你发现一组函数与同一块数据紧密联系,那么就可以考虑将它们单独封装成类。
原始代码
function base(reading){}
function textbleCharge(reading){}
function calculateBaseCharge(reading){}
重构代码:
class Reading{
constructor(){}
base(){}
textableCharge(){}
calculateBaseCharge(){}
}
10. 函数组合变换
个人更倾向于函数组合成类,函数组合变换结果还是一个函数。
原始代码:
function base(reading){}
function textableCharge(reading){}
重构代码:
function enrichReading(reading){
const aReading = _.cloneDeep(reaading);
aReading.baseCharge = base(aReading);
aReading.textableCharge = textableCharge(aReading);
return aReading;
}
11. 拆分阶段
当看到一段代码在同时处理两件不同的事情,那么可以将其拆分为各自独立的模块。简单的,可以将大段代码行为分成顺序执行的两阶段。
function priceOrder(product, quantity, shipingMethod){
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold,0)
* product.basePrice * product.discountRate;
const shippingPerCase = basePrice > shipingMethod.discountThreshold
? shipingMethod.discountFee : shipingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
const price = basePrice - discount + shippingCost;
return price;
}
重构代码:
function priceOrder(product, quantity, shipingMethod){
const priceData = calculatePricingData(product,quantity);
return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product,quantity){
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold,0)
* product.basePrice * product.discountRate;
return {basePrice,quantity,discount}
}
function applyShipping(priceData, shippingMethod){
const shippingPerCase = priceData.basePrice > shippingMethod.discountThreshold
? shippingMethod.discountFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost;
}
小结
本篇文章介绍了常见的简单的11种重构的奇技淫巧,能够帮助我们的代码变得更加有品位,更加整洁,远离坏味道。
参考文章
《重构──改善既有代码的设计(第2版)》
写在最后
我是前端小菜鸡,感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。
更多最新文章敬请关注笔者掘金账号一川萤火和公众号前端万有引力。