最新发布的 org-zettel-ref-mode (以下简称 OZR)版本号从 0.3.3 跳到 0.4,在这一轮冲刺中,我实现了比之前更加复杂的功能,更多的代码行数,更加复杂的结构。而所花费的时间,是过去的三分之一,我认为自己取得了可喜的进步。
OZR 新的版本,增加了一个新功能,用于管理引用来源文件的可视化面板。这句话看上去很简单,但背后的工作量并不小。前端改一个词,足以让后端跑断腿。出现产品经理被砍这样的新闻,也不必奇怪了。
可视化面板,是很多产品里常见的特性,它的功能就是,让用户更加舒服地查看、管理特定的对象。在 OZR 里,其管理对象是,一个个被充当引用来源的参考文件。
如果让你来设计这个面板,你觉得第一个问题会是什么?我一开始的反映是,文件应该以什么样的方式进行展示?但实际上,在我真正着手开发时,真正重要的问题是: 在面板里展示的数据源来自哪里? 在数据源发生了改动之后,如何确保原来的文件映射关系不变?
继续追问这两个问题,会发现,从工程上需要实现:
- 将原本非格式化的数据转换成格式化数据,保存到数据库中,面板访问数据库才能既快又好地展现文件信息
- 由于用户会在面板上编辑文件信息,因此面板上更新的数据,需要同步到数据库对应的字段中,进行信息层面的对齐,这样子才能确保面板在重新加载之后不会出现错误的信息
从这 2 个问题继续追问,就会出现大量需要实现的工程细节。但最重要的一个问题是,需要使用一个怎么样的数据库,才能够满足以上需求。再深挖这个问题,就是数据库应当使用什么结构,能够满足当前的业务需求?再追问这个问题,就会回到业务层面上,实际业务的情景是什么?
设计数据库之前需要先探明业务情景
在没弄清楚实际业务发生的情形之前,我所尝试的各种实现数据库的方法,都是错的,各种 Bug 层出不穷,按下葫芦浮起瓢。最后我冷静下来,深究原因——保存与存取,总是出现问题。我得感谢之前开发的经验里,让我牢牢记住一个原则:一个 Bug 如果修改 3 次都不成功,一定要转换思路,不要继续重复原来的方法。
所以,我和 AI 对话,对实际的业务情景进行分析,寻找最适合的数据库结构。这个问题决定了数据库的读写方法——
我: overview-id 一定对应一个 ref-id (必需关系) 也未必会有对应的一个 ref-id,因为有的 overview 文件不是从 ref 源文件夹中的文件创建。不过,从严格意义上来讲,一个 overview 文件,必须对应有一个 source 文件
AI: 明白了,让我重新梳理这个关系: 从数据库角度: ref-id 可以没有对应的 overview-id overview-id 可以没有对应的 ref-id 但如果存在映射,则是一对一的关系 从业务逻辑角度: overview 文件必须对应一个 source 文件(业务规则) 但这个对应关系可能还未在数据库中建立(数据状态) 映射关系的建立是一个渐进的过程 Map 表的职责: 记录已建立的映射关系 确保映射的一对一性 不强制要求所有实体都必须有映射 但提供检查机制发现"孤立"的 overview 这样的理解更准确了吗?
我: 在我们的场景里,一个 ref-id 对应一个 overview-id。
AI: 啊,抱歉我理解错了。是的,在我们的场景中是一对一的关系。 让我们重新分析 Map 表的工作机制: 一对一映射的特点: 每个 ref-id 只能对应一个 overview-id 每个 overview-id 也只能对应一个 ref-id 这意味着映射是双向唯一的 Map 表的约束: 创建新映射前需要检查 ref-id 是否已有映射 创建新映射前需要检查 overview-id 是否已被映射 更新映射时需要保证双向唯一性 删除映射时需要同时解除双向关联 查询优化: 可以通过 ref-id 直接找到对应的 overview-id 可以通过 overview-id 反查对应的 ref-id 不需要复杂的多重映射处理 这样的理解更准确吗
我: 场景是这样的,新增加的 ref-id 未必会有对应的 overview-id。
AI: 啊,明白了。这是一个可选的一对一关系:
- 映射的特点:
- ref-id 可以没有对应的 overview-id (可选关系)
- 但如果有映射,则是一对一的
- overview-id 一定对应一个 ref-id (必需关系)
- Map 表的约束:
- 不是所有 ref-id 都需要在 Map 表中
- 创建映射时只需检查 overview-id 是否已被使用
- overview-id 不能重复使用
- ref-id 已有映射时,新映射会替换旧映射
- 业务场景:
- 新建 ref 时可以不立即创建 overview
- 可以后续再为 ref 关联 overview
- 可以更换 ref 对应的 overview
- overview 必须关联到某个 ref
这样的理解更符合实际场景了吗?
我: overview-id 一定对应一个 ref-id (必需关系) 也未必会有对应的一个 ref-id,因为有的 overview 文件不是从 ref 源文件夹中的文件创建。不过,从严格意义上来讲,一个 overview 文件,必须对应有一个 source 文件
AI: 明白了,让我重新梳理这个关系:
- 从数据库角度:
- ref-id 可以没有对应的 overview-id
- overview-id 可以没有对应的 ref-id
- 但如果存在映射,则是一对一的关系
- 从业务逻辑角度:
- overview 文件必须对应一个 source 文件(业务规则)
- 但这个对应关系可能还未在数据库中建立(数据状态)
- 映射关系的建立是一个渐进的过程
- Map 表的职责:
- 记录已建立的映射关系
- 确保映射的一对一性
- 不强制要求所有实体都必须有映射
- 但提供检查机制发现"孤立"的 overview
经过几轮对话之后,数据库所面对的场景,才真正的抽象出来。
实际开发时通过测试用例来 Debug
由于数据库设计的操作复杂,又是读又是取,又是加载又是保存,加上需要从数据库中读取数据,保存数据的函数很多。因此,按照以往,直接修改函数很容易遗漏。
最佳实践是,通过测试用例,来找到还没修改和完善的地方。Emacs 自带 ert 这个测试框架,可以很完善的测试各种功能。有了测试用例配合测试,就可以非常清楚地知道问题出在哪里,直接检查对应的函数。节省了大量的时间,和精力——在 Debug 过程中,最痛苦的不是改 Bug 而是找到 Bug 的源头。