综述
javascript 属于弱类型语言,参数的类型错误只能在运行期发现。当你需要 expose “非常健壮”的接口给外部,或者在调试较大项目的时候,你可能会怀念强类型语言的类型约束,或者 assert
一类东西。
正因为 js 没有类型约束,也没有 assert
这样的“契约型”断言工具,所以同一个人写出的 js 代码,健壮性常常是不稳定的,有时约束多,有时约束少,有时候返回 null,有时候抛异常,并且约束代码也常常不统一放在函数入口处。
本文尝试编写一种参数检查工具,期待能缓解类似问题。
参数检查
假设,我们需要给所有接口统一添加稳定的约束,以及约束破坏后统一的反馈行为(比如崩溃),除了语言原生支持(听说 Eiffel 有这个能力,有兴趣的可以 google 下),最直接的方法就是设计一个类似 assert
的参数检查函数 check
,在每个函数入口处调用 check
检查参数,如果检查失败则执行既定的失败反馈。
如果所有的函数都这样编写,就可以保证所有函数严格执行约束,约束破坏后立刻停止运行,并打印相应的信息。
接口
我们很容易大致设想一个 check 接口的模样——
check.setCheckFailedCallback(function (e) {});
function test(a) {
check(a).检查1(条件1).检查2(条件2)……
}
有几个细节需要讨论一下:
上面的代码使用了链式调用,链式调用的必要性是很显然的——我们需要一种组合检查步骤的方式。为了实现链式调用,
check
返回的是一个特殊的包装对象Checker
。当参数
a
通过所有检查后,代码向下执行。如果有一个检查没有通过,此时需要执行一个反馈。由于外层代码可能存在try
块,所以这里抛异常是不可靠的,或者说我们要想一个办法抛出一个“不可 catch”的异常。这里采用的最简单的办法,上层设置回调函数checkFailedCallback
,检查失败后自行处理结果,同时抛出一个异常。check(a)
这种写法,实际上是做不到的。js 里没有宏,所以没有办法接受一个变量同时拿到变量的名称。如果要打印出检查失败的参数名,需要写成check(a, 'a')
。这种写法有点累赘,可能有更好的方案,我还在思考。
逻辑组合
刚才说到链式调用可以用来组合检查步骤,但是只有一种组合方式显然是不行的。因为检查步骤之间的关系可能有三种:与、或、非。我们要想办法使用同一的规则把三种关系表达清楚。
具体就不解释了,分享一下我的规则:
链式调用实现“与”:
// a 是 number 型,并且大于 1 小于 3
check(a, 'a').is('number').gt(1).lt(3);
参数表实现“或”:
// a 是 number 型,并且位于 [0, 1) || (1, 2] 区间上
check(a, 'a').is('number').within('[0, 1)', '(1, 2]');
注:由于参数表实现“或”,所以这里“或”的优先级永远比“与”高,如果需要“与”比“或”高,则需要一点技巧,具体见我这篇文章。
not 属性实现“非”:
// a 是字符串并且不符合正则表达式 /^[\w][\w\d]+$/
check(a, 'a').is('string').not.match(/^[\w][\w\d]+$/);
// a 是字符串并且不符合正则表达式 /^[\w][\w\d]+$/, 并且长度等于 10
check(a, 'a').is('string').not.match(/^[\w][\w\d]+$/).length().eq(10);
注:
not 是一个特殊属性,会返回一个特殊对象
NotChecker
,这个对象使用try
执行原对象的检查方法,catch
到异常则认为检查通过。并且NotChecker
的检查方法返回的是原对象而不是自己,所以not.match
之后连接length
时,已经不再not
的作用范围。-
由于德摩根定律的存在,not 后的参数表实际上在表达"与"的关系,比如:
check(a, 'a').not.is('string', 'number').
表示的是参数 a 既不为 string 也不为 number。
其他
另外,为了方便使用,还需要实现一些另外的接口,比如:
// a 包含属性 foo,大于 1 小于 3; 同时包含属性 bar, 大于 2 小于 4
check(a, 'a').has('foo').gt(1).lt(3).owner.has('bar').gt(2).lt(4);
注:
上面的代码中,
has
是一个特殊方法,它检验参数中是否包含指定的属性(own property),如果包含,就返回一个包装该属性的 Checker,否则抛检查失败的异常。owner
是一个特殊属性,它返回包装上一层对象的 Checker 对象。所以我们可以在调用has
检查属性之后,调用owner
“跳回去”继续检查上层对象。
代码
为了检验上面的想法,我实现了一个 js 库 param-check,代码位于:
https://github.com/yusangeng/param-check
因为只是一个语言切换是产生的 idea,所以目前这个库还不完善,实际能有多大意义还不好说,对性能和编程范式的影响还需要评估。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。