309
社区成员
发帖
与我相关
我的任务
分享UML 是软件工程领域的标准建模语言,类图是其中最核心的工具。说白了,它是一张写代码之前的设计蓝图,根据我们的想法绘制,然后指导我们撰写代码。
在第四单元之前,我写 OO 作业都是把指导书看完,直接写代码,把构思的任务交给 AI。这就导致我之前写代码的时候,总有一种云里雾里、当局者迷的感觉——我不了解整个框架,不知道自己代码完成到什么程度,直到代码能跑通,我才知道"哦,我写完了"。这个单元很好地帮助我在动手写代码之前先进行构思,而且我发现这样做反而更加迅速高效。
正向建模的核心价值在于:先想清楚再动手。类图逼着你在编码前回答三个问题:系统里有哪些核心对象?每个对象承担什么职责?对象之间如何协作?这三个问题想清楚了,代码实现就变成了按图施工。
一阶类图是在编码前的架构设计阶段完成的。它的核心作用是清晰表达系统的核心职责划分和整体结构,而不是对代码中所有属性与方法进行事无巨细的抄录。
我画一阶类图时遵循的原则是:
BorrowAndReturnOffice 做类名,而不是 Office;用 returnBook 做方法名,而不是 handle。OO加油站公众号提到的"关键词覆盖率"指标,本质上就是检测类和方法命名是否过于空泛。二阶类图是在代码完成过程中对一阶类图进行修正后的实现版本。它必须精确匹配代码:类名、属性名、类型、可见性、方法签名、返回参数、关联关系,一个都不能错。
从一阶到二阶的演进过程中,我的一阶图里 Library 只有几个核心方法,到了二阶图,我增加了 findOrAddUser 这个私有方法——因为编码时发现需要频繁根据学号查找或创建用户,提取出来可以复用。另外,我在 Library 中直接建了一个 copyMap 属性,把所有的 BookCopy 按 ID 存起来,这样查询轨迹时可以直接 O(1) 定位,不需要在每个地点类里逐个寻找。这些调整都体现了设计随编码而演进的自然过程。
两阶类图的设计体现了正向建模的核心思想:设计是迭代的。一阶类图允许抽象,方法参数可以简略,不画私有成员;二阶类图则必须精确,与代码严格一致。这种从抽象到具体的演进,让我深刻理解了"设计先行"的价值——不是追求一次完美,而是在设计→编码→修正的循环中不断完善。
图书馆系统的核心是管理图书副本的生命周期。我采用的架构是中心管理 + 地点存储模式:
LibraryManager:总控类,持有所有地点和学生数据,处理所有命令BookCopy:图书副本实体,记录状态、轨迹、预约/借阅信息BookShelf(书架)、BorrowAndReturnOffice(借还处)、AppointmentOffice(预约处)、ReadingRoom(阅览室)Student:用户管理,记录借阅、预约、阅读状态和信用分三个地点的数据结构选择体现了"由业务操作决定"的原则:
| 地点 | 核心操作 | 数据结构 | 选型理由 |
|---|---|---|---|
| 书架 | 按ISBN快速取任意一本可用副本 | Map<ISBN, Set<BookCopy>> | 借书需要O(1)定位 |
| 借还处 | 整理时批量取出全部 | List<BookCopy> | 只需要全部取出,够用就好 |
| 预约处 | 按学生取书 + 按过期日期批量清理 | Map<学生ID, BookCopy> + TreeSet<BookCopy> | 双索引,两种查询都高效 |
预约处的双索引设计是我比较满意的地方。如果只用 HashMap,每次清理都要遍历所有预约书;如果只用 PriorityQueue,只能取堆顶,无法实现取书时的随机删除。所以用了双索引——TreeSet 按过期日期排序,清理时遇到第一个没过期的就可以 break;HashMap 按学生 ID 取书 O(1)。
这里有一个坑:TreeSet 默认按 compareTo 判断相等,如果只按过期日期比较,同一天过期的多本书会被认为是同一个元素。解决方法是用复合比较器:先按过期日期排,再按 bookId 的字符串排。
在开发过程中,我保持了代码和UML模型之间的追踪关系。具体做法是:
borrowBook、returnBook、arrangeBook)在类图和代码中保持一致BookCopy 的状态转移(书架→用户→借还处→书架)在状态图中明确画出,代码中用 @Trigger 注解标注实际操作中,我的顺序是:先画一阶类图(设计阶段)→ 写代码 → 根据代码修正二阶类图。这样既保证了设计的指导性,又允许编码过程中的灵活调整。
整理流程是图书馆系统中最复杂的部分。我的整理顺序经历了从复杂到简单的演进:
初版设计:在整理时直接从借还处取书送到预约处,减少移动次数。但实现起来比较复杂,需要处理多个来源的书籍。
最终简化:观察规则后发现"终点为预约处的移动不计入次数限制",于是干脆先把借还处和过期书籍全部清空放回书架,然后统一从书架取书满足预约队列。这样做虽然某些书可能被移动了两次(先从借还处到书架,再从书架到预约处),但因为第二次的终点是预约处,不计入次数限制,所以完全合法,而且实现起来简单得多。
开馆前整理需要额外处理精品书架的归类,我采用了只处理今日被评分ISBN的优化策略——用 ratedIsbnToday 记录今天被评分的ISBN,整理时只检查这些ISBN对应的书籍是否需要移动。这样避免了每次整理都遍历所有书,效率更高。
我在本单元中使用大模型辅助了以下场景:
HashMap、TreeSet 还是 List 时,向大模型描述业务场景,获取选型建议在实践中,我发现引导大模型的关键在于提供足够的上下文:
但是,大模型在生成类图时经常会忽略一些细节。例如,我曾让大模型生成 Student 类的设计,它给出了 credit 属性,但忽略了信用分变化时机的具体规则(逾期还书扣15、预约不取扣15、按时还书加10),这些需要在后续对话中补充。
大模型在正向建模中的主要局限是:
BookCopy 中同时保存多个日期(借阅日期、续订日期、应还日期等),但实际上只需一个 borrowedExpireDate 就够了总的来说,大模型适合作为设计助手,给出初步方向和建议,但最终的架构决策和细节调整还需要自己来完成。在第四单元,我实际设计出的 TreeSet 复合比较器和整理顺序的优化,都是在大模型建议基础上结合对业务场景的深入理解得出的。
第一单元我基本是看到需求就直接写代码,没有架构设计的概念。表达式展开的需求虽然不算复杂,但因为没有事先规划,经常出现改一个地方要修改多个类的情况,代码耦合度极高,扩展起来非常痛苦。
第二单元的电梯调度让我第一次意识到模块划分的重要性。我划分了 Elevator(电梯)、Scheduler(调度器)、Request(请求)等模块,但模块间的职责划分仍不够清晰,比如调度器既负责分配电梯又负责控制电梯运行,导致代码臃肿。
第三单元引入了 JML,我学习了规格驱动开发的方法论。这个单元让我养成了"先读规格再写实现"的习惯,也让我理解了接口与实现分离的设计思想。但JML主要针对方法级别的规格,缺少对全局架构设计的指导。
第四单元让我真正建立了"设计先行"的工作流。从一阶类图到二阶类图的演进过程,让我理解了设计不是一次成型,而是通过迭代不断完善的。具体来说:
arrange 没有参数,但编码时需要区分开馆前/闭馆后整理)findOrAddUser 等辅助方法,细化了参数类型这个迭代过程让我理解了两阶类图差异的意义:一阶是蓝图,二阶是施工图,它们可以不同,但要诚实记录变化原因(重构说明)。
第一单元我主要依靠手动测试,在本地跑几个样例,看看输出对不对。这种方式效率极低,而且覆盖不到边界情况。
第二单元我开始尝试随机测试。电梯调度的正确性涉及时间顺序,我编写了随机请求生成器,对比不同调度策略的输出,但由于多线程的复杂性,测试结果难以判断是否"正确"。
第三单元的JML规格让测试变得有据可依。我写了大量的JUnit测试用例,覆盖正常路径和异常路径。在测试 pure 方法时遇到了浅拷贝问题,采用了双 Network 方案解决。我还在测试中使用了随机测试,固定随机种子来保证可复现性。
第四单元的评测方式是交互式的——只给出借阅/预约/阅读/查询/评分请求,而还书/取书/归还/续订请求由评测机根据输出动态生成。这要求我的程序保持状态一致性,任何错误状态都会在后续请求中被放大。
我测试的重点放在了:
通过四个单元的学习,我最大的收获是理解了"设计先行"的价值。从第一单元的"写完再说"到第四单元的"先画图再编码",我的工作方式发生了根本性的变化。虽然画类图看起来是额外的工作量,但实际上它让编码阶段更加高效——因为核心决策已经在设计阶段做完了。
这个学期让我明白,设计不是一次完成的,而是在编码过程中不断演进的。一阶类图和二阶类图的差异不是错误,而是迭代的自然结果。重要的是要诚实记录变化原因,让设计文档反映真实的代码状态。