重构改善既有代码的设计 --方法篇
重新组织数据
-
目的
:解决不易理解的错综复杂的长函数,促使代码更清晰更易维护。 -
解决思路
:长函数变成短函数、算法结构优化 -
常用方法
:抽离函数(Extract Method),通过查询的方式来获取值 (Replace Temp With query), 优化算法 (Substitute Algorthm) -
AF老项目中有很多行数过百的函数,导致维护起来很费时费力,很多函数都可以用Extract Method方法,拆成小函数,无论是后期增加逻辑还是要代码复用都都能帮助理解。另外函数很长的原因的是场景特别多有很多出来了函数,但是这些个处理函数都写在一个入口中,还是希望用Replace Temp With query方式来处理数据
- 抽离函数(Extract Method)
-
一个函数如果需要写很多注释来让人理解的时候,这个函数应该是可以被拆解成小函数。良好的函数命名可以让人更容易理解代码
-
场景说明
let infoArr = Array(10).fill().map((item, index) => {
return {
name: 'VFrank' + index,
age: Math.floor(Math.random() * 10 + 20)
}
});
function printInfo() {
// 输出一个横幅
console.log('**********************')
console.log('*****Hello VFrank*****')
console.log('**********************')
// 获取所有name和age字段数组统计项
let temp = { nameArr: [], ageArr: [] };
infoArr.forEach(item) {
let { name, age } = item;
temp.nameArr.push(name);
temp.ageArr.push(age);
}
// 打印统计数据
console.log(nameArr);
console.log(ageArr);
}
- 上面的printInfo函数,其实每个注释都可以是独立的一个函数,这里说明抽离函数中的几个问题
- 第一块注释,那无对内部变量的引用,可以直接方法抽离为一个小函数
- 第二块注释,属于数据处理层可以单独抽离出来,以返回值形式
- 第三快注释,打印逻辑对内部变量有引用关系,需要以参数形式传入
修改后如下
let infoArr = Array(10).fill().map((item, index) => {
return {
name: 'VFrank' + index,
age: Math.floor(Math.random() * 10 + 20)
}
});
function printInfo() {
printBanner();
let result = getOutStand(); // 这里用Replace Temp With query方法修改
printDetail(result);
}
// 无引用内部变量关系直接抽离
function printBanner() {
console.log('**********************')
console.log('*****Hello VFrank*****')
console.log('**********************')
}
// 计算逻辑代码抽离,
function getOutStand() {
let result = { nameArr: [], ageArr: [] };
infoArr.forEach(item) {
let { name, age } = item;
result.nameArr.push(name);
result.ageArr.push(age);
}
return result;
}
// 对内部变量有引用关系的通过参数传入
function printDetail(result) {
console.log(result.nameArr);
console.log(result.ageArr);
}
- 通过查询的方式来获取值(Replace Temp With query)
-
函数中存在很多临时变量,这些变量只在这个函数中使用,这个导致一个函数过长而不易理解。换成一个函数可以增加函数的颗粒度,给后续维护带来极大的帮助
-
场景
// 一个计算价格的函数, 基本价格 * 折扣
getPrice() {
let basePrice = this.quantity * this.itemPrice;
let discountFactor = 0;
if (basePrice > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
- 第一次修改把basePrice抽取出来
getPrice() {
let discountFactor = 0;
if (this.getBasePrice() > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return this.getBasePric() * discountFactor;
}
getBasePric() {
return this.quantity * this.itemPrice;
}
- 修改后发现discountFactor其实也可以抽出来,再次修改如下
getPrice() {
return this.getBasePric() * this.discountFactor();
}
getBasePric() {
return this.quantity * this.itemPrice;
}
getDisCountFactor() {
if (this.getBasePrice() > 1000) {return 0.95;}
return 0.98;
}
在对象之间搬移特性
-
目的
:解决对象因为承担过多的责任而变得臃肿不堪的场景(比如业务代码Mgr经常作了Panel或者Form应该做的事情) -
解决思路
:梳理对象的责任,移动对应的函数。(决定把责任放在哪) -
常用方法
:搬移方法(Move Method),提炼类(ExTract Class)
搬移方法(Move Method)
-
一个类如果与另外一个类有着高度耦合的关系,就需要搬移函数,使得每个类更简单
-
小原则:函数与哪个对象交流比较多,就移入这个对象
重新组织数据
-
目的
:轻松处理数据的重构 -
解决思路
:魔数改成常量、数组转换成对象 -
常用方法
:搬移方法(Move Method),提炼类(ExTract Class)
简化条件表达式
-
目的
:简化一些条件逻辑复杂的判断 -
解决思路
:将“分支逻辑”和“操作细节”进行分离 -
常用方法
:分解条件表达式(Decompose Conditioal),合并条件表达式(Consolidate Conditional Expression),以卫语句代替嵌套表达式(Replace Nested Conditional withGuard Clauses)
分解条件表达式(Decompose Conditioal)
-
复杂条件逻辑是导致复杂度上升的原因之一,编写不同的条件分支,根据不同的分支做不同的事情,但是复杂的条件判断会让你弄不清楚为什么要这样做,可读性大大的降低,所以需要拆解为多个独立的判断函数,根据用途来命名区分,突出条件逻辑。
-
场景
// dealWithData(num) {
// if (num < this.minNum || num > this.maxNum) {
// // doSomeThing
// } else {
// // doSomeThing
// }
// }
function dealWithData(num) {
if (this.isOverFlow(num)) {
// doSomeThing
} else {
// doSomeThing
}
}
isOverFlow(num) {
return num < this.minNum || num > this.maxNum
}
合并条件表达式(Consolidate Conditional Expression)
- 有一系类的判断条件,得到的结果都是一致的
// function dealWithData(num) {
// if (num < 2) {
// return 0;
// }
// if (num > this.maxNum) {
// return 0;
// }
// if (this.isFlag(num)) {
// return 0;
// }
// }
// 修改后
dealWithData(num) {
if (this.isZero(num)) {
return 0;
}
}
isZero(num) {
return num < 2 || num > this.maxNum || this.isFlag(num);
}
以卫语句代替嵌套表达式(Replace Nested Conditional withGuard Clauses)
- 函数中的条件逻辑使人难以看清正常的执行路径的情况下,使用卫语句理清
// dealWithData() {
// let result;
// if (this.isManage()) {
// result = this.getManage();
// } else {
// if (this.isEmploy()) {
// result = this.getEmploy();
// } else {
// if (this.isReired()) {
// result = this.getReired();
// } else {
// result = this.getDefault()
// }
// }
// }
// }
// 修改后
dealWithData() {
if (this.isManage()) {
return this.getManage();
}
if (this.isEmploy()) {
return this.getEmploy();
}
if (this.isReired()) {
return this.getReired();
}
result = this.getDefault()
}
简化函数调用
-
目的
:让接口变得更容易理解和使用 -
解决思路
:对函数的返回值、函数名、参数进行优化 -
常用方法
:函数改名(Rename Method),添加参数(Add Parameter),分离查询函数和修改函数(Spaaeate Query form Modifier),保持对象完整(Preserve Whole Object)
函数改名(Rename Method)
- 将复杂的处理过程分解成小函数,小函数命名不好,很难弄清楚函数式做什么的,没有达到预期的效果
添加参数(Add Parameter)
- 某个函数需要从调用端的到更多的信息,为此可以添加一个对象参数,让该对象带进函数所需信息。但是过多的参数要注意数据泥团的问题。
分离查询函数和修改函数(Spaaeate Query form Modifier)
// doSomeCode(nameArr) {
// for (let i = 0; i < nameArr.length; i++) {
// if (name === 'VFrank') {
// showMsg();
// return 'VFrank'
// }
// }
// return ''
// }
// 修改后
doSomeCode(nameArr) {
if (findPerson(nameArr)) {
showMsg();
}
}
findPerson(nameArr) {
for (let i = 0; i < nameArr.length; i++) {
if (name === 'JhoVFrankn') {
return 'VFrank'
}
}
}
处理概括关系
-
目的
:梳理继承关系,划分函数应该在父类还是在子类中 -
解决思路
:将函数方法在继承体系同上下移动 -
常用方法
: 函数上移(Pull Up Method), 函数下移(Push Down Method),塑造模板函数(Form Template Method), 提炼父类,提炼子类, 提炼接口
函数上移(Pull Up Method), 函数下移(Push Down Method)
- 有些函数在子类中实现的都是相同的功能,这些函数应该抽离到父类中,反之,如果父类函数中某个函数只是与某一个子类有关系,这个函数应该移到子类中去
构造函数本体上移
-
在各个子类中拥有一些构造函数, 本体几乎一样,可以在超类中构造一个函数,并在子类中调用。
-
这一点Ext里面用的很多,比如Ext的field组件,在initComponent的时候addEvent(‘focus’, ‘blur’)等事件,把子类共有的操作都提到父类中来执行
塑造模板函数
-
子类中,相应的某些函数以相同的顺序来执行类似的操作,但是每个函数的操作细节上有所不同,可以把这些操作放到独立的函数中,用同样的函数名表示。
-
Ext的组件的基本上都是这种模式来实现的, 每个组件都有initComponent,onRender,afterRender等函数,就是 用了这种方式,由此也有了钩子函数,也就有了生命周期概念。