项目参考https://github.com/miloyip/json-tutorial
https://sourceforge.net/projects/cjson/
一、实现 首先我们要明白什么是JSON库,在了解JSON库之前就需要JSON是什么,这里我们引用网络爬虫那一篇里关于JSON的介绍
1.JSON数据格式
JSON,全称为 JavaScript Object Notation, 也就是 JavaScript 对象标记,它通过对象和数组的组合来表示数据,构造简洁但是结构化程度非常高,是一种轻量级的数据交换格式
;
1.1 JSON数据 JSON数据可能的数据类型为如下6种:
null: 表示为 null
boolean: 表示为 true 或 false
number: 一般的浮点数表示方式,如 3.14
string: 表示为 “…”
千万注意 JSON 字符串类型的数据表示需要使用双引号而非单引号(这点与其他编程语言不同)!
array: 表示为 [ … ]
object: 表示为 { … }
以上六种数据类型中最常用的是数组和对象(因为JSON数据本质上就是一个序列化的对象或数组,至于字符串等类型只是其组成部分):
对象
:它在 JavaScript 中是使用花括号 {} 包裹起来的内容,数据结构为 {key1:value1, key2:value2, …} 的键值对结构,键名只能使用字符串,键值可以是以上任何数据类型
;
数组
:数组在 JavaScript 中是方括号 [] 包裹起来的内容,数据结构为 [“java”, “javascript”, “vb”, …] 的索引结构,数组值的类型可以是以上任意类型
;
一个 JSON 数据可以写为如下形式(由于最外层是中括号,所以最终该JSON数据的类型是列表类型):
1 2 3 4 5 6 7 8 9 [ { "name" : "Bob" , "gender" : "male" , "birthday" : "1992-10-18" } , { "name" : "Selina" , "gender" : "female" , "birthday" : "1995-10-18" } ]
尽管JSON源自JS语言,但它只是一种数据格式,可用于任何编程语言(JSON、XML、YAML这方面具有相似性);
1.2 JSON库
我们可以调用 JSON 库的 loads 方法将文本字符串转为 JSON 数据,可以通过 dumps() 方法将 JSON 数据转为文本字符串;
1.2.1 JSON文本转换为JSON数据 如果从 JSON 文本中读取内容,例如这里有一个 data.json 文本文件,其内容是之前定义过的 JSON 字符串
(一定注意JSON文本文件中保存的就是普通的字符串
!!!因为这种字符串可以转换为JSON数据所以我们称为JSON字符串或JSON文本),我们可以先将文本文件内容读出,然后再利用 loads 方法转化
1 2 3 4 5 6 7 8 9 10 11 #data.json[ { "name" : "Bob" , "gender" : "male" , "birthday" : "1992-10-18" } , { "name" : "Selina" , "gender" : "female" , "birthday" : "1995-10-18" } ]
1 2 3 4 5 6 import jsonwith open ('data.json' , 'r' ) as file: str = file.read() data = json.loads(str ) print (data)
运行结果如下
1 [{'name' : 'Bob' , 'gender' : 'male' , 'birthday' : '1992-10-18' }, {'name' : 'Selina' , 'gender' : 'female' , 'birthday' : '1995-10-18' }]
1.2.2 JSON数据转换为JSON文本 一般我们将JSON数据转换为JSON文本是为了保存相关信息,对于保存JSON文本有两种方法:
一种是先转换为JSON文本再调用文件的write()方法写入文本,保存为普通文本字符串格式
;
1 2 with open ('data.json' , 'w' ) as file: file.write(json.dumps(data))
另一种是先将JSON数据转换为文本字符串,再将文本字符串转调整为JSON树状格式
保存(增加参数indent表示缩进字符个数使调整为JSON格式),本质上还是普通文本字符串只是缩进更美观;
1 2 with open ('data.json' , 'w' ) as file: file.write(json.dumps(data, indent=2 ))
当写入文本中有中文时,需要指定参数 ensure_ascii 为 False,另外还要规定文件输出的编码
1 2 with open ('data.json' , 'w' , encoding='utf-8' ) as file: file.write(json.dumps(data, indent=2 , ensure_ascii=False ))
我们从上面也可以看到,Python中处理JSON文本就借助了Python中的JSON库,当然我们这里要实现的是以C/C++来实现一个JSON库,我们的JSON库主要完成三个需求:
把 JSON 文本
解析为一个JSON数据
(parse);
提供接口访问该数据结构(access);
把JSON数据转换成 JSON 文本(stringify);
2.Parse部分 本部分主要介绍如何将JSON文本转换为JSON数据
1 2 3 4 5 6 7 8 输入 -> 输出 字符串类型:"\" Hello \" -> "Hello" 数值类型:"123.31" -> 123.31 空值:"null" -> null 布尔值:"false" -> false 布尔值:"true" ->true 数组:"[123]" -> [123 ] 对象:"{" hello ":123}" -> {"hello" :123 }
需要注意的有几点:
我们在测试的时候输入的都是JSON文本(这是符合实际生产要求的,因为我们从.json文件中取出来的也是json文本),也就是带双引号的json数据,而转换过后的JSON数据只有字符串类型是带双引号的;
至于为什么测试JSON字符串的时候需要加上转义的双引号,因为我们在测试的时候,最外层的双引号模拟的是.json文件,双引号内部的数据才是.json文件中的内容,内部的转义双引号模拟的是.json文件中的json字符串;
可能很多人分不清json文本和json数据的区别,我们可以简单理解为,json文本是加了双引号的json数据;
2.1 数据结构 2.1.1 节点类型 JSON 中有 6 种数据类型,如果把 true 和 false 当作两个类型就是 7 种,为此声明一个lept_type枚举类型
1 typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;
2.1.2 节点结构 JSON数据是树结构,每个节点使用lept_value结构体表示,我们称它为一个JSON数据值
1 2 3 4 5 6 7 8 9 struct lept_value { union { struct { lept_member* m; size_t size; }o; struct { lept_value* e; size_t size; }a; struct { char * s; size_t len; }s; double n; }u; lept_type type; };
2.1.3 对象成员 JSON对象由对象成员构成,对象成员就是键值对,其中键只能是字符串类型,值可以是七种类型中的任何一种;
这里我们使用lept_value结构体来保存对象成员的值
,但是对象成员的键
我们舍弃使用lept_value因为不需要type这个字段;
1 2 3 4 struct lept_member { char * k; size_t klen; lept_value v; };
2.1.4 Debug标识 借助枚举常量的方式定义一些必要的Debug标识,lept_parse()函数的返回值是这些枚举值其中之一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enum { LEPT_PARSE_OK = 0 , LEPT_PARSE_EXPECT_VALUE, LEPT_PARSE_INVALID_VALUE, LEPT_PARSE_ROOT_NOT_SINGULAR, LEPT_PARSE_NUMBER_TOO_BIG, LEPT_PARSE_MISS_QUOTATION_MARK, LEPT_PARSE_INVALID_STRING_ESCAPE, LEPT_PARSE_INVALID_STRING_CHAR, LEPT_PARSE_INVALID_UNICODE_HEX, LEPT_PARSE_INVALID_UNICODE_SURROGATE, LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET, LEPT_PARSE_MISS_KEY, LEPT_PARSE_MISS_COLON, LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET
2.1.5 缓冲结构 在JSON解析的时候,我们需要一个合适的缓冲区结构体来存储临时的解析结果(特别是像字符串、数组、对象这种很长的数据类型)
1 2 3 4 5 typedef struct { const char * json; char * stack ; size_t size, top; }lept_context;
2.2 解析器 我们分别按照从简单数据类型到复合数据类型的方式介绍(复合数据类型的解析器采用的是简单类型解析器的递归调用)
2.2.1 解析器外部接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int lept_parse (lept_value* v, const char * json) { lept_context c; int ret; assert(v != NULL ); c.json = json; c.stack = NULL ; c.size = c.top = 0 ; lept_init(v); lept_parse_whitespace(&c); if ((ret = lept_parse_value(&c, v)) == LEPT_PARSE_OK) { lept_parse_whitespace(&c); if (*c.json != '\0' ) { v->type = LEPT_NULL; ret = LEPT_PARSE_ROOT_NOT_SINGULAR; } } assert(c.top == 0 ); free (c.stack ); return ret; }
2.2.2 解析器核心 这部分负责选择将JSON文本交给哪个解析器进行解析
1 2 3 4 5 6 7 8 9 10 11 12 static int lept_parse_value (lept_context* c, lept_value* v) { switch (*c->json) { case 't' : return lept_parse_literal(c, v, "true" , LEPT_TRUE); case 'f' : return lept_parse_literal(c, v, "false" , LEPT_FALSE); case 'n' : return lept_parse_literal(c, v, "null" , LEPT_NULL); default : return lept_parse_number(c, v); case '"' : return lept_parse_string(c, v); case '[' : return lept_parse_array(c, v); case '{' : return lept_parse_object(c, v); case '\0' : return LEPT_PARSE_EXPECT_VALUE; } }
(1)literal解析器 该解析器处理 “null” “true” “false” 的JSON文本;
注意之前提出的一个问题:为什么不能直接使用memcmp()这种函数比较是否是null true false而使用指针挨着比较每个字符呢?
实际上我们使用memcmp()也能解决这个问题,作者应该是为了让我们更加熟练的使用指针所以使用了指针的方法;
1 2 3 4 5 6 7 8 9 10 static int lept_parse_literal (lept_context* c, lept_value* v, const char * literal, lept_type type) { size_t i; EXPECT(c, literal[0 ]); for (i = 0 ; literal[i + 1 ]; i++) if (c->json[i] != literal[i + 1 ]) return LEPT_PARSE_INVALID_VALUE; c->json += i; v->type = type; return LEPT_PARSE_OK; }
1 2 3 4 5 6 7 8 9 10 static int lept_parse_true (lept_context* c, lept_value* v) { if (memcmp (c->json,"true" ,4 )!=0 ) { return LEPT_PARSE_INVALID_VALUE; } c->json += 4 ; v->type = LEPT_TRUE; return LEPT_PARSE_OK; }
(2)number解析器 该解析器处理json值为数字的JSON文本;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static int lept_parse_number (lept_context* c, lept_value* v) { const char * p = c->json; if (*p == '-' ) p++; if (*p == '0' ) p++; else { if (!ISDIGIT1TO9(*p)) return LEPT_PARSE_INVALID_VALUE; for (p++; ISDIGIT(*p); p++); } if (*p == '.' ) { p++; if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE; for (p++; ISDIGIT(*p); p++); } if (*p == 'e' || *p == 'E' ) { p++; if (*p == '+' || *p == '-' ) p++; if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE; for (p++; ISDIGIT(*p); p++); } errno = 0 ; v->n = strtod(c->json, NULL ); if (errno == ERANGE && (v->n == HUGE_VAL || v->n == -HUGE_VAL)) return LEPT_PARSE_NUMBER_TOO_BIG; v->type = LEPT_NUMBER; c->json = p; return LEPT_PARSE_OK; }
(3)string解析器 处理json值为字符串的JSON文本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 static int lept_parse_string (lept_context* c, lept_value* v) { size_t head = c->top, len; const char * p; EXPECT(c, '\"' ); p = c->json; for (;;) { char ch = *p++; switch (ch) { case '\"' : len = c->top - head; lept_set_string(v, (const char *)lept_context_pop(c, len), len); c->json = p; return LEPT_PARSE_OK; case '\\' : switch (*p++) { case '\"' : PUTC(c, '\"' ); break ; case '\\' : PUTC(c, '\\' ); break ; case '/' : PUTC(c, '/' ); break ; case 'b' : PUTC(c, '\b' ); break ; case 'f' : PUTC(c, '\f' ); break ; case 'n' : PUTC(c, '\n' ); break ; case 'r' : PUTC(c, '\r' ); break ; case 't' : PUTC(c, '\t' ); break ; default : c->top = head; return LEPT_PARSE_INVALID_STRING_ESCAPE; } break ; case '\0' : c->top = head; return LEPT_PARSE_MISS_QUOTATION_MARK; default : if ((unsigned char )ch < 0x20 ) { c->top = head; return LEPT_PARSE_INVALID_STRING_CHAR; } PUTC(c, ch); } } }
(4)array解析器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 static int lept_parse_array (lept_context* c, lept_value* v) { size_t i, size = 0 ; int ret; EXPECT(c, '[' ); lept_parse_whitespace(c); if (*c->json == ']' ) { c->json++; v->type = LEPT_ARRAY; v->u.a.size = 0 ; v->u.a.e = NULL ; return LEPT_PARSE_OK; } for (;;) { lept_value e; lept_init(&e); if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) break ; memcpy (lept_context_push(c, sizeof (lept_value)), &e, sizeof (lept_value)); size++; lept_parse_whitespace(c); if (*c->json == ',' ) { c->json++; lept_parse_whitespace(c); } else if (*c->json == ']' ) { c->json++; v->type = LEPT_ARRAY; v->u.a.size = size; size *= sizeof (lept_value); memcpy (v->u.a.e = (lept_value*)malloc (size), lept_context_pop(c, size), size); return LEPT_PARSE_OK; } else { ret = LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET; break ; } } for (i = 0 ; i < size; i++) lept_free((lept_value*)lept_context_pop(c, sizeof (lept_value))); return ret; }
(5)object解析器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 static int lept_parse_object (lept_context* c, lept_value* v) { size_t i, size; lept_member m; int ret; EXPECT(c, '{' ); lept_parse_whitespace(c); if (*c->json == '}' ) { c->json++; v->type = LEPT_OBJECT; v->u.o.m = 0 ; v->u.o.size = 0 ; return LEPT_PARSE_OK; } m.k = NULL ; size = 0 ; for (;;) { char * str; lept_init(&m.v); if (*c->json != '"' ) { ret = LEPT_PARSE_MISS_KEY; break ; } if ((ret = lept_parse_string_raw(c, &str, &m.klen)) != LEPT_PARSE_OK) break ; memcpy (m.k = (char *)malloc (m.klen + 1 ), str, m.klen); m.k[m.klen] = '\0' ; lept_parse_whitespace(c); if (*c->json != ':' ) { ret = LEPT_PARSE_MISS_COLON; break ; } c->json++; lept_parse_whitespace(c); if ((ret = lept_parse_value(c, &m.v)) != LEPT_PARSE_OK) break ; memcpy (lept_context_push(c, sizeof (lept_member)), &m, sizeof (lept_member)); size++; m.k = NULL ; lept_parse_whitespace(c); if (*c->json == ',' ) { c->json++; lept_parse_whitespace(c); } else if (*c->json == '}' ) { size_t s = sizeof (lept_member) * size; c->json++; v->type = LEPT_OBJECT; v->u.o.size = size; memcpy (v->u.o.m = (lept_member*)malloc (s), lept_context_pop(c, s), s); return LEPT_PARSE_OK; } else { ret = LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET; break ; } } free (m.k); for (i = 0 ; i < size; i++) { lept_member* m = (lept_member*)lept_context_pop(c, sizeof (lept_member)); free (m->k); lept_free(&m->v); } v->type = LEPT_NULL; return ret; }
3.Stringify部分 3.1 数据结构 3.1.1 缓冲结构 这里使用的缓冲结构与解析器使用的缓冲结构是一样的
1 2 3 4 5 typedef struct { const char * json; char * stack ; size_t size, top; }lept_context;
3.2 生成器 3.2.1 生成器外部接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 char * lept_stringify (const lept_value* v, size_t * length) { lept_context c; assert(v != NULL ); c.stack = (char *)malloc (c.size = LEPT_PARSE_STRINGIFY_INIT_SIZE); c.top = 0 ; lept_stringify_value(&c, v); if (length) *length = c.top; PUTC(&c, '\0' ); return c.stack ; }
3.2.2 生成器核心 我们根据节点类型来辨别需要生成的JSON字符串;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static void lept_stringify_value (lept_context* c, const lept_value* v) { size_t i; switch (v->type) { case LEPT_NULL: PUTS(c, "null" , 4 ); break ; case LEPT_FALSE: PUTS(c, "false" , 5 ); break ; case LEPT_TRUE: PUTS(c, "true" , 4 ); break ; case LEPT_NUMBER: c->top -= 32 - sprintf (lept_context_push(c, 32 ), "%.17g" , v->u.n); break ; case LEPT_STRING: lept_stringify_string(c, v->u.s.s, v->u.s.len); break ; case LEPT_ARRAY: PUTC(c, '[' ); for (i = 0 ; i < v->u.a.size; i++) { if (i > 0 ) PUTC(c, ',' ); lept_stringify_value(c, &v->u.a.e[i]); } PUTC(c, ']' ); break ; case LEPT_OBJECT: PUTC(c, '{' ); for (i = 0 ; i < v->u.o.size; i++) { if (i > 0 ) PUTC(c, ',' ); lept_stringify_string(c, v->u.o.m[i].k, v->u.o.m[i].klen); PUTC(c, ':' ); lept_stringify_value(c, &v->u.o.m[i].v); } PUTC(c, '}' ); break ; default : assert(0 && "invalid type" ); } }
(1)string生成器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static void lept_stringify_string (lept_context* c, const char * s, size_t len) { static const char hex_digits[] = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' }; size_t i, size; char * head, *p; assert(s != NULL ); p = head = lept_context_push(c, size = len * 6 + 2 ); *p++ = '"' ; for (i = 0 ; i < len; i++) { unsigned char ch = (unsigned char )s[i]; switch (ch) { case '\"' : *p++ = '\\' ; *p++ = '\"' ; break ; case '\\' : *p++ = '\\' ; *p++ = '\\' ; break ; case '\b' : *p++ = '\\' ; *p++ = 'b' ; break ; case '\f' : *p++ = '\\' ; *p++ = 'f' ; break ; case '\n' : *p++ = '\\' ; *p++ = 'n' ; break ; case '\r' : *p++ = '\\' ; *p++ = 'r' ; break ; case '\t' : *p++ = '\\' ; *p++ = 't' ; break ; default : if (ch < 0x20 ) { *p++ = '\\' ; *p++ = 'u' ; *p++ = '0' ; *p++ = '0' ; *p++ = hex_digits[ch >> 4 ]; *p++ = hex_digits[ch & 15 ]; } else *p++ = s[i]; } } *p++ = '"' ; c->top -= size - (p - head); }
(2)数组生成器 生成数组只需要输出[],中间部分使用递归调用lept_stringify_value()
1 2 3 4 5 6 7 8 9 10 11 case LEPT_ARRAY: PUTC(c, '[' );for (i = 0 ; i < v->u.a.size; i++) { if (i > 0 ) PUTC(c, ',' ); lept_stringify_value(c, &v->u.a.e[i]); } PUTC(c, ']' );break ;
(3)对象生成器 生成对象主要分为两步,生成键(使用lept_stringify_string()递归调用)以及生成值(使用lept_stringify_value()),在键值之间需要添加:
1 2 3 4 5 6 7 8 9 10 11 case LEPT_OBJECT: PUTC(c, '{' );for (i = 0 ; i < v->u.o.size; i++) { if (i > 0 ) PUTC(c, ',' ); lept_stringify_string(c, v->u.o.m[i].k, v->u.o.m[i].klen); PUTC(c, ':' ); lept_stringify_value(c, &v->u.o.m[i].v); } PUTC(c, '}' );break ;
二、附录 1.单元测试 我们日常在写练习题的时候一般都是以printf/cout输出结果观察是否正确,但是随着软件项目越来越复杂这样做会非常低效,我们经常采用的一种方式是单元测试;
常用的单元测试框架有 xUnit 系列,如 C++ 的 Google Test 、C# 的 NUnit 。简单起见,本项目编写了一个极简单的单元测试方式。
一般来说,软件开发是以周期进行的。例如,加入一个功能,再写关于该功能的单元测试。但也有另一种软件开发方法论,称为测试驱动开发(test-driven development, TDD),它的主要循环步骤是:
加入一个测试。
运行所有测试,新的测试应该会失败。
编写实现代码。
运行所有测试,若有测试失败回到3。
重构代码。
回到 1。
TDD 是先写测试,再实现功能(单元测试是先加入功能再写测试)。好处是实现只会刚好满足测试,而不会写了一些不需要的代码,或是没有被测试的代码。
但无论我们是采用 TDD,或是先实现后测试,都应尽量加入足够覆盖率的单元测试。
回到 leptjson 项目,test.c
包含了一个极简的单元测试框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <stdio.h> #include <stdlib.h> #include <string.h> #include "leptjson.h" static int main_ret = 0 ;static int test_count = 0 ;static int test_pass = 0 ;#define EXPECT_EQ_BASE(equality, expect, actual, format) \ do {\ test_count++;\ if (equality)\ test_pass++;\ else {\ fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n" , __FILE__, __LINE__, expect, actual);\ main_ret = 1;\ }\ } while(0) #define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d" ) static void test_parse_null () { lept_value v; v.type = LEPT_TRUE; EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null" )); EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v)); }static void test_parse () { test_parse_null(); }int main () { test_parse(); printf ("%d/%d (%3.2f%%) passed\n" , test_pass, test_count, test_pass * 100.0 / test_count); return main_ret; }
现时只提供了一个 EXPECT_EQ_INT(expect, actual)
的宏,每次使用这个宏时,如果 expect != actual(预期值不等于实际值),便会输出错误信息。 若按照 TDD 的步骤,我们先写一个测试,如上面的 test_parse_null()
,而 lept_parse()
只返回 LEPT_PARSE_OK
:
1 2 /Users/mi loyip/github/ json-tutorial/tutorial01/ test.c:27 : expect: 0 actual: 1 1 /2 (50.00 %) passed
第一个返回 LEPT_PARSE_OK
,所以是通过的。第二个测试因为 lept_parse()
没有把 v.type
改成 LEPT_NULL
,造成失败。我们再实现 lept_parse()
令到它能通过测试。
然而,完全按照 TDD 的步骤来开发,是会减慢开发进程。所以我个人会在这两种极端的工作方式取平衡。通常会在设计 API 后,先写部分测试代码,再写满足那些测试的实现。