309
社区成员
发帖
与我相关
我的任务
分享本单元遵循的工作流程:
明确需求 → 设计(UML)→ 实现 → 对齐修正
第一阶段,我们根据需求用UML画出图书馆系统的设计图。这迫使我们在写任何代码之前,就要想清楚系统中有哪些实体、各自负责什么、边界在哪里。
确认好设计图之后,才进入实现阶段。第二阶段,我们测试并修复代码,再回头更新设计图,使其与实际实现保持一致。
一阶图和二阶图之间的差距并不是失败。它是最真实的记录,记录了我们最初的假设在哪些地方出了问题。
两者都要求在实现之前做出决策和规划,但关注的层面不同。
| 正向建模 | 规格先行 | |
|---|---|---|
| 关注点 | 结构——系统中存在什么、各部分如何关联 | 行为——每个方法必须做什么 |
| 产出物 | UML类图/状态图 | JML契约 |
| 分析层面 | 系统层面 | 方法层面 |
| 对应单元 | 第四单元 | 第三单元 |
可以把它们理解为同一种"设计先行"理念的两个层次:正向建模搭出骨架,规格先行为每个方法定义契约。两者合在一起,才能在写代码之前覆盖完整的设计过程。
这让我们小小体验了一把真实商业工程的工作方式。
大型科技企业中有一个特殊岗位叫做前沿部署工程师(FDE)——他们处于工程团队和客户之间,负责在真实客户环境中部署和定制产品。这要求他们既要深入了解产品的实现方式,也要能坦诚地告诉客户什么可以做、什么不行,并在正式开发前向客户展示可行的产品模型。
初始模型与最终交付物之间的差距越小 → 返工越少、对其他开发者的指导越准确、客户越满意。
我们的两阶类图正是这个道理。一阶图是"提案"——在实现之前就锁定了设计方向。二阶图是诚实的修正——记录了我们实际建出来的东西。
整个系统通过三次作业迭代完成,每次在已有基础上叠加新需求:
| 版本 | 新增功能 | 架构影响 |
|---|---|---|
| HW13 | 基本借还、预约功能 | 建立基础架构 |
| HW14 | 阅览室、精品书架、评分 | 引入 ReadingRoom、TreasuredBookshelf、IBookshelf 接口 |
| HW15 | 信用分、借阅期限、续订 | User 新增 creditScore,BookCopy 新增 dueDate,新增 renewed 处理器 |
最终架构以 LibraryManager 为主控制器,持有所有地点对象并分发所有请求:

关键追踪关系——代码与一阶图的主要偏差:
Library 最初设计为直接处理请求,实现后发现它变成了一个上帝类,于是引入 LibraryManager 作为控制器,Library 退化为纯数据目录。IBookshelf 接口在一阶图中并不存在。实现 TreasuredBookshelf 时发现两个书架的操作完全相同,于是提取出这个接口。arrange() 方法最初是一整块。实现过程中逻辑过于密集,不得不拆分为 sweepReadingRoom()、expireOrders()、moveBroToShelves() 和 redistributeShelves() 四个方法。📌 [此处插入UML类图、状态图和顺序图]
在前几个单元中一直依赖大模型直接给出解决方案,后来开始觉得这样不太对劲。于是在HW13中,我尝试了一种不同的方式——自己完成一阶图设计,只用大模型来压力测试它。
📌 [此处插入草稿设计图]
我把设计上传给Claude,让它来挑战我的方案。而当我提问时,它不直接回答,而是把问题抛回来:
我问:
关于请求管理,哪种方式更安全、性能更好?
- 在每个地点存储待移动书籍的请求
- 根据位置状态(AO/BRO/User/Bookshelf)搜索图书
Claude回:
你问的是:在每个地点存储移动请求,还是按位置状态搜索?
从两个角度思考:
- 整理操作发生得频繁吗?
- 图书总共有多少本?最多100个ISBN × 10个副本 = 1000本
在这个规模下,性能真的重要吗?
哪种方式在编写整理逻辑时更清晰?
经过多轮问答,修改了5稿左右,一阶图终于定稿。然后开始写代码——然后发现还是有很多地方需要改。😔
技术可行性分数不太好看,但这次实验让我学到了更有价值的东西:被迫为每一个设计决策进行辩护,让我真正理解了系统本身——实体是什么、各自负责什么、边界在哪。我也发现了自己的一个习惯:总是倾向于把设计压缩到最简,图上看起来整洁,但实现时方法逻辑就会过载。arrange() 就是最典型的例子——图上是一个整齐的方法,代码里最终拆成了四个。
核心结论:用大模型挑战你已有的设计,而不是让它替你生成一个。
让Claude从需求出发直接生成架构,结果看起来合理——但我没办法解释其中任何一个决策,因为根本不是我思考出来的。这就是陷阱所在。
真正有效的方式是反过来。三个实用模式:
① 一起走一遍场景
描述一个具体流程——比如从预约到取书的完整流程——让Claude逐步走过你的类设计,指出哪里职责不清或有遗漏。
② 问"各自让什么变难了",而不是"哪个更好"
"哪个设计更好?"只会给你一个答案。"每个设计让什么变得更难?"才能给你权衡分析。最终的决定还是你来做。
③ 问哪里实现起来会很痛
"这个设计哪部分最难写?"这个问题出奇地准。在HW13中,它准确预判了单一 arrange() 方法会失控——后来确实发生了。
问题不在于大模型的答案是否正确,而在于直接拿到答案,跳过了正向建模本来要你完成的思考过程。
每个单元都在问同一个问题,只是答案一直在变:类到底是什么?
第一单元 → "谁用谁?" (类是数据容器)
第二单元 → "谁用谁?什么时候用?" (类是需要协调的参与者)
第三单元 → "谁用谁?怎么用?" (类具有行为契约)
第四单元 → "这个东西是什么?" (类有明确的身份和职责)
第一单元里,类基本上就是一个有名字的数据盒子。设计就是决定哪个盒子访问哪个盒子。
第二单元加入了时间维度。有了线程,光知道谁访问谁不够——还要考虑什么时候访问,以及是否线程安全。类开始感觉像是需要相互同步的参与者,而不只是容器。
第三单元转向了方式。JML要求你精确说明一个方法做什么——进入时的前置条件,返回时的后置条件。一开始觉得很烦,但它让测试清晰了很多。有规格,就有测试。
第四单元在最前面加了一个新问题:这个东西是什么? 在写任何代码之前,就要在图上承诺每个类的身份和职责。回头看,四个单元衔接得相当自然——静态结构 → 时序协调 → 方法契约 → 系统级身份与职责。
第一单元:运行 → 看输出 → 往回追
第二单元:打日志 → 看事件顺序 → 往回追
第三单元:写规格 → 写测试 → 在运行前预判bug
第四单元:过滤 → 隔离单个实体的时间线 → 找偏差点
第一单元很直接:运行,看输出,找bug。在一切都是确定性的情况下,这完全够用。
第二单元把这套方法打破了。有了线程,同样的输入在不同的运行中会产生不同的结果。唯一稳定有效的方法是用 System.err.println 实时追踪发生了什么。我不再问"出了什么错",开始问"什么时候出错、以什么顺序出错"。在多线程代码里,不打日志就是在瞎猜。
第三单元引入了提前捕获bug的思路。从JML规格出发写测试,意味着在运行任何东西之前就要把边界情况想清楚。有一点值得注意:规格是下限,不是上限。你的实现可能在规格完全没提到的地方出bug,所以测试要比规格覆盖得更广。
第四单元是最棘手的。因为交互式评测机根据你的输出动态生成请求,修一个bug就会改变整条执行路径——原来的bug就没办法复现了。有效的方法是过滤:只针对一本书或一个用户打印 System.err 输出,把他们的完整历史当作时间线来读,找到时间线在哪里偏离了规格要求。
第一单元——一个基本习惯开始形成:问"这件事应该由谁负责?"设计不好看,但问题问对了。
第二单元——正确性不只是得到正确的值,还关乎事情以正确的顺序在正确的时间发生。并发bug让人清醒:它们只在两件事在恰好错误的时刻碰撞时才会出现,光静态阅读代码是看不出来的。
第三单元——意料之外的自信。写JML、从规格推导测试,让我意识到自己可以独立地严格推理方法的行为,不需要工具来替我判断。不管我多么希望自己的设计完美,规格才是最终的裁判,实现必须满足它。这是一种更踏实的思考方式。
第四单元——设计成了核心。一阶图和二阶图之间的差距,直接反映了我在开始写代码之前对问题理解得有多深。这个差距永远不会归零,目标只是每次让它小一点。
四个单元走下来,轨迹是这样的:
静态结构 → 动态协调 → 方法设计 → 系统设计
回头看是一条很清晰的路——尽管在截止日期前的凌晨两点,完全感觉不到。
说真的,这门课是我见过的对拖延症最有效的治疗方案。