今天读了曾探的《设计模式》中的代码重构这一章节, 知识点不多却非常实用, 在实际项目中除了使用设计模式重构代码外, 还有一些容易忽略的细节. 这些细节也是重构的重要手段. 写这篇文章总结一下.

代码是写给人看的, 顺便给机器运行. 优雅的代码应该是简单、易维护、可扩展的.

提炼函数

如果一个函数过长, 而且需要注释才清楚它是如何工作的, 那么需要考虑把它独立出来. 这样做的好处是:

  • 避免超大函数, 作用域过大变量不好维护
  • 抽离公共逻辑, 易于复用和覆写
  • 良好的命名起到了注释作用

例子:

1
2
3
4
5
6
7
8
9
const getUserInfo = () => {
fetch('https://xxx.com/userInfo', (data) => {
console.log(`
userId: ${data.userId}\n
userName: ${data.userName}\n
nickName: ${data.NickName}
`);
});
};

重构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const printDetail = (data) => {
console.log(`
userId: ${data.userId}\n
userName: ${data.userName}\n
nickName: ${data.NickName}
`);
}

const getUserInfo = () => {
fetch('https://xxx.com/userInfo', (data) => {
printDetail(data);
});
};

⤴️Go Top

合并重复的条件片段

如果一个函数体内有一些条件分支, 而且这些条件分支内散布了一些重复的代码, 这个时候就有必要进行合并去重操作.

例子:

1
2
3
4
5
6
7
8
9
10
11
const paging = (currPage) => {
if (currPage <= 0) {
currPage = 0;
jump(currPage); // 跳转
} else if (currPage >= totalPage) {
currPage = totalPage;
jump(currPage); // 跳转
} else {
jump(currPage); // 跳转
}
};

可以看到上面的 jump(currPage) 重复了, 完全可以把它独立出来:

1
2
3
4
5
6
7
8
const paging = (currPage) => {
if (currPage <= 0) {
currPage = 0;
} else if (currPage >= totalPage) {
currPage = totalPage;
}
jump(currPage);
}

⤴️Go Top

把条件分支语句提炼成函数

复杂的条件分支语句是导致程序难以阅读和理解的重要原因, 而且很容易导致一个庞大的函数. 这种情况下就需要把条件分支语句提炼出来.

例子:

1
2
3
4
5
6
7
8
9
// 需求: 如果当季是处于夏季, 那么所有的商品 8 折出售

const getPrice = (price) => {
const date = new Date();
if (date.getMonth() >= 6 && date.getMonth() <= 9) { // 夏季
return price * 0.8;
}
return price;
};

观察这段代码:

1
2
3
if (date.getMonth() >= 6 && date.getMonth() <= 9) { // 夏季
// ...
}

我们需要让它更符合语义, 这样读代码的人能很轻松理解意图, 这里就可以提炼成一个单独函数.

1
2
3
4
5
6
7
8
9
10
11
12
const isSummer = () => {
const date = new Date();
return date.getMonth() >= 6 && date.getMonth() <= 9;
};

const getPrice = (price) => {
const date = new Date();
if (isSummer()) {
return price * 0.8;
}
return price;
};

⤴️Go Top

合理的使用循环

在函数体内, 如果一些代码只是完成一些重复的工作, 那么可以合理利用循环完成同样的功能, 这样可以保持代码量更少.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 需求: IE9 以下创建一个 XHR 对象

var createXHR = function() {
var xhr;
try {
xhr = new ActiveXObject('MSXML2.XMLHttp.6.0');
} catch(e) {
try {
xhr = new ActiveXObject('MSXML2.XMLHttp.3.0')
} catch(e) {
xhr = new ActiveXObject('MSXML2.XMLHttp');
}
}
return xhr;
};

var xhr = createXHR();

这个时候, 使用循环来优化上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 这段代码就看着符合语义了, 而且好理解 🤡

var createXHR = function() {
var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
for (var i = 0, version; version = versions[i++];) {
try {
return new ActiveXObject(version);
} catch(e) {};
}
};

var xhr = createXHR();

⤴️Go Top

提前让函数退出代替嵌套条件分支

很多程序员👨‍💻‍都有这种观念 “每个函数只能有一个入口和一个出口”, 但是关于 “函数只有一个出口” 往往有一些不同的看法, 下面用代码来说明一下, 光讲概念有点晦涩.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 下面是 "函数只有一个出口" 的典型代码

const del = (obj) => {
let ret;
if (!obj.isReadOnly) {
if (obj.isFolder) {
ret = delFolder(obj);
} else if (obj.isFile) {
ret = delFile(obj);
}
}
return ret;
};

嵌套的条件分支绝对是代码维护者的噩梦, 多层嵌套的条件更加不容易理解, 有时候如果代码过长, 上一个 if () 语句可能相隔很远. 严重影响了阅读体验啊. 重构的 del 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const del = (obj) => {
if (obj.isReadOnly) { // 反转 if
return;
}
if (obj.isFolder) {
return delFolder(obj);
}
if (obj.isFile) {
return delFile(obj);
}
};

// 瞬间清爽 👌

⤴️Go Top

传递对象参数代替过长的参数列表

有时候一个函数可能接收多个参数, 而且参数越多, 就越难理解和使用. 最重要的一点需要注意参数的顺序, 如果不传的情况要使用占位符代替. 如果参数超过 3 个, 请使用对象吧.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const setUserInfo = (id, name, address, sex, mobile, qq) => {
// ...
}

// 调用 ☹️:
setUserInfo(1, 'ifyour', undefined, 'male', undefined, '123456');


// 重构 😁:
setUserInfo({
id: 1,
name: 'ifyour',
sex: 'male',
qq: '123456',
})

⤴️Go Top

尽量减少参数数量

如果调用一个函数, 需要传递很多参数, 这样的函数用起来就要小心了, 必须搞清楚每个参数的含义, 所以, 如果能尽量少传参数就少传参数. 把复杂的逻辑封装到函数内部.

例子:

1
2
3
4
5
6
7
8
9
10
11
// 需求: 有一个画图函数 draw

const draw = (width, height, square) => {
// ...
};

// 优化如下:
const draw = (width, height) => {
const square = width * height;
// ...
}

这里的 square 参数没必要, 可以通过 widthheight 计算获得, 还有一种情况, 这个画图函数, 如果可以画多种图形呢? 所以需要在内部去处理, 什么时候需要 square 什么时候需要 radius, 然后可以使用 策略模式 让它支持画多种图形.

⤴️Go Top

少用三目运算

三目运算能减少代码量, 但是不能滥用, 如果以牺牲代码可读性为代价, 那就得不偿失了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const global = typeof window !== 'undefined' ? window : this;

// 但是这种情况就完全没必要了, 老实写 if..else...

if (!aup || !bup) {
return a === doc ? -1 :
b === doc ? 1 :
aup ? -1 :
bup ? 1 :
sortInput ?
(indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) :
0;
}

// 😡 这是人看的吗

⤴️Go Top

合理使用链式调用

jQuery 中的链式调用用起来还是很爽的, 它的实现也非常简单, 我们可以很容易的实现一个链式调用. 使用的前提就是链条的结构相对来说稳定, 不容易发生修改, 如果是经常发生修改的话, 还是建议使用普通的调用. 因为它调试的时候需要把这个链子拆开, 才知道是哪个环节有 Bug.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ES5

var User = function() {
this.id = null;
this.name = null;
};

User.prototype.setId = function (id) {
this.id = id;
return this; // 返回实例本身
}

User.prototype.setName = function (name) {
this.name = name;
return this;
}

// 链式调用
new User()
.setId(1)
.setNmae('ifyour');

或者这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var User = {
id: null,
name: null,
setId: function(id) {
this.id = id;
return this;
},
setName: function(name) {
this.name = name;
return this;
}
};

User.setId(1).setName('ifyour');

⤴️Go Top

分解大型类

从字面意思我们就能理解, 如果一个类里面有太多的方法, 我们要考虑把其中同一类型相关的方法抽离出来. 模块化的好处不言而喻, 把逻辑复杂的分离成小的单元, 这样更便于维护, 出现 bug 了也好定位问题. 来看一段代码.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 英雄类
var Spirit = function() {
this.name = name;
};

// 原型上的攻击方法
Spirit.prototype.attack = function (type) {
if (type === 'waveBoxing') {
console.log(this.name + ': 使用波动拳');
} else if (type === 'whirlKick') {
console.log(this.name + ': 使用旋风腿');
}
// ... 还有很多攻击方法
}

var spirit = new Spirit('RYU');
spirit.attack('waveBoxing'); // RYU: 使用波动拳
spirit.attack('whirlKick'); // RYU: 使用旋风腿

面向对象的设计鼓励将 行为 分解到合理数量的更小的对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Attack = function (spirit) {
this.spirit = spirit;
};

Attack.prototype.start = function (type) {
return this.list[type].call(this);
}

Attack.prototype.list = {
waveBoxing: function () {
console.log(this.sprite.name + ': 使用波动拳')
},
whirlKick: function () {
console.log(this.sprite.name + ': 使用旋风腿')
}
}

现在 Sprite 类精简了很多, 不再直接包含攻击相关的方法, 而是把方法委托给 Attack 类来执行. 这段代码也是 策略模式 的运用之一, 看代码:

1
2
3
4
5
6
7
8
9
10
11
12
var Spirit = function() {
this.name = name;
this.attackObj = new Attack(this);
};

Spirit.prototype.attack = function (type) {
this.attackObj.start(type);
}

var spirit = new Spirit('RYU');
spirit.attack('waveBoxing'); // RYU: 使用波动拳
spirit.attack('whirlKick'); // RYU: 使用旋风腿

⤴️Go Top

退出多重循环

使用 return 退出多重循环. 一定程度上能简化代码, 使代码更容易理解.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 需求: 假设在一个函数体内的多重循环内, 达到某个临界条件时退出循环

var func = function () {
var flag = false;
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
flag = true;
break;
}
}
}
};

// 这种代码看着就头晕目眩, 重构如下:

var func = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return;
}
}
}
};

直接使用 return 后, 后面的代码无法执行, 还是有点小问题的:

1
2
3
4
5
6
7
8
9
10
var func = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return;
}
}
}
console.log(i) // 无法执行
};

为了解决这个问题, 我们可以把相关的代码放到 return 后面. 如果需要执行的代码较多, 可以提取成一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
var print = function (i) {
console.log(i)
};

var func = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return print(i); // 执行
}
}
}
};

⤴️Go Top