minikv 重构项目
读完一章,回到一份真实的 C++ 代码里把学到的东西用一遍
minikv 是什么
读书时每条原则都能点头,可面对一份"能跑、有测试、看着也还行"的代码,你能指出它哪里复杂、并且改得更简单吗?
minikv 是本站原创的一个小型键值存储,约 1400 行 C++17,没有第三方依赖。它的功能完整:
- Redis 风格的文本命令:
SETGETDELEXISTSINCRAPPENDGETRANGEKEYSSTATSCOMPACT; - 所有数据在内存里,每次修改追加写到日志文件(带 CRC 校验),启动时重放;
- 日志里的垃圾多了以后自动压缩:把活数据写到新文件,原子地替换旧日志;
- 26 个测试,只通过命令接口和"关掉再重新打开日志"来检查行为。
它是按"赶工期的战术式写法"写的:一个能干但很赶的团队会写出来的样子。没有离谱的错误,也没有故意写坏的语法;问题都藏在设计里:谁知道什么、接口要求使用者知道什么、同一件事写了几遍。这正是这本书要训练的眼光。如果你读过 OSTEP,日志、重放、崩溃一致性这些机制你会很熟悉(OSTEP 第 42、43 章),这里的重点不是机制,而是代码怎么组织。
结构图
一条命令自上而下穿过这些模块。方块上的标签是处理该部分问题的任务。
获取、构建、运行、测试
需要 Linux 或 WSL、支持 C++17 的 g++(本站用 g++ 14 测过,-Wall -Wextra 零警告)、make。
编译、跑测试、交互试几条命令:
cd project/minikv
make test # 编译并运行全部测试,最后一行应是 26/26 tests passed
./minikv data.log # 交互模式,QUIT 或 Ctrl-D 退出
printf 'SET a 1\nINCR a 41\nGET a\nSTATS\n' | ./minikv data.log
make bench # 微基准,任务 m08 会用到目录里还有 README.md(命令列表、文件结构、日志格式),建议先读一遍,就像你刚加入这个团队。
project/ 下面的结构:
project/
minikv/ 基线:你要改的代码
solutions/m01/ … m08/ 参考答案:每个都是"基线 + 只做这一个任务",附 DISCUSSION.md
solutions/MAP.md 全部埋下的问题与章节的对照表(做完再看)
8 个任务
任务和章节一一对应,建议按顺序做:读完一章(或几章)→ 做对应任务 → 对照参考答案。每个任务都从干净的基线出发,彼此独立,所以也可以跳着做。
| 任务 | 对应章节 | 你要做的事 | 测试 |
|---|---|---|---|
| m01 | 第 4 章、第 5 章 | 日志格式散在三个类里,外加一圈浅类和一个暴露过多的 open:把日志做成一个深模块 | 不变 |
| m02 | 第 6 章(+ 第 11 章) | 每个命令一个专用的存储方法、各自重复写日志和索引:先设计两套接口,再收敛成一个通用的核心 | 不变 |
| m03 | 第 7 章 | 一层层原样转发的方法,和一个穿过四层只为到达一处的变量:让每一层提供不同的抽象 | 不变 |
| m04 | 第 8 章 | 压缩阈值等参数全推给使用者去调:自己算,只留下使用者真正更懂的那一个 | 不变 |
| m05 | 第 9 章 | 复制粘贴的参数检查、靠成员变量暗中通信的两个方法、知道具体命令的通用切词器:决定什么该合、什么该分 | 不变 |
| m06 | 第 10 章 | 把能定义掉的错误定义掉,散落的 catch 聚合起来,末尾半条记录在底层屏蔽,真损坏就带着清楚的信息崩溃 | 行为有变,附新测试 |
| m07 | 第 12~15 章 | 含糊的名字、重复代码的注释、被实现污染的接口注释;再先写注释、后实现一个新命令 RENAME | 新增 RENAME 测试 |
| m08 | 第 18 章、第 20 章 | pair 返回值、bool 参数、出人意料的副作用;然后先测量,再围绕 GET 的关键路径重新设计 | 不变 |
怎么做、怎么自查
每个任务的节奏
- 只读不改:按任务页"你要找的问题"读指定的文件,把发现写下来。能说清楚"问题在哪、为什么是问题",比改代码更重要。
- 从干净的基线开始:复制一份基线再动手,例如
cp -r minikv work-m03,或在 git 里每个任务开一个分支。 - 保持测试全绿:除了 m06、m07 这两个明确要求改变或增加行为的任务,
tests/一行都不许改。测试是黑盒的,只要外部行为不变,内部随你重组。 - 只做这一个任务:基线里同时有很多问题,忍住别顺手改别的,否则没法和参考答案对照,也说不清每个改动是为了什么。
- 对照参考答案:先自己做完,再展开任务页底部的参考答案,读
solutions/mNN/DISCUSSION.md。
自查的三个问题
- 使用者要知道的东西变少了吗?列出改动前后调用方必须知道的条目(包括只能写在注释里的规则),数一数。
- 同一个知识现在在几个地方?如果某个决定(格式、阈值、规则)要改,你需要碰几个文件?
- 你付出了什么代价?每个参考答案都写了取舍。说不出代价的改动,往往是还没想清楚。
和参考答案对照
cd project # minikv/ 和 solutions/ 所在的目录
make -C minikv clean && make -C solutions/m03 clean
diff -ru minikv solutions/m03 # 或者 diff -ru work-m03 solutions/m03
make -C solutions/m03 test
参考答案只是一种合理的设计。你的方案和它不同,不代表错;能用上面三个问题讲清楚它为什么更简单、代价是什么,就是好的方案。最好找一位同事互相审代码:书的作者也说,问题在别人的代码里更容易看出来。
solutions/MAP.md 列出了基线里埋下的全部 27 个问题,以及几处"看起来像问题但其实没问题"的代码。全部任务做完后,拿它检查一下:有没有你自己发现了、但任务没要求的问题?有没有 MAP 里没列、你却认为是问题的地方?后者很可能是你判断力在长进的信号。