
1. 项目缘起当“我的Notion”遇上工程化焦虑如果你和我一样是一个重度依赖Notion来管理个人知识、项目甚至生活的用户那么“打造一个更符合自己习惯的Notion”这个念头可能时不时就会冒出来。市面上的Notion虽然强大但总有些地方感觉“差那么一点意思”——也许是某个视图的展示逻辑不够顺手也许是某个自动化流程无法实现又或者你只是想在一个完全可控的环境里折腾点属于自己的小功能。于是“My-Notion”这个想法就诞生了一个可以自己动手从零开始按照个人意志去塑造的、轻量级的类Notion应用。想法很美好但动手的过程却常常陷入混乱。一开始你可能和我一样兴致勃勃地打开编辑器脑子里充满了各种炫酷的功能点双链笔记、看板视图、日历集成、AI辅助写作……然后你开始写第一行代码。很快你会发现需求在变今天想加这个明天觉得那个更重要代码结构在失控各个模块像藤蔓一样纠缠在一起测试更是无从下手每改一点东西都心惊胆战生怕哪里就崩了。项目进度缓慢成就感被挫败感取代最终“My-Notion”很可能就变成了硬盘里又一个“烂尾工程”。这正是我启动这个项目时面临的困境。我意识到对于这样一个兼具复杂业务逻辑笔记、视图、关系和个人化定制需求的“玩具级”但“五脏俱全”的项目传统的、想到哪写到哪的“游击队”式开发是行不通的。我需要一套系统性的工程方法论来约束我的想法规范我的开发流程并最终驱动项目稳步前进。这套方法论就是我在实践中融合并演进出的PDD、DDD、SDD 到 TDD 的“四步驱动法”而贯穿其中的灵魂则是一个我称之为“工程Agent”的思维模型。2. 方法论基石拆解PDD、DDD、SDD与TDD在深入我的实践之前有必要先厘清这几种常常被提及但又容易混淆或流于形式的方法论。它们在我的流程中并非并列关系而是一个环环相扣、递进驱动的链条。2.1 PDD问题驱动开发——一切的原点PDD即 Problem-Driven Development。这是最容易被忽略却也是最重要的一步。它回答的是“我们为什么要写代码”这个根本问题。在个人项目中这个问题常常被“我想做个XX功能”的冲动所替代。但“功能”不等于“问题”。在My-Notion项目中我强制自己为每一个要添加的模块或特性先写下一份“问题陈述”。例如不是“我要加一个任务看板”而是问题在管理个人项目时我无法直观地看到所有任务的当前状态待办、进行中、已完成也无法通过拖拽快速更新任务进度导致项目跟进效率低下。受影响方我自己项目管理者。现状目前使用纯文本列表记录任务手动修改状态容易遗漏且不直观。期望提供一个可视化的看板界面任务以卡片形式存在于不同状态列中支持拖拽移动状态变更自动保存。PDD文档通常很短但它像锚一样牢牢锁定了开发的初衷。当我在后续开发中陷入细节或想添加“炫技”功能时我会回头审视这份PDD这个改动是否真正解决了最初定义的问题这能有效避免范围蔓延和过度设计。2.2 DDD领域驱动设计——构建核心“大脑”明确了要解决的问题接下来就要构建解决问题的“大脑”即核心业务逻辑。这里我引入了DDD。对于个人项目完整的DDD战术建模实体、值对象、聚合根、仓库、领域服务等可能显得繁重但我取其精髓统一语言和领域模型分离。统一语言我和我的“代码”必须说同一种语言。在My-Notion的代码库、文档、甚至提交信息里我严格使用“文档”、“块”、“数据库”、“视图”、“过滤器”这些业务术语而不是“那个JSON对象”、“这个UI组件”。这保证了从问题到代码的一致性。领域模型分离这是DDD对我帮助最大的地方。我明确地将代码分为至少两层领域层包含Document文档、Block内容块如文本、标题、待办列表、Database数据库用于管理一组文档的元数据、View视图如列表、看板、日历等核心类。这些类只关心业务规则和数据状态完全不依赖任何UI框架、数据库驱动或网络库。它们就是My-Notion这个领域的“纯净”模型。应用/基础设施层负责将领域模型持久化如用IndexedDB存储、渲染到UI如用React组件、处理用户交互。这一层依赖于领域层。通过这种分离我的核心业务逻辑变得极其稳定且可测试。例如我可以单独写一段脚本测试Document和Block之间的嵌套关系是否正确完全不需要启动浏览器或构建UI。这为后续的TDD打下了坚实基础。2.3 SDD方案驱动设计——勾勒实现蓝图有了清晰的领域模型DDD的输出我们就知道了“要处理什么”。接下来SDD负责回答“具体怎么做”。SDD即 Solution-Driven Design 或 Scenario-Driven Design我更倾向于前者。它是在动手编码前针对PDD中提出的具体问题结合已定义的领域模型设计出具体的实现方案。这个过程不是写详细设计文档而是进行“思维实现”和关键决策。例如针对“看板视图”的PDD和已有的Database、View、Document模型我的SDD思考包括数据层面看板视图KanbanView作为View的一个子类其核心配置是“状态属性名”如status。它如何根据文档的该属性值对文档进行分组交互层面拖拽完成后是直接更新领域模型Document的属性然后由领域事件触发保存还是通过一个应用层的“拖拽服务”来协调UI层面使用哪个拖拽库react-dnd还是dnd-kit各自的优劣是什么如何将其封装使其不污染领域逻辑我会为每个稍复杂的特性用草图工具或简单的Markdown列表勾勒出这些关键决策点。SDD的输出是一系列清晰的技术选型和架构决策它确保了从领域模型到具体实现的路径是清晰的避免了边写边想的混乱。2.4 TDD测试驱动开发——用反馈循环构建信心最后也是将一切落地的实践保障TDD。在PDD、DDD、SDD的铺垫下TDD变得异常顺畅和有力。我不再是为“不知道能不能跑通”的代码补测试而是在明确的问题、清晰的领域模型和具体的技术方案指导下进行“红-绿-重构”的循环。具体操作流程根据SDD编写一个最小化的失败测试例如针对KanbanView我先写测试“给定一个包含status属性的文档列表当创建KanbanView并指定分组属性为status时应该返回按Todo,Doing,Done分组的文档映射。” 运行测试它肯定是红的失败因为KanbanView类甚至还不存在。编写最少代码让测试变绿创建KanbanView类实现一个最简单的分组逻辑只求通过当前测试。这个过程强迫我思考类的方法签名和职责是从使用者的角度驱动设计。重构在测试保护下放心地优化代码结构消除重复应用设计模式。由于有领域层的隔离重构通常只发生在相对独立的模块内风险很低。TDD在这里扮演了“即时验证者”和“设计反馈器”的角色。它确保我每一步的代码都紧扣PDD定义的需求符合DDD划分的领域边界并实现了SDD规划的技术方案。更重要的是它为我构建了一个安全网让我敢于持续重构和演进代码项目不会因为复杂度增加而腐化。3. “工程Agent”思维串联方法的粘合剂你可能注意到了从PDD到TDD每一步都需要高度的自律和上下文切换。如何保证自己在编码的狂热中不跳过PDD如何在设计SDD时不忘DDD的模型约束这时我引入了“工程Agent”的思维模型。这个“Agent”不是指一个AI代码助手而是内化在我开发流程中的一个虚拟的、严格执行规则的“代理”。你可以把它想象成一个严格的代码审查机器人或者你内心那个“工程化”的人格。这个Agent负责监督整个流程的纪律当我想直接写代码时Agent会提醒“PDD文档写了吗问题定义清楚了吗”当我在设计SDD试图让一个UI组件既负责渲染又负责复杂业务计算时Agent会警告“这违反了DDD的分离原则业务逻辑应放入领域模型。”当我在实现一个功能时Agent会质问“对应的测试用例TDD先写了吗”在提交代码前Agent会检查本次提交是否关联了一个清晰的PDD问题代码变更是否在领域层和应用层有清晰的边界测试覆盖率是否达标在实践中我通过一些简单的工具来具象化这个AgentIssue模板在GitHub/GitLab的Issue中我创建了PDD模板强制填写问题、现状、期望。分支命名规范分支名格式为pdd/[issue-id]-short-description例如pdd/123-add-kanban-view从源头关联问题。提交信息规范使用Conventional Commits并在信息中引用Issue ID如feat(view): implement kanban view drag-drop (closes #123)。CI/CD流水线配置自动化流程在合并请求时必须通过所有单元测试领域层和集成测试应用层。通过这套“仪式感”和自动化检查我将“工程Agent”思维固化到了工作流中使得PDD-DDD-SDD-TDD不再是一套空谈的理论而是可执行、可检查的日常习惯。4. My-Notion实战看板视图从想法到落地让我们以My-Notion中“看板视图”这个具体特性为例完整走一遍这个流程。4.1 阶段一PDD锚定问题与价值我创建了一个Issue标题为“作为用户我希望以看板形式可视化和管理我的任务状态”。在PDD模板中我详细描述了前文提到的痛点、现状和期望。关键产出一个明确、无歧义的需求定义以及“完成”的验收标准AC支持按属性分组、卡片拖拽、状态实时保存。4.2 阶段二DDD巩固领域模型在动手前我回顾了现有的领域模型。Document拥有动态的properties属性键值对。View是一个抽象基类用于定义如何“查看”一组文档。我意识到看板是一种特殊的、基于某个属性进行分组和排序的视图。关键决策创建KanbanView类继承自View。它的核心是groupByProperty: string用于分组的属性键名。sortOrder: ‘asc’ | ‘desc’组内排序规则。一个核心方法groupDocuments(docs: Document[]): Mapstring, Document[]纯粹根据文档属性和自身配置进行逻辑分组不涉及任何UI。这个设计严格遵循了领域层的纯洁性输入是文档和配置输出是分组数据没有任何副作用。4.3 阶段三SDD设计技术实现方案现在我需要设计如何将这个领域模型与React前端和持久化层连接。状态管理使用Zustand。创建一个useKanbanStore它内部持有KanbanView实例和当前数据库的文档列表。Store提供groups计算属性调用view.groupDocuments和moveDocument调用领域方法更新文档属性并触发持久化等方法。UI组件KanbanBoard主组件从Store读取groups渲染多个KanbanColumn。KanbanColumn表示一个状态列渲染其中的KanbanCard。KanbanCard表示单个任务文档。拖拽库选型对比后选择dnd-kit因其更现代、性能更好、与React 18兼容性更佳。SDD中明确拖拽逻辑在UI层拖拽结束的回调函数onDragEnd中调用Store的moveDocument方法从而更新领域模型。持久化moveDocument动作会更新Document的properties并发布一个领域事件如DocumentUpdated。应用层的事件处理器会监听到此事件并调用Repository层将其保存到IndexedDB。这份SDD方案让我在写代码前就对模块划分、数据流和技术栈有了清晰蓝图。4.4 阶段四TDD驱动具体实现现在进入编码阶段严格遵循TDD循环。第一步测试领域模型。我先为KanbanView.groupDocuments方法写测试。// KanbanView.test.ts describe(‘KanbanView’, () { it(‘should group documents by specified property’, () { const docs [ new Document(‘1’, { status: ‘Todo’, title: ‘Task A’ }), new Document(‘2’, { status: ‘Doing’, title: ‘Task B’ }), new Document(‘3’, { status: ‘Todo’, title: ‘Task C’ }), ]; const view new KanbanView({ groupByProperty: ‘status’ }); const groups view.groupDocuments(docs); expect(groups.get(‘Todo’)).toHaveLength(2); expect(groups.get(‘Doing’)).toHaveLength(1); expect(groups.get(‘Done’)).toBeUndefined(); }); });运行测试红- 实现最简单的KanbanView类绿- 重构可能将分组逻辑提取到私有方法。第二步测试应用层Store。为useKanbanStore的groups派生状态和moveDocument动作写测试。这里需要模拟领域层和Repository层。第三步测试UI交互集成测试。使用Vitest Testing Library测试拖拽一个卡片后对应的DOM更新和Store方法是否被以正确的参数调用。每一步都在测试的保护下进行。当所有测试通过我知道这个看板功能不仅能用而且其核心逻辑领域层是坚固、可独立验证的。5. 避坑指南与心得反思这套方法并非银弹在实践中我也踩过不少坑总结出以下几点心得5.1 PDD阶段最容易犯的错问题定义过于宽泛或解决方案前置错误示例“我需要一个像Trello一样的看板。”这是解决方案不是问题正确做法深挖背后的真实痛点。“我需要可视化任务状态并快速更新”这才是问题。明确的问题定义能防止后续设计走偏。5.2 DDD在小型项目中的“度”的把握对于My-Notion这样的个人项目严格遵循聚合根、领域服务等所有模式是杀鸡用牛刀。我的经验是至少做到“领域模型与基础设施分离”。只要保证核心业务类不导入任何UI、网络、数据库相关的包你就已经获得了80%的DDD收益可测试性、可维护性。过度设计反而会拖慢个人项目的迭代速度。5.3 SDD选型时的“未来债”与“现在债”在选型拖拽库、状态管理库时容易陷入“哪个最流行、功能最全”的陷阱。我的原则是优先选择符合项目长期架构理念如模块化、响应式且API简洁的库。dnd-kit的模块化设计就比某个大而全但封装死的库更符合我的架构哲学。同时要为可能的更换留好抽象层例如将拖拽逻辑封装在一个自定义Hook里未来换库只需改这个Hook的内部实现。5.4 TDD的节奏感何时写测试写多细TDD不是宗教。对于极其简单、一眼就能看透的增删改查CRUD方法有时可以先写代码再补测试效率更高。但对于核心业务逻辑、算法、状态转换如看板分组、文档树形结构处理必须坚持先写测试。测试的粒度以“行为”为单位而不是“方法”。例如测试“移动文档到另一列”这个行为它会涉及Store动作、领域模型更新、可能的事件发布这是一个完整的集成测试场景比单独测试某个私有方法更有价值。5.5 “工程Agent”的自动化落地手动检查纪律很难持久。务必利用好现代工具链Git Hooks使用husky设置pre-commit钩子运行lint和单元测试领域层。CI Pipeline在GitHub Actions中设置流水线在每次推送时运行完整的测试套件包括集成测试并在合并请求时要求必须通过。代码模板在IDE中为领域实体、值对象等创建代码片段模板减少重复劳动并引导正确结构。6. 方法论的演进与适用边界这套“PDD - DDD - SDD - TDD”的流程配合“工程Agent”的监督思维是我在My-Notion项目中逐步磨合出来的。它并不是一成不变的教条。对于探索性项目前期可能更侧重PDD和轻量级的SDD快速原型DDD和TDD的比例可以降低先验证想法。对于业务逻辑复杂的核心模块必须严格执行完整的流程尤其是DDD和TDD这是保证长期代码健康的基石。对于纯UI/交互类功能DDD的权重可以降低但PDD定义交互问题和TDD组件测试依然重要。归根结底这套方法论的目的是对抗熵增让个人项目也能像正规军一样在清晰的愿景、稳固的架构和快速的反馈循环中稳步推进。它让我在享受创造乐趣的同时不再恐惧于代码的复杂度。My-Notion项目也因此从一个随时可能夭折的玩具成长为一个结构清晰、功能持续增长、我真正愿意长期维护和使用的个人工具。如果你也在为个人项目的混乱而苦恼不妨尝试引入这套“组合拳”或许它能为你带来同样的秩序与信心。