编程的方式有很多种:面向工资编程、面向 bug 编程、面向性能编程、面向领导编程。但是有一种编程方式你们一定没有听说过:面向调试编程。下面我拿 javascript 举例来说一说怎么面向调试编程。
一道面试题
从一道面试题开始说起:请实现一个 flat 函数,将多维数组展开
,这道题有很多种写法,但是肯定有一种写法它是最炫酷的:
js
function flat(arr=[]) {
return arr.reduce((acc, val) => (Array.isArray(val) ? acc.concat(flat(val)) : acc.concat(val)), []);
}
看到没有,一行代码解决,是不是很牛,很多面试题网上不乏都有这样的答案,这里我不去讨论其实现的复杂度如何,单单看这种写法会给后面的维护带来什么问题。
假如业务中使用到了这个工具方法,但是计算出的结果不正确,需要调试解决 bug,这个时候会发现没办法打断点,这是其一:箭头函数一时爽,到了后面就哭天喊地了。
这时候肯定有人会说了:把箭头函数加上一个括号,然后 debug 不就行了,是的单单看这一个案例是没问题,但是如果面对的是复杂上千行的代码那又该当如何呢?为什么不提前加上大括号呢?
好,现在就加上大括号,reduce 函数可以 debug 了。
下面看一下改造之后的代码:
js
function flat(arr = []) {
const result = arr.reduce((acc, val) => {
const isArray = Array.isArray(val);
return isArray ? acc.concat(flat(val)) : acc.concat(val);
}, []);
return result;
}
改造之后的代码看起来很Low
但是到了实际出问题的时候能断点的位置比之前多了 5 处,解决问题更加方便了,是选择表面上的高大上
,还是内心的健壮
,这就看你自己的选择了。
面向调试编程
上面这种方式我愿意把它称为面向调试编程
,作为程序员,我不希望程序有 bug,但是我阻止不了 bug 的产生。下面我来列举一下常见的场景:
对象与纯函数
对象与纯函数
是调试的重灾区,这一点我深有体会,比如为了转换数据格式然后传给后端接口,我写了下面一段伪代码:
js
const arr = [
{
name: "Li Ming",
age: 42,
idNo: "1234567890123456789",
},
];
const params = {
id: 1,
people: arr.map((item) => ({
...item,
label: item.name,
value: item.idNo,
})),
extra: {
xxx: arr.map((item) => ({
...item,
label: item.name,
value: item.idNo,
})),
abc: arr.filter((item) => item.age > 40),
},
};
对象上面充斥着大量的纯函数表达式,这样写确实一时爽,我以前也这么写。但是如果 arr 是个 null 导致程序报错了,然后又用 trycatch 包裹了这个请求,这个时候只要 catch 里面没有打印错误信息,那么报错就很难查找。在 debug 的时候你不得不把所有的表达式都改写一下,如下:
js
const arr = null;
const people = arr.map((item) => {
return {
...item,
label: item.name,
value: item.idNo,
};
});
const xxx = arr.map((item) => {
return {
...item,
label: item.name,
value: item.idNo,
};
});
const abc = arr.filter((item) => {
return item.age > 40;
});
const params = {
id: 1,
people,
extra: {
xxx,
abc,
},
};
这个时候痛苦加倍!早知今日,何必当初要这么写呢?
函数参数
函数参数
也很容易出现不容易调试的问题,下面来看一段非常熟悉的代码:
js
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
这是 Koa 洋葱模型的核心实现,Koa 这样写没问题,因为它有完备的单元测试,如果我业务中也写成这样那就会被所有同事嫌弃了。可以这样改写:
js
try {
const result = dispatch.bind(null, i + 1);
const promise = fn(context, result);
return Promise.resolve(promise);
} catch (err) {
return Promise.reject(err);
}
这样一来是不是好理解多了呢?
一句话总结:面向调试编程就是将表达式都拆分出来然后赋值给变量,这样调试的时候就能对每个过程的值进行测试,便于找出真正有问题的代码。目前我找到的有两个场景:一个是对象与纯函数,另外一个场景是函数参数,如果有其他场景,欢迎大家评论区留言分享。