迁移练习 · 8 个任务

minikv 重构项目

读完一章,回到一份真实的 C++ 代码里把学到的东西用一遍

minikv 是什么

读书时每条原则都能点头,可面对一份"能跑、有测试、看着也还行"的代码,你能指出它哪里复杂、并且改得更简单吗?

minikv 是本站原创的一个小型键值存储,约 1400 行 C++17,没有第三方依赖。它的功能完整:

它是按"赶工期的战术式写法"写的:一个能干但很赶的团队会写出来的样子。没有离谱的错误,也没有故意写坏的语法;问题都藏在设计里:谁知道什么、接口要求使用者知道什么、同一件事写了几遍。这正是这本书要训练的眼光。如果你读过 OSTEP,日志、重放、崩溃一致性这些机制你会很熟悉(OSTEP 第 42、43 章),这里的重点不是机制,而是代码怎么组织。

结构图

一条命令自上而下穿过这些模块。方块上的标签是处理该部分问题的任务。

main.cpp 读 stdin,一行一条命令 Serverserver.cpp tokenize · 10 个 handleXxx · bulk 格式化回复 m05 m06 m08 Databasedatabase.cpp 大多数方法转发给 Store · maybeCompact 决定何时压缩 m03 m04 Options WriteOptions Storestore.cpp set · del · incr · append · getRange · 重放 · 压缩 m02 m07 Index unordered_map 包装 日志相关的 6 个类 m01 LogWriter LogReader RecordParser FileHandle ByteWriter / ByteReader crc32() data.log
基线 minikv 的模块与调用关系(本站自绘)。读完任务后回来看:哪些方块该合并,哪些边该消失?

获取、构建、运行、测试

需要 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 的关键路径重新设计不变

怎么做、怎么自查

每个任务的节奏

  1. 只读不改:按任务页"你要找的问题"读指定的文件,把发现写下来。能说清楚"问题在哪、为什么是问题",比改代码更重要。
  2. 从干净的基线开始:复制一份基线再动手,例如 cp -r minikv work-m03,或在 git 里每个任务开一个分支。
  3. 保持测试全绿:除了 m06、m07 这两个明确要求改变或增加行为的任务,tests/ 一行都不许改。测试是黑盒的,只要外部行为不变,内部随你重组。
  4. 只做这一个任务:基线里同时有很多问题,忍住别顺手改别的,否则没法和参考答案对照,也说不清每个改动是为了什么。
  5. 对照参考答案:先自己做完,再展开任务页底部的参考答案,读 solutions/mNN/DISCUSSION.md。

自查的三个问题

  1. 使用者要知道的东西变少了吗?列出改动前后调用方必须知道的条目(包括只能写在注释里的规则),数一数。
  2. 同一个知识现在在几个地方?如果某个决定(格式、阈值、规则)要改,你需要碰几个文件?
  3. 你付出了什么代价?每个参考答案都写了取舍。说不出代价的改动,往往是还没想清楚。

和参考答案对照

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 里没列、你却认为是问题的地方?后者很可能是你判断力在长进的信号。