最近业余时间在阅读《代码大全》,阅读“防御式编程”章节的时候非常受启发,自己之前对系统的错误处理这块也确实随意了。
什么时候应该代码应该自己catch掉错误并且处理了,什么时候应该抛出给上层,抛到上层时候要如何处理,如何使用断言等等没有一套规范。所以这里总结下应该如何来做防御式编程。
什么是防御式编程
防御式编程是承认程序都会有问题,无论是自己写的模块,团队中其他人写的模块甚至第三方工具包。然后依据这个来指导写代码,子程序不应该因为传入的错误数据被破坏了。
为什么需要防御式编程
因为防范看似微小的错误,收获可能大到你的想象。
最简单的例子就是对输入的参数做校验,比如删除一个数据的接口需要对输入参数做校验防止产生全匹配从而删除全部数据。
如何来做
一般会结合使用断言(asset)和错误捕获及处理(try...catch)。
断言对于前端开发来说可能有些陌生,断言的语法是assert(equal, message)
,只要equal为假,那么message会被记录下来(对于前端来说可能是写到console中,对于后端来说可能是写到日志文件)。
两者区别在那里呢:断言来处理绝对不应该发生的状况(如果发生了,说明代码存在bug),错误处理来处理预期会发生的状况,或者说断言用来检查代码的bug,错误处理用来检查有害的输入数据。
断言更多是在开发中使用的,可以更好的帮助我们在开发过程中定位错误(如果console中出现了断言的记录,我们可以顺着调用栈找到出错的源头,这比自己猜测然后加断点调试要快一些)。如果不是对性能有太大的影响的话我个人建议可以在生产环境也打开断言记录到sentry之类的日志服务中(需要注意信息的加密),这样可以更好的帮助我们定位bug。
比如从一个用户列表中删除一个用户的函数deleteUser(userId)
,可以使用断言来判断这个用户是不是在列表中,如果没在列表中说明肯定是代码出现了bug了(比如重复删除同一个用户或者这个函数的调用者传参数存在问题):
class User {
constructor() {
this.userList = [];
}
deleteUser(userId) {
const isExitUser = this.userList.findIndex(user => user.id === userId) !== -1;
asset(isExitUser, `${userId}不在用户列表中`);
}
}
错误处理来应对预期会发生的状况,比如客户端请求了一个api接口,正常情况下这个接口返回200,但还是存在极少数的情况下这个接口会返回500,我们可以捕捉到这个错误来做一些处理,比如重新发起一次请求之类的。
fetchUserList().then((res) => {
storeUserInfo(res.data);
}).catch((e) => {
if (e.message = '500') {
// TODO 这个函数还要注意下调用栈的深度,最多重复执行四次,防止调用栈过深导致爆掉
fetchUserList();
}
})
异常使用有这么几个原则:
-
如果异常可以在局部处理就在局部处理,不要放到外面去。
-
千万不要只捕获异常却什么都不做,比如
fertchUserList().catch(() => {})
,catch函数里面没有做任何处理。 -
抛出的异常的消息中加入异常发生的相关信息,比如如果在删除一个用户的时候,因为用户不在列表中我抛出了一个异常,那么异常就要写明白,要删除的用户的id是什么以及其他环境信息,这样可以帮助我们排查问题。
写代码的时候一定要去考虑要各种异常的情况并做好稳妥的处理,这样才能写出更加鲁棒性高并且易于debug的代码。
网友评论