导言
最近实践了[Milo Yip] (https://www.zhihu.com/people/miloyip/answers) 的从零开始的 JSON 库教程,文中的几点自己觉得最有收获的地方。当然在课程设计上,用%进度来显示完成的情况,真的是非常用心。
JSON 是什么
JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为ECMA-404。
虽然 JSON 源至于 JavaScript 语言,但它只是一种数据格式,可用于任何编程语言。现时具类似功能的格式有 XML、YAML,当中以 JSON 的语法最为简单。
例如,一个动态网页想从服务器获得数据时,服务器从数据库查找数据,然后把数据转换成 JSON 文本格式:
{
"title": "Design Patterns",
"subtitle": "Elements of Reusable Object-Oriented Software",
"author": [
"Erich Gamma",
"Richard Helm",
"Ralph Johnson",
"John Vlissides"
],
"year": 2009,
"weight": 1.8,
"hardcover": true,
"publisher": {
"Company": "Pearson Education",
"Country": "India"
},
"website": null
}
网页的脚本代码就可以把此 JSON 文本解析为内部的数据结构去使用。
从此例子可看出,JSON 是树状结构,而 JSON 只包含 6 种数据类型:
- null: 表示为 null
- boolean: 表示为 true 或 false
- number: 一般的浮点数表示方式,在下一单元详细说明
- string: 表示为 “…”
- array: 表示为 [ … ]
- object: 表示为 { … }
我们要实现的 JSON 库,主要是完成 3 个需求:
- 把 JSON 文本解析为一个树状数据结构(parse)。
- 提供接口访问该数据结构(access)。
- 把数据结构转换成 JSON 文本(stringify)。
虽然JSON在项目中有使用,却很少去关注,JSON遵循了什么协议要求,在以上的内容还看出了,关于需求的分解和实践上的有突破点,这反而是刚入行的很少去注意这样了。
单元测试
在做练习题时,都是以 printf
/cout
打印结果,再用肉眼对比结果是否乎合预期。但当软件项目越来越复杂,这个做法会越来越低效。一般我们会采用自动的测试方式,例如单元测试(unit testing)。单元测试也能确保其他人修改代码后,原来的功能维持正确(这称为回归测试/regression testing)。
常用的单元测试框架有 xUnit 系列,如 C++ 的 Google Test、C# 的 NUnit。我们为了简单起见,会编写一个极简单的单元测试方式。
一般来说,软件开发是以周期进行的。例如,加入一个功能,再写关于该功能的单元测试。但也有另一种软件开发方法论,称为测试驱动开发(test-driven development, TDD),它的主要循环步骤是:
- 加入一个测试。
- 运行所有测试,新的测试应该会失败。
- 编写实现代码。
- 运行所有测试,若有测试失败回到3。
- 重构代码。
- 回到 1。
TDD 是先写测试,再实现功能。好处是实现只会刚好满足测试,而不会写了一些不需要的代码,或是没有被测试的代码。
但无论我们是采用 TDD,或是先实现后测试,都应尽量加入足够覆盖率的单元测试。
这样的规范的开发流程,在没有经历过很规范的项目或者团队,真的没有机会有这样的机会去了解。在自己实际开发流程中,是自己设定边界值去测试,而不是加入单元测试的来测试。规范的好处在于降低理解业务的内容难度,即使其他人的业务也可以保证正确
重构
在软件工程中,代码重构(code refactoring)是指在不改变软件外在行为时,修改代码以改进结构。代码重构十分依赖于单元测试,因为我们是通过单元测试去维护代码的正确性。有了足够的单元测试,我们可以放胆去重构,尝试并评估不同的改进方式,找到合乎心意而且能通过单元测试的改动,我们才提交它。
最后,我希望指出,软件的架构难以用单一标准评分,重构时要考虑平衡各种软件品质。例如上述把 3 个函数合并后,优点是减少重复的代码,维护较容易,但缺点可能是带来性能的少量影响。
重构:减少重复的代码,维护较容易。在实际项目中,进行方法提取,减少重复的代码,以达到代码简洁和清晰。当然也可以实现功能中发现,发现有方法可以复用,进行方法提取。
JSON数字语法
回归正题,本单元的重点在于解析 JSON number 类型。我们先看看它的语法:
number = [ "-" ] int [ frac ] [ exp ]
int = "0" / digit1-9 *digit
frac = "." 1*digit
exp = ("e" / "E") ["-" / "+"] 1*digit
number 是以十进制表示,它主要由 4 部分顺序组成:负号、整数、小数、指数。只有整数是必需部分。注意和直觉可能不同的是,正号是不合法的。
整数部分如果是 0 开始,只能是单个 0;而由 1-9 开始的话,可以加任意数量的数字(0-9)。也就是说,0123 不是一个合法的 JSON 数字。
小数部分比较直观,就是小数点后是一或多个数字(0-9)。
JSON 可使用科学记数法,指数部分由大写 E 或小写 e 开始,然后可有正负号,之后是一或多个数字(0-9)。
JSON 标准 ECMA-404 采用图的形式表示语法,也可以更直观地看到解析时可能经过的路径: