|
| 1 | +--- |
| 2 | +title: 适当地引入防卫性编程 |
| 3 | +tags: JavaScript 封装 接口 重构 弱类型 |
| 4 | +--- |
| 5 | + |
| 6 | +> Anything that can go wrong will go wrong. -- Edward Murphy |
| 7 | +
|
| 8 | +**防卫性编程(Defensive Programming)** 是指限制对程序的不可预见的使用,增加软件的安全性。 |
| 9 | +防卫性编程在程序鲁棒性、可维护性上都有帮助,尤其是在你不幸地选择弱类型语言编写源码时。 |
| 10 | + |
| 11 | +在 C++ STL 程序设计中,我们称函数模板和类模板为 [隐式接口][cpp-41],这些接口描述了编译期多态。 |
| 12 | +在 JavaScript 中,接受一个对象时也不需要声明其类型,只有后续对它的使用方式描述了它的接口。 |
| 13 | +[Harttle](/) 把 JavaScript 中的这一现象称为 **隐式接口**。 |
| 14 | + |
| 15 | +**隐式接口调试困难**。顾名思义隐式接口是无法使用工具检查的,因此只能依赖运行时调试。 |
| 16 | +那么当 `page.controller` 为空时,抛出的错误会包含有用信息吗? |
| 17 | + |
| 18 | +```javascript |
| 19 | +function init(page) { |
| 20 | + page.controller(); |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +具体的错误消息取决于你的JavaScript引擎,可能是 "page.controller is not a function", |
| 25 | +可能是 "undefined is not a function"。 |
| 26 | +如果是 uglify 后的代码,那就更难定位问题了。 |
| 27 | + |
| 28 | +**隐式接口容错困难**。因为接受的对象没有类型保证,但我们可以进行容错。 |
| 29 | +思路很简单:为了避免被调用接口为空,我们事先判断它。 |
| 30 | + |
| 31 | +```javascript |
| 32 | +function init(page) { |
| 33 | + page && page.view && page.view.start && page.view.start() |
| 34 | + page && page.controller && page.controller.start && page.controller.start() |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +这样的 Duck Test 其实在 JavaScript 中随处可见,然而这样正确性就不明显了: |
| 39 | + |
| 40 | +* 如果 `view.controller` 为空,应该吞掉这个错误吗?假设我们都认为随处吞掉错误是很烂的实践。 |
| 41 | +* 即使 `view.start` 存在,那么它是一个 `function` 么? |
| 42 | +* 即使 `view.start` 是一个 `function`,那么它是想要的那个 `function` 么? |
| 43 | + |
| 44 | +**隐式接口重构困难**。因为没有限制接受的输入范围,可能会有很多作者预期之外的使用方式, |
| 45 | +这些使用方式会让重构变得困难,例如: |
| 46 | + |
| 47 | +我们有一个发送 HTTP 请求的接口,我们只想让它发送 POST/PUT 请求, |
| 48 | +并对这两种请求做了特殊处理,比如加了时间戳或者安全性封装等等: |
| 49 | + |
| 50 | +```javascript |
| 51 | +function writeRequest(url, method) { |
| 52 | + let req = construct(url, method) |
| 53 | + return req.send() |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +有一天可能需要对 POST 和 PUT 做单独的逻辑,我们把它重构成如下形式: |
| 58 | + |
| 59 | +```javascript |
| 60 | +function writeRequest(url, method) { |
| 61 | + let req |
| 62 | + if (method === 'POST') { |
| 63 | + req = construct(url, 'POST') |
| 64 | + // do some thing here ... |
| 65 | + } else { |
| 66 | + // WHAT IF method != 'PUT' ? |
| 67 | + req = construct(url, 'PUT') |
| 68 | + } |
| 69 | + req.send() |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +在 ELSE 分支中,如果 `method !== "PUT"` 怎么办?一搜代码库,发现真有地方 `method` 就是 `GET`。 |
| 74 | +重构前的代码无意中支持了 `GET`?! |
| 75 | +现在如果不再支持 GET 则会不兼容地挂掉客户代码, |
| 76 | +如果转向支持 GET 则会与设计初衷,甚至函数名 `writeRequest` 相违背。 |
| 77 | + |
| 78 | +> 这里虽然是取值范围引起的重构困难,而根据定义,数据的取值范围就是类型(比如把它做成一个枚举类型就可以等价)。 |
| 79 | +
|
| 80 | +在 JavaScript 中我们确实无法编译期检查类型(甚至没有编译阶段), |
| 81 | +你可以选择类似 TypeScript 之类的语言。或者在设计接口时引入 **防卫性编程的思想**: |
| 82 | + |
| 83 | +1. 确定可接受的输入范围 |
| 84 | +2. 在入口处检查这一范围是否得到了满足 |
| 85 | +3. 对所有输入都产出符合预期的输出(行为/返回值),最好再配一项测试 |
| 86 | + |
| 87 | +例如: |
| 88 | + |
| 89 | +```javascript |
| 90 | +function writeRequest(url, method) { |
| 91 | + assert(url, 'cannot POST to malformed url') |
| 92 | + assert(/^POST|PUT$/.test(method), `method not supported: ${method}`) |
| 93 | + |
| 94 | + let req = construct(url, method) |
| 95 | + return req.send() |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +这样上文中提到的隐式接口的各种问题都可以得到不同程度的解决: |
| 100 | + |
| 101 | +* 调试困难。现在可以抛出具有足够信息的错误,便于调试。上述例子中 `assert` 的第二个参数可以极尽详细地描述错误。 |
| 102 | +* 容错困难。实现应满足一切合法输入,不再需要在实现的过程中进行容错,减少容错也让正确性更加明显。 |
| 103 | +* 重构困难。防卫性的接口描述可以做到足够清晰,接口描述不再影响重构。上述例子中,只需要继续支持 `POST`, `PUT` 即可保持接口的向后兼容。 |
| 104 | + |
| 105 | +[cpp-41]: /2015/09/08/effective-cpp-41.html |
0 commit comments