2026 OO 课程技术博客:从规格驱动到正向建模的架构演进总结

贺严24370403 2026-06-18 19:34:59

2026 OO 课程技术博客:从规格驱动到正向建模的架构演进总结

本文围绕 2026 年面向对象课程第三、第四单元,以及四个单元整体学习过程展开总结。第三单元重点讨论 JML、规格驱动开发、JUnit 测试、三次作业迭代和性能问题;第四单元重点讨论图书馆管理系统中的正向建模、两阶类图、UML 与代码的追踪关系,以及大模型辅助复杂架构设计的经验。最后总结自己在四个单元中架构设计思维、测试思维和课程认知的变化。


一、从代码驱动到规格/模型驱动:课程后半段的核心转变

在 OO 课程的前两个单元中,我的主要工作方式仍然是“先把程序写出来,再通过样例和互测修补问题”。第一单元更多考察表达式对象的抽象、递归结构和化简逻辑;第二单元则把重点转向多线程电梯调度、生产者—消费者模式、同步控制和状态一致性。进入第三、第四单元后,课程重点发生了明显变化:

  • 第三单元要求我先阅读 JML 规格,再根据规格实现程序,核心是“规格先于实现”;
  • 第四单元要求我先进行 UML 正向建模,再依据模型完成图书馆系统开发,核心是“模型先于代码”;
  • 整体上,开发方式从“面向功能补丁”转向“面向契约、职责和可追踪结构”。

这也是我对 OO 课程后半段最大的理解:一个复杂系统不能只靠临时判断和局部修补维持正确性,必须有更稳定的上层约束。第三单元中的 JML 是行为层面的约束,第四单元中的 UML 类图和顺序图则是结构层面与交互层面的约束。前者告诉我“方法应该满足什么条件”,后者告诉我“系统中应该有哪些对象、对象之间如何协作”。


二、Unit3:对 JML 和规格驱动开发的理解

2.1 JML 的本质:把隐含需求变成显式契约

JML 最大的价值在于把自然语言中容易含混的需求转化为接近形式化的契约。对一个方法来说,JML 通常通过 requires 描述前置条件,通过 ensures 描述后置条件,通过 assignable 约束可修改范围,通过 signals 描述异常行为,通过不变量描述对象长期保持的性质。

我在第三单元中逐渐形成了一个理解:JML 不是普通注释,而是“客户—实现者之间的契约”。调用者只要满足前置条件,就有权期望实现者满足后置条件;实现者只要正确处理规格允许的状态,就不需要对规格之外的调用负责。这样一来,程序正确性的讨论就不再停留在“我觉得这样写应该对”,而是可以回到“该方法是否满足规格所描述的行为集合”。

在规格驱动开发中,我认为最关键的不是机械翻译 JML,而是理解 JML 背后的抽象状态。例如社交网络类作业中,PersonRelationTagMessage 等对象的具体容器可以不同,但外部可观察行为必须符合规格。也就是说,JML 关注的是抽象语义,而不是规定一定使用 ArrayListHashMap 还是并查集。实现者需要在满足语义的前提下自行选择合适的数据结构。

2.2 规格驱动开发中的三个层次

我把第三单元的开发过程理解为三个层次:

层次核心问题我的体会
规格阅读层这个方法到底承诺了什么?不能只看方法名,要看前置、后置、异常和可修改范围
抽象建模层哪些状态必须被程序维护?需要找到对象之间的真实关系,例如人、关系、消息、标签之间的映射
实现优化层怎样在不破坏规格的情况下提高效率?规格通常不限制容器,但数据规模会倒逼容器和算法升级

最初我容易把 JML 当成“高级版题面”,只关注输出对不对。后来我意识到 JML 更像是程序行为的边界定义。比如一个查询方法如果没有修改对象状态,那么 assignable \nothing 就是非常重要的约束;一个涉及旧状态和新状态比较的方法,则必须认真区分 \old(...) 和执行后的状态,否则很容易把“执行前已经存在”和“执行后新增”混在一起。

2.3 JML 作业中的常见风险

在实践中,我认为 JML 作业最容易出问题的地方主要有四类:

  1. 边界条件遗漏:例如 id 不存在、重复添加、自己与自己建立关系、消息发送双方不合法等情况。如果只根据正常路径写代码,很容易漏掉异常路径。
  2. 容器语义和规格语义不一致:例如 List 中允许重复,但规格中的集合语义可能不允许重复;如果容器选择和抽象语义不一致,就会引入隐蔽错误。
  3. 旧状态和新状态混淆:JML 中的 \old 强调执行前状态,很多后置条件都依赖这种状态差异。如果实现中先修改再判断,就可能破坏逻辑。
  4. 正确但低效:有些方法直接照着量词枚举实现可以通过小样例,但在强测中会因为复杂度过高而超时。

因此,规格驱动开发并不是“照着 JML 翻译代码”这么简单。更准确地说,它要求我们从规格中抽取抽象模型,再设计高效且可维护的实现模型。


三、Unit3:JUnit 测试经验总结

第三单元中,JUnit 对我最大的帮助是把“临时手测”转化为“可重复执行的测试资产”。手动构造输入输出虽然直观,但每次修改程序后都需要重新运行、重新观察结果;JUnit 则可以把这些经验固化下来,使每次重构后都能快速确认基本行为是否被破坏。

3.1 我对 JUnit 测试对象的划分

我在 JUnit 测试中主要关注四类对象:

测试对象典型内容目的
构造与基础状态添加人、添加关系、添加消息、添加标签确认基础容器和初始化逻辑正确
正常业务路径查询关系值、发送消息、计算联通块、修改标签确认主流程符合规格
异常路径重复 id、不存在 id、非法关系、非法消息确认异常类型和触发顺序正确
性能相关路径大量节点、大量关系、集中查询、重复查询发现线性扫描、重复计算等瓶颈

我一开始更重视“样例是否通过”,后来逐渐意识到 JUnit 的价值不只是检查答案,而是为迭代提供安全网。尤其是在第三单元后两次作业中,新增方法会影响已有容器设计。如果没有针对已有方法的回归测试,修改一个容器很可能导致旧功能被破坏。

3.2 JUnit 测试的经验

我的主要经验有以下几点:

  1. 先测规格边界,再测复杂组合。JML 规格中异常分支很多,边界条件比正常路径更容易暴露问题。
  2. 一个测试只验证一个核心行为。如果一个测试同时添加、删除、查询、异常判断全部覆盖,失败后很难定位原因。
  3. 保留回归测试。每次发现 bug 后,都应该把对应场景沉淀为测试用例,防止之后重构再次引入同类问题。
  4. 把性能测试单独组织。功能测试关注正确性,性能测试关注数据规模和复杂度,两者不应混在一起。
  5. 构造对拍模型。对于部分查询功能,可以用一个简单但低效的暴力模型作为 oracle,与正式实现进行随机对拍。

JUnit 让我意识到,测试不是提交前的最后一步,而应该贯穿开发过程。尤其是规格驱动开发中,测试用例应当直接从 JML 中抽取,而不是只从自然语言题面中抽取。


四、Unit3:三次作业迭代、容器变化与性能瓶颈分析

4.1 三次作业的迭代特点

第三单元三次作业的迭代过程可以概括为:

迭代阶段主要变化架构压力
第一次作业建立基本对象与关系查询需要快速建立 id 到对象的映射
第二次作业增加标签、消息、复杂查询关系和消息不再是简单线性结构,容器设计开始影响性能
第三次作业增加更多统计与动态维护需要在修改时维护缓存,而不是查询时全量重算

第一次作业中,如果所有方法都用数组或列表线性扫描,可能仍然能接受;但后续迭代中,查询次数和数据规模上来后,线性扫描会迅速成为瓶颈。因此,第三单元真正考察的不只是“能不能读懂 JML”,还考察“能不能从 JML 中预判未来迭代对数据结构的要求”。

4.2 如何发现已有方法/容器在迭代中的变化

我的方法是把每次新增规格按以下方式重新审视一遍:

  1. 找新增方法访问了哪些旧状态。如果新方法大量访问旧容器,说明旧容器的暴露方式可能不够好。
  2. 找新增方法是否改变旧不变量。例如新增消息或标签功能后,原本只维护人的关系网络可能不够,需要维护消息归属、标签成员关系等额外状态。
  3. 找查询频率高的方法。如果一个方法会在强测中被大量调用,就不能每次都从零遍历全图。
  4. 找新增异常是否依赖旧容器。异常判断顺序往往要求快速定位对象是否存在,因此 id 映射容器非常重要。
  5. 画出状态拥有者。例如某个 Message 到底由网络统一管理,还是由发送者、接收者、标签共同管理,必须明确所有权。

对容器的选择,我逐渐形成了一个原则:凡是通过 id 高频查找的对象,都应该有 HashMap<Integer, Object> 类型的索引;凡是需要动态维护集合关系的结构,都应该避免只用线性表;凡是查询结果可以在更新时维护,就不要在每次查询时从头计算。

4.3 如何发现程序性能瓶颈

我发现性能瓶颈主要依靠三种方式:

  1. 从规格中的量词结构预判复杂度。如果 JML 中存在多重 forallexists,直接翻译通常意味着较高复杂度,需要思考是否可以用缓存或增量维护。
  2. 构造极端数据。例如大量人、稠密关系、重复查询、集中向一个标签添加对象,都可以放大复杂度问题。
  3. 统计关键方法调用次数。如果一个查询方法内部又调用多个线性查询方法,那么表面上是一次查询,实际上可能是多层嵌套遍历。

这让我认识到,规格驱动开发不能忽视性能。规格给出了“做什么”,但没有直接给出“如何高效地做”。真正的实现需要在规格正确性和算法复杂度之间取得平衡。


五、Unit3:JML“击鼓传花”研讨课的感悟

第二次研讨课中的 JML“击鼓传花”给我的触动很大。这个活动表面上是在传递和修改 JML,实际上暴露的是多人协作开发中最典型的问题:每个人都以为自己理解了需求,但每个人理解的边界条件可能并不一样。

5.1 是否发现 JML 的 bug

在传递过程中,我主要意识到以下几类 JML bug 或风险:

  1. 前置条件不完整:例如只规定了对象存在,但没有规定对象之间是否允许相同、是否允许重复关系、是否允许空集合等。
  2. 后置条件不精确:有些规格只描述了目标对象的变化,却没有说明其他对象不应变化,导致实现自由度过大。
  3. 异常条件顺序不明确:当多个异常同时可能触发时,如果规格没有明确优先级,不同实现者可能抛出不同异常。
  4. 自然语言和 JML 不一致:自然语言中说“所有相关对象”,但 JML 中可能只量化了一部分对象,造成语义缺口。
  5. 传递过程中边界被弱化:后一个同学在理解前一个同学的规格时,可能会把隐含约束当成默认常识,从而没有显式写入 JML。

这说明 JML 的难点不只在语法,而在语义精确性。形式化语言能减少歧义,但前提是编写者真的把边界写完整。

5.2 需求和边界是否在传递过程中发生变化

我认为确实发生了变化。变化并不一定表现为“大需求被改掉”,更多时候是细节边界发生漂移。例如:

  • 某个对象不存在时应当抛异常,还是静默不处理;
  • 重复添加时是否允许覆盖;
  • 某个查询是否应该包含自己;
  • 修改一个对象时是否影响相关缓存;
  • 规格中没有写出的对象是否默认保持不变。

这些变化单独看都不大,但在多人协作中会累积成严重的信息差。一个人认为“显然不允许”,另一个人可能认为“规格没禁止就是允许”。最终结果就是同一份需求在不同人手中被实现成不同程序。

5.3 多人组队编程如何减少信息差

如果今后进行多人组队编程,我认为至少要采取以下措施:

  1. 建立统一术语表:对核心概念进行定义,例如“有效关系”“活跃消息”“可借阅书籍”等,避免同词异义。
  2. 维护需求—规格—代码追踪表:每个需求对应哪个 JML 条款、哪个类、哪个方法、哪些测试用例,都应能追踪。
  3. 明确异常优先级:只要存在多个异常同时触发的可能,就必须统一判断顺序。
  4. 规定容器所有权:一个对象由谁创建、谁保存、谁删除、谁负责保持一致,必须写清楚。
  5. 评审规格而不是只评审代码:如果规格本身有漏洞,代码写得再整齐也无法保证系统一致。
  6. 用测试用例固化讨论结果:每次争议都应沉淀为测试,而不是只停留在口头共识。

这次研讨让我认识到,团队协作中最危险的不是“大家不会写代码”,而是“大家以为自己理解的是同一个需求”。


六、Unit4:正向建模与开发的实践总结

第四单元的主题是图书馆管理系统。相比前三个单元,第四单元的特殊之处在于它明确要求先进行 UML 建模,再完成代码实现,并且需要提交预设计类图和最终类图。这使我真正体验到了正向建模的作用。

6.1 正向建模的基本流程

我在本单元中的正向建模过程大致如下:

  1. 阅读需求,识别业务实体:例如图书、书籍副本、用户、预约记录、借还处、预约处、书架等。
  2. 识别系统边界和外部接口:输入输出由课程提供的 LibraryIO 或命令类负责,自己的程序只负责业务状态维护。
  3. 划分类的职责:避免把所有逻辑都堆到 Main 或单一管理类中。
  4. 绘制预设计类图:在编码前明确类、属性、方法和关系。
  5. 根据模型编码:尽量让类名、方法名、依赖方向与类图一致。
  6. 根据实现修正最终类图:将编码中新增的工具类、服务类和字段补回 UML,形成最终可追踪模型。

这个过程让我认识到,UML 不是作业提交的附属品,而是复杂系统开发前的结构草图。尤其在图书馆系统这种状态多、对象多、流程多的任务中,如果没有模型,后续迭代很容易变成局部补丁堆叠。

6.2 两阶类图在正向建模中的作用

本单元的两阶类图可以理解为“预设计类图”和“最终类图”。它们的作用并不相同。

类图阶段主要作用我的理解
预设计类图 uml_pre.mdj在编码前确定对象划分和责任边界重点是方向正确,避免一开始就写成过程式程序
最终类图 uml_ultimate.mdj在编码后反映真实实现结构重点是追踪一致,保证模型能解释代码

预设计类图的价值在于“限制随意编码”。例如在 HW13 中,我的预设计模型已经包含 BookBookCopyBookshelfAppointmentOfficeBorrowAndReturnOfficeReservationRecordUserAccountBorrowLimitCheckerLibraryManager 等核心对象。虽然当时的架构仍然偏集中式,但至少已经把图书副本、用户账户、预约记录和不同馆藏位置分离出来,没有把所有状态都写在一个大数组里。

最终类图的价值在于“反向校验实现”。例如 HW14、HW15 的最终模型中,RequestHandlerLibrarySystemLibraryIoAdapterArrangeServiceMoveServiceReadingRoomTreasuredBookshelfScoreBoardBorrowRecord 等类都应在最终图中体现。否则就会出现模型和代码脱节:代码已经分层了,但 UML 还停留在旧结构;或者 UML 中存在某个类,但代码中并没有对应实现。

因此,两阶类图不是重复画两张图,而是对应两个不同问题:

  • 预设计类图回答:“我准备怎样设计系统?”
  • 最终类图回答:“我的代码最终是否真的按这个结构实现?”

6.3 两阶类图对迭代开发的帮助

第四单元三次作业迭代明显体现了两阶类图的作用。

作业代码规模与类结构架构特点
HW13约 11 个 Java 类,约 616 行核心代码LibraryManager 承担主要调度,已有基础实体类和场所类
HW14约 20 个 Java 类,约 1185 行核心代码引入系统入口、请求处理、移动服务、整理服务、阅览室、热门书架等分层
HW15约 22 个 Java 类,约 1400 行核心代码增加借阅记录、续借、逾期、信用分、信用查询等状态管理

从这个过程可以看出,如果没有预设计模型,HW14 和 HW15 的新增功能很容易直接塞进 LibraryManager,使其变成难以维护的“上帝类”。而有了两阶类图后,我能更清楚地判断:新增功能到底应该成为原有类的新方法,还是应该抽象成新类。

例如:

  • “图书移动”被抽象到 MoveService,避免每个业务方法都手动维护出入库逻辑;
  • “开馆/闭馆整理”被抽象到 ArrangeService,避免整理逻辑和请求处理逻辑混杂;
  • “请求分发”由 RequestHandler 负责,使 LibrarySystem 只负责读取命令和启动流程;
  • “借阅期限和逾期状态”由 BorrowRecord 负责,而不是让 UserBookCopy 单独承担全部语义;
  • “信用分”由 User 保存,由 BorrowLimit 和业务操作共同维护,使信用规则有独立位置。

这些变化都说明,两阶类图帮助我在迭代中维持架构边界,而不是简单地把新增需求补在原来的代码末尾。


七、Unit4 三次作业架构设计总结

7.1 HW13:基础图书馆模型与集中式调度

HW13 是第四单元的第一步,核心功能包括借书、还书、预约、取书、查询移动轨迹、开闭馆整理等。我的初始架构主要由以下类构成:

职责
LibraryManager统一管理图书馆状态和业务流程
Book表示某一 ISBN 的图书
BookCopy表示具体副本,维护位置和移动轨迹
Bookshelf管理在书架上的图书副本
AppointmentOffice管理预约到达的图书
BorrowAndReturnOffice管理归还后的图书
UserAccount管理用户借阅和预约状态
ReservationRecord管理预约生命周期
BorrowLimitChecker检查借阅和预约限制

这一版的优点是对象划分基本成立,图书副本、用户、场所和预约记录都有独立表示。缺点是 LibraryManager 承担了过多职责:请求分发、状态修改、预约过期、图书移动、开闭馆整理都在同一类中完成。这样在第一次作业规模下尚能维护,但一旦功能扩展,就会出现方法过长、职责交叉、修改影响范围过大的问题。

7.2 HW14:从集中式管理到分层服务

HW14 在 HW13 基础上引入了更多功能,例如阅览、归还到阅览室、热门书架、评分等。为了应对复杂度,我的架构从单中心逐步拆分为多层结构:

新增/强化类作用
LibrarySystem系统运行入口,负责读取命令并分派
LibraryIoAdapter适配课程包输入输出,隔离外部接口
RequestHandler按请求类型处理业务流程
ArrangeService处理开馆/闭馆整理
MoveService统一处理图书在不同场所之间的移动
ReadingRoom维护阅览室状态
TreasuredBookshelf维护热门/珍本书架状态
ScoreBoard维护评分与平均分
BookCategory封装 A/B/C 类图书的可借阅、可预约规则
User替代 UserAccount,承担更完整的用户状态管理

这一版的关键进步是把“请求处理”和“馆藏状态管理”分离。LibrarySystem 不直接处理业务,只负责命令循环;RequestHandler 负责不同请求的业务流程;LibraryManager 更像一个领域对象聚合根,提供图书、用户、场所和服务的访问;MoveService 统一处理移动,减少重复代码。

这个阶段我对架构设计的理解发生了变化:一个类不应该因为“它知道所有对象”就负责所有操作。LibraryManager 可以持有系统状态,但具体业务流程应该由更专门的对象完成。

7.3 HW15:信用、续借与逾期状态的加入

HW15 继续扩展功能,引入了信用分查询、续借、逾期扣分、借阅期限等规则。最终代码中新增或强化了以下内容:

类/方法作用
BorrowRecord记录借阅期限、是否逾期、是否已经扣分、续借逻辑
User.refreshOverdueCredit在开馆或操作前刷新逾期扣分
BorrowLimit.addCreditScore对正常还书、正常归还阅览书等行为增加信用分
RequestHandler.queryCreditScore处理信用分查询
RequestHandler.renewBook处理续借请求
BookCopy.borrowDeadline在副本层面维护借阅到期日
BorrowAndReturnOffice.recordOverdueBook记录逾期归还图书

这一版的难点在于“时间状态”和“信用状态”会影响多个业务流程。借书、还书、续借、开馆刷新、信用查询都可能触发或依赖信用分变化。如果把这些逻辑散落在每个方法里,很容易重复扣分或漏扣分。因此,我将借阅期限封装到 BorrowRecord,并通过 markOverduePenaltyApplied 防止重复扣分。

HW15 让我进一步认识到:当一个状态具有生命周期时,它就不应该只是一个字段,而应当被建模为对象。BorrowRecord 的出现正是因为“借阅”不再只是用户和书之间的一条关系,而是具有起始时间、到期时间、续借次数/效果、逾期状态和扣分状态的业务实体。


八、最终代码设计和 UML 模型设计之间的追踪关系

8.1 类级追踪关系

从最终实现看,UML 与代码之间可以建立以下追踪关系:

UML 模型元素代码实现追踪说明
系统入口Main, LibrarySystemUML 中的系统启动对象对应代码的命令循环
请求处理对象RequestHandler类图中的业务控制类对应代码中的请求分发和业务流程
图书馆聚合对象LibraryManager维护图书、用户、场所和服务对象的整体关系
图书实体Book, BookCopy, BookCategory分别对应 ISBN 聚合、副本状态和图书类型规则
用户实体User维护借阅、预约、阅览、信用分等用户状态
预约实体Reservation维护预约状态、预约副本、过期时间
借阅实体BorrowRecord维护借阅期限、逾期、续借和扣分状态
场所对象Bookshelf, TreasuredBookshelf, AppointmentOffice, BorrowAndReturnOffice, ReadingRoom对应图书可能所在的不同位置
服务对象ArrangeService, MoveService对应整理和移动这两类跨实体操作
辅助对象ScoreBoard, MovingTrace, BorrowLimit, LibraryIoAdapter对应评分、移动轨迹、规则检查和 IO 适配

这种追踪关系说明,最终代码基本保留了 UML 中的主要抽象。尤其是 HW14 之后,类图不再只是实体类图,而是包含了控制类、服务类和边界适配类,能够较完整地解释程序结构。

8.2 方法级追踪关系

方法级追踪关系更能体现模型和实现的一致性。例如:

业务需求UML/模型方法代码实现
借书borrowBookRequestHandler.borrowBook 调用 findAvailableShelfCopycheckBorrowLimitUser.borrowBookBookCopy.moveToUser
还书returnBookRequestHandler.returnBook 查询 BorrowRecord,更新信用分,将书移入 BorrowAndReturnOffice
预约orderBookRequestHandler.orderBook 创建 Reservation 并加入 AppointmentOffice
取书pickBookAppointmentOffice.pickBook 取出副本,Reservation.completeOrder 完成预约
查询轨迹queryMovingTrace通过 LibraryManager.getMovingTraceMovingTrace.queryMovingTrace 输出轨迹
阅览readBook将副本从书架移动到 ReadingRoom,并记录用户当前阅览书
归还阅览书restoreBookUserReadingRoom 移除阅览状态,放入借还处
评分gradeBookScoreBoard.gradeBook 记录评分,整理时影响目标书架
续借renewBookUser.renewBookBorrowRecord.renewBook 更新到期时间
查信用分queryCreditScoreUser.refreshOverdueCredit 后由 LibraryIoAdapter.printCreditInfo 输出

通过这种追踪表可以看到,一个业务需求通常不会只落在一个类上,而是由控制对象、领域对象、服务对象共同完成。UML 的意义就在于提前说明这些协作关系,避免代码中出现临时耦合。

8.3 模型与代码不完全一致的地方

最终模型和代码并不是天然完全一致的。我在本单元中也遇到过模型—代码追踪中的问题,例如:

  1. 类名大小写不一致:如 LibraryIOAdapterLibraryIoAdapter 的命名差异,会导致模型和代码追踪困难。
  2. 顺序图生命线与代码类不对应:如果 UML Lifeline 没有正确设置 represent,评测可能认为不存在合规消息路径。
  3. 为了覆盖 UML 消息而加入空方法:例如某些消息需要在 UML 中体现,但代码逻辑中没有自然方法对应,容易出现“为了模型而模型”的问题。
  4. 预设计图偏抽象,最终代码更细:预设计时不一定能想到所有 getter、辅助方法、容器访问方法,最终图需要补全。
  5. 重构后忘记同步 UML:代码中拆出了 MoveServiceBorrowRecord,如果类图未更新,就会破坏追踪关系。

这些问题让我认识到,UML 与代码的追踪不是一次性工作,而是随迭代持续维护的工作。模型不能停留在“提交材料”,而应当成为解释代码结构的工具。


九、Unit4 中出现过的 Bug 及原因分析

9.1 UML 追踪类 Bug

本单元中比较典型的问题是 UML 模型与程序结构不完全对应。例如顺序图中生命线没有正确关联到类,或者消息路径没有形成评测机认可的合规调用链。其根本原因是我一开始把顺序图理解为“画出大致流程”,而没有充分意识到课程评测会检查模型元素与代码类之间的严格对应关系。

这个问题的教训是:

  • 类图中的类名必须与程序类名保持一致;
  • 顺序图中的 lifeline 应能对应具体类或对象;
  • 消息名称应尽量对应代码中的方法名;
  • 预设计图和最终图都要随着代码重构同步修改。

9.2 逻辑边界类 Bug

在图书馆系统中,很多 bug 并不是语法问题,而是边界状态问题。例如:

  • 归还一本书后,书应先进入借还处,而不是直接回到书架;
  • 预约图书到预约处后,需要保护该副本,不能被其他预约或整理流程错误移动;
  • 用户已经有活跃预约时不能继续预约;
  • 用户阅览一本书时不能同时阅览另一 本;
  • 逾期扣分不能重复发生;
  • 续借必须在未逾期状态下发生。

这些问题的根本原因是状态转移复杂,而我早期没有把“状态机”抽象得足够清楚。后来通过 BookCopy.moveTo...MoveServiceBorrowRecord 等对象将状态变化集中管理,才逐渐降低了这类错误的出现概率。

9.3 架构类 Bug

HW13 中 LibraryManager 过于集中,导致多个业务流程共享同一批内部方法。一旦修改某个移动规则,可能影响借书、还书、预约整理等多个场景。这种 bug 的根本原因不是某一行代码写错,而是职责划分不充分。

HW14 之后,我通过 RequestHandlerArrangeServiceMoveService 分离职责,使请求处理、开闭馆整理、图书移动分别由不同对象负责。这样虽然类数量增加了,但每个类的修改原因更明确,架构风险反而下降。


十、使用大模型辅助正向建模的体验与方法

10.1 大模型的优势

在第四单元中,大模型对我最有帮助的地方不是直接生成完整代码,而是辅助我进行结构化思考。它比较擅长:

  1. 从需求中抽取候选类:例如图书、用户、预约、借还处、阅览室等。
  2. 给出职责划分建议:帮助我判断某个功能应该放在实体类、控制类还是服务类中。
  3. 发现命名和结构不一致:例如类名、方法名、UML 元素和代码之间的对应问题。
  4. 生成追踪表:把需求、类图、方法、测试点关联起来。
  5. 辅助检查边界条件:提醒我考虑重复预约、逾期扣分、预约过期等情况。

10.2 大模型的风险

但是,大模型也有明显风险:

  1. 容易给出看似合理但不符合课程包接口的设计。课程接口是硬约束,不能被大模型自由发挥。
  2. 可能忽视性能问题。它容易生成语义正确但复杂度较高的线性扫描实现。
  3. 可能忽视 UML 评测细节。例如 lifeline 的 represent、消息路径、类名严格对应等细节,大模型未必天然知道。
  4. 容易过度设计。对于课程作业,过多设计模式反而可能增加调试成本。
  5. 可能编造不存在的 API。因此必须用手册和代码环境校验。

10.3 如何引导大模型完成复杂架构设计任务

我总结出比较有效的提示方式是:不要一开始就让大模型写代码,而要分阶段约束它。

第一步,要求它只做需求抽象:

请只根据需求列出候选类、每个类的职责、需要维护的状态,不要写代码。

第二步,要求它给出架构边界:

请区分边界类、控制类、实体类、服务类,并说明每个类的修改原因。

第三步,要求它输出 UML 追踪表:

请建立“需求 -> UML 类/方法 -> Java 类/方法 -> 测试点”的追踪表。

第四步,要求它做反例检查:

请列出该架构在迭代中可能出错的边界状态和性能瓶颈。

第五步,才让它辅助局部实现或重构:

请在不改变已有类职责和公共接口的前提下,重构某一个方法。

这种使用方式的核心是:我必须掌握架构控制权,大模型只作为辅助分析和局部生成工具,而不能让它直接决定最终结构。


十一、如果在规格驱动开发中使用大模型

在 Unit3 的 JML 场景中,大模型也有一定价值,但需要谨慎使用。

11.1 优势

大模型可以帮助我:

  • 将 JML 条款翻译为自然语言;
  • 总结一个方法的前置条件、后置条件和异常行为;
  • 根据 JML 构造边界测试;
  • 提醒我注意 \oldassignable、异常优先级等问题;
  • 生成 JUnit 测试框架。

11.2 风险

大模型最容易忽视的是效率和容器设计。因为它在根据 JML 写代码时,常常倾向于“直接把量词翻译成循环”。这种实现语义上可能正确,但复杂度不一定能接受。例如涉及图结构连通性、最短路径、统计查询、标签成员维护等问题时,如果每次查询都全量遍历,强测很容易超时。

因此,在使用大模型处理 JML 时,我认为必须额外追问:

这个实现每个方法的时间复杂度是多少?
哪些查询可以增量维护?
哪些容器应该从 List 改成 MapSet?
是否存在重复计算?

11.3 用大模型辅助单元测试

大模型可以辅助生成基础单元测试,但不能完全替代人工设计测试。比较可靠的方式是让它按规格分组生成测试:

  • 正常路径测试;
  • 异常路径测试;
  • 边界值测试;
  • 状态转移测试;
  • 随机对拍测试;
  • 性能压力测试。

同时,要让大模型解释每个测试对应 JML 的哪一条规格。否则测试很可能只是“看起来覆盖很多”,但没有真正覆盖关键语义。


十二、四个单元中架构设计思维的演进

回顾四个单元,我的架构设计思维大致经历了四个阶段。

12.1 第一单元:从过程拆分到对象抽象

第一单元让我开始理解对象抽象的意义。表达式处理不能只靠字符串替换,而需要建立表达式、项、因子等结构。这个阶段我主要学习的是“把问题拆成对象”。

但当时我的架构设计仍然偏功能导向:先想输入怎么解析,再想输出怎么化简。虽然已经有对象,但对象之间的职责边界还不够清楚。

12.2 第二单元:从对象抽象到并发协作

第二单元的多线程电梯让我意识到,对象不仅有属性和方法,还有运行时协作关系。调度器、电梯、请求队列、输入线程之间存在并发交互,如果同步策略设计不好,就会出现死锁、轮询浪费、请求丢失等问题。

这个阶段我的架构思维从“静态对象划分”扩展到“动态协作与线程安全”。我开始重视共享资源的所有权、锁的粒度、线程退出条件和调度策略。

12.3 第三单元:从实现正确到规格正确

第三单元的 JML 让我意识到,程序不是样例正确就算正确,而是必须满足完整规格。架构设计也不能只看功能模块,还要看每个方法的契约边界。

这个阶段我开始重视:

  • 方法的前置条件和后置条件;
  • 对象不变量;
  • 异常优先级;
  • 查询与修改的副作用边界;
  • 容器选择和规格语义之间的一致性。

12.4 第四单元:从规格正确到模型可追踪

第四单元进一步要求我把架构显式建模出来。类图、顺序图和代码之间需要能互相解释。这个阶段我开始关注:

  • 类是否有明确职责;
  • 关系是否能解释代码依赖;
  • 顺序图是否能解释业务流程;
  • 预设计模型和最终模型是否一致;
  • 新增需求是否能映射到已有架构中。

因此,我认为自己的架构思维从“能写出来”逐步演进为“能解释、能验证、能迭代”。


十三、四个单元中测试思维的演进

我的测试思维也经历了明显变化。

单元早期测试方式后来的认识
第一单元构造表达式样例,看输出是否等价需要覆盖递归结构、化简边界和格式边界
第二单元跑随机请求,看是否能结束需要关注并发时序、线程退出、死锁和性能
第三单元对照 JML 构造单元测试测试应直接来源于规格,尤其是异常和边界
第四单元根据业务流程构造场景测试应覆盖状态转移、模型追踪和迭代回归

最初我把测试当成“找错工具”,后来逐渐把测试看成“设计的一部分”。好的测试可以反过来检验架构:如果一个功能很难测试,往往说明这个功能的职责边界不清晰;如果一个 bug 很难复现,往往说明状态变化没有被集中管理。

到第四单元时,我更倾向于从状态机角度设计测试。例如一本书从书架到用户、到借还处、再到书架或预约处,这条路径上的每个状态都应该被测试覆盖。用户从无预约到有预约、预约可取、完成取书、预约清除,也应该作为完整生命周期测试。


十四、课程收获总结

这门 OO 课程对我最大的影响,不是让我学会某个具体 Java API,而是让我逐渐建立了面向对象开发的完整意识。

第一,我认识到对象不是简单的数据容器。一个好的对象应该同时封装状态和行为,并且有明确的职责边界。像第四单元中的 BorrowRecord,如果只把借阅期限作为字段散落在 UserBookCopy 中,系统会很难维护;把它抽象为对象后,借阅生命周期就清晰得多。

第二,我认识到架构设计必须服务于迭代。作业每次都会增加功能,如果初始设计没有扩展点,后续就只能不断打补丁。LibraryManager 从单中心调度逐步拆分出 RequestHandlerArrangeServiceMoveService,正是我对可维护性的逐步理解。

第三,我认识到正确性不仅来自测试,也来自规格和模型。JML 让我从行为契约角度理解程序,UML 让我从结构追踪角度理解程序。测试可以发现错误,但规格和模型可以帮助我提前减少错误。

第四,我认识到复杂系统需要可追踪性。需求、规格、模型、代码、测试之间不能互相脱节。只要其中一个环节发生变化,其他环节也应同步更新。否则程序虽然暂时能运行,但长期看会越来越难维护。

第五,我认识到大模型可以作为有效工具,但不能替代开发者的判断。它可以帮助我梳理类、生成测试、检查边界,但最终的架构边界、课程接口约束、性能判断和模型追踪仍然需要我自己负责。

总体而言,OO 课程让我从“写出能通过样例的程序”逐步走向“设计一个可解释、可验证、可迭代的系统”。这也是我认为本课程最重要的训练价值。


十五、结语

第三单元和第四单元共同构成了我对软件工程化开发的进一步认识:JML 解决的是“行为是否有明确契约”的问题,UML 正向建模解决的是“结构是否有清晰设计”的问题,JUnit 和回归测试解决的是“迭代是否破坏已有行为”的问题。

如果用一句话总结本课程的收获,我认为是:面向对象开发的核心不是把代码写成很多类,而是让每个类都有清晰职责,让每个方法都有明确契约,让每次迭代都有可追踪的模型和可重复的测试。只有这样,程序才能从一次性作业逐渐接近真正可维护的软件系统。

...全文
19 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

308

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧