OO Unit4 博客作业及OO课程总结

回顾整个学期的 OO 作业,真的感触颇深
每一次看需求看不懂的时候,每一次debug找不出来的时候,每一次进C房或者甚至没进互测的时候,每次都非常崩溃,但是回过头来看,当时阻止我的都是些非常小的事情,现在有一种如释重负的感觉。

> 坦白来说很多次测试不够的原因都是周末出去看剧了,遗憾吗那很遗憾了,明明稍微测试注意一下可能就很好了,后悔吗不后悔,再选一次我也会这么选择。

第四单元作为整个课程的收束,要求我们通过 UML 类图、状态图、顺序图来描述系统结构和对象协作。

一、第四单元:正向建模与两阶类图

1. 一阶类图:先定方向

一阶类图对我来说像是“设计草稿”,它要求我在写代码前先回答几个问题:

  • 哪些对象是系统中的核心对象?
  • 哪些规则应该放在用户身上?
  • 哪些状态应该放在图书副本身上?
  • 哪些流程需要统一的管理类来调度?
  • 哪些地点类需要作为独立对象存在?

这个阶段的重点是先搭出系统骨架。先抽象出用户、图书、图书副本、书架、预约处、借还处这些概念,否则后续代码很容易变成一个大类里塞满各种 Map 和判断语句。

2. 二阶类图:实现后的校准

二阶类图更像是“复盘和校准”。代码写完之后,很多一开始没想清楚的地方会暴露出来,比如:

  • LibraryManagerLibrary分别应该负责什么职责;
  • TreasuredBookshelf 应该作为独立地点出现还是可以直接作为一个属性放在BookShelf里;
  • User 需要维护信用分、预约状态、借阅状态;
  • 查询、加入、移除等职责是放在地点类里还是都放在Library里。

因此,两阶类图最重要的是逼着我们完成:

1
设计 -> 实现 -> 修正

如果只有最后一张图,我很可能只是照着代码反向生成一个uml图;但有了一阶图,后面的二阶图修改就能看出自己设计思路的演化。


二、第四单元架构设计与 UML—代码追踪

最终我的图书馆系统大致分为以下几部分。

模块 主要类 职责
输入与分发 MainClass 读取命令,转发给管理类
全局调度 LibraryManager 处理开馆、闭馆、整理、移动和各类请求
图书信息 Book 保存 ISBN 层面的类别、评分、是否精品等信息
副本状态 BookCopy 保存具体副本的位置、轨迹、预约归属、借阅期限等
用户状态 User 保存持有图书、预约记录、正在阅读图书、信用分等
地点对象 BookShelfTreasuredBookshelfAppointmentOfficeBorrowAndReturnOfficeReadingRoom 保存各地点持有的图书并提供对应操作

1. 类图与代码的追踪

代码和 UML 的追踪关系主要体现在三个方面:

追踪内容 具体表现
类名追踪 UML 中出现的核心类应在代码中存在
属性方法追踪 UML 中的属性和方法应对应真实职责
关联关系追踪 UML 中类之间的关联应和代码中的持有关系一致

2. 状态图与代码的追踪

在 HW14 中,我遇到过 Guard 无解的问题。当时我一直纠结 isTreasured 是否可变,后来才理解:状态图不应该机械照搬代码里的 if 判断,而应该表达抽象状态和转移条件。如果 Guard 设计得太死,就会出现逻辑上不可达的路径。

这让我对“模型”和“代码细节”的区别有了更直观的认识。模型应该表达核心状态变化,而不是把代码中的每个判断都搬到图里。
alt text

3. 顺序图与代码的追踪

顺序图则让我关注对象之间的协作。例如预约和取书流程并不是用户直接修改书的位置,而是需要经过协作过程:

1
User -> LibraryManager -> AppointmentOffice / BookShelf -> BookCopy

顺序图要求 Lifeline 对应代码中的类,消息对应代码中的方法,这使我进一步理解了对象之间“谁调用谁、谁负责处理、谁保存状态”的关系。

alt text

三、大模型辅助正向建模的体会

第四单元我比较多地使用了大模型。它最有帮助的地方是帮我整理复杂需求、列出可能的类、解释评测反馈。
比如一开始从题目中抽取 BookBookCopyUser、地点类时,大模型能比较快地给出一个初始框架;评测报缺类、缺方法时,它也能帮助分析应该补到哪个类里。

但我也发现,大模型很容易生成“看起来合理但不一定符合本作业”的设计。比如它可能倾向于把所有逻辑都放进一个 Manager,或者画出语义上像样、但和代码不一致的 UML。状态图中它也容易把代码里的 if 条件直接翻译成 Guard,导致路径无解。

因此,我认为在复杂场景中引导大模型做架构设计更有效的方式是分步骤引导。

1. 先抽取需求,而不是直接设计

可以先让大模型列出:

  • 业务实体;
  • 状态;
  • 操作;
  • 限制条件;
  • 对象之间的关系。

这样可以避免它一开始就给出过早、过泛的架构。

2. 再整理职责表

比起直接生成图,更好的方式是让它输出:

类名 职责 属性 方法 关联对象

这样更容易检查职责是否错位,也更容易和代码对应。

3. 结合评测反馈做最小修改

评测反馈要原样给大模型,并明确要求:

  • 不要推翻整体架构;
  • 只解释每条反馈对应的问题;
  • 只给出最小修改方案。

比如地点类缺少 query,就补地点类方法;LibraryManager 缺少地点属性,就补关联和属性,而不是重新设计整个系统。

4. 最终判断仍然要靠自己

大模型可以提高效率,但不能代替自己理解系统。比如:

  • BookBookCopy 的边界;
  • 信用分应该属于 User
  • isTreasured 是可变属性;
  • LibraryManager 应该负责全局整理;
  • Guard 条件不能写得过死。

这些都需要结合题意和自己的代码判断。


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

四个单元下来,我的架构设计思维经历了比较明显的变化。

单元 架构关注点 我的理解
Unit1 内部表示 复杂表达式需要合适的数据抽象
Unit2 对象协作 多线程系统要明确同步与协作关系
Unit3 状态一致性 方法实现要围绕规格和状态变化设计
Unit4 模型追踪 UML、代码和需求应能互相对应

1. Unit1:从“能跑”到“要有内部表示”

第一单元中,我最开始的架构是:

1
Processor -> Lexer / Parser -> Expr / Term / Factor -> Poly / Mono -> OutputHandler

一开始这个结构比较清晰,但随着函数、选择表达式、递推函数、求导和输出优化不断加入,PolyOutputHandler 的复杂度越来越高,bug 也主要集中在这些类中。

这个单元让我第一次意识到:架构不是把功能都塞进一个核心类里,而是要真正拆清职责。早期为了方便而采用字符串预处理,后面遇到复杂函数嵌套和选择表达式时就变得很难维护,最后不得不重构。这让我体会到,前期偷懒的架构选择,后期往往要付出更大的代价。

2. Unit2:从“类的划分”到“对象的协作”

第二单元是多线程电梯,重点不再只是类怎么拆,而是对象之间如何协作。ScheduleElevatorThreadRequestTableRequestQueueShaft 等对象之间需要通过锁、等待、唤醒机制配合。

这一单元让我认识到:架构不仅是静态结构,也包括运行时的协作关系。多线程程序中的很多问题来自共享状态、输出顺序、线程结束和资源竞争。相比第一单元,我开始更多思考:

  • 谁持有共享状态?
  • 谁负责加锁?
  • 谁负责唤醒线程?
  • 线程在什么条件下安全结束?

3. Unit3:从“实现方法”到“维护状态一致性”

第三单元引入 JML 后,我开始从规格角度看程序。我的理解是:

1
先读规格 -> 再设计状态 -> 最后写实现

JML 中的 requiresensuresassignablesignals 明确规定了方法的前置条件、结果、副作用和异常行为。这让我意识到,写方法不是只要返回值正确,还要保证状态变化范围正确。

这一单元我开始重视“状态影响表”:每新增一个字段,都要思考哪些旧方法会读它、改它,哪些查询依赖它。比如 watchVideo 后来不仅要维护未观看列表,还要维护播放量、观看历史和用户兴趣;coinVideo 也不仅是给视频加投币数,还要同步维护用户硬币和 up 主贡献者信息。

因此,第三单元让我从“类的职责划分”进一步转向“状态的一致性维护”。

4. Unit4:从“代码结构”到“模型追踪”

第四单元要求先画一阶类图,再编码,再修正二阶类图。这让我真正感受到 UML 不是代码完成后的补图,而是编码前帮助自己理清对象、职责和关系的工具。

第四单元最大的收获是“可追踪性”:代码中的类、属性、方法、状态变化和对象协作,都应该能在 UML 类图、状态图和顺序图中找到对应。到这一单元,我对架构的理解从“写一些类完成任务”,变成了“让需求、模型和代码之间能互相解释”。


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

我的测试思维也随着四个单元逐渐变化。

单元 测试方式 关注重点
Unit1 极端样例测试 解析、化简、输出
Unit2 场景测试 多线程协作、锁、结束条件
Unit3 JUnit 测试 返回值、副作用、状态一致性
Unit4 模型检查 UML 与代码追踪、状态图可达性

1. Unit1:极端样例测试

第一单元中,我的测试主要依赖手写复杂样例和互测经验,重点攻击符号、括号、指数、函数嵌套、输出格式和性能边界。

例如:

  • * 后带正负号;
  • 括号后连续正负号;
  • (0)^0
  • 深层函数嵌套;
  • exp() 输出格式。

这一单元让我认识到,测试不能只测样例,而要攻击自己代码中最复杂、最不放心的部分。

2. Unit2:场景测试

第二单元中,多线程 bug 往往不能通过单个输入稳定复现,因此测试重点变成了构造场景。

我主要关注:

  • 所有电梯维修;
  • 单部电梯被大量派单;
  • F2 层互斥;
  • 维修、回收、改造时的输出顺序;
  • 线程能否安全结束。

这个单元让我意识到,测试不只是构造输入,还要理解程序运行过程。尤其是多线程程序,必须围绕资源竞争、时序和结束条件设计测试。

3. Unit3:规格化测试

第三单元后,我开始形成比较固定的 JUnit 测试方式:

1
固定边界样例 + 随机生成数据 + 朴素 oracle + 状态快照检查

固定样例覆盖边界情况,随机样例扩大覆盖范围,朴素 oracle 计算期望结果,状态快照检查方法是否误改了不该改的对象。

这一单元让我真正理解了 assignable 的意义:测试不仅要看返回值,还要看副作用是否正确。比如查询方法不能改变状态,清理评论不仅要删除内容,还要保持 commentIdscommentContents 的对应关系。

4. Unit4:模型一致性检查

第四单元中,测试对象不再只有代码,还包括 UML 模型本身。类图要和代码中的类、属性、方法、关联关系对应;状态图要保证 Guard 可解、Trigger 和代码注解对应;顺序图要保证 Lifeline 和消息能追踪到代码。

之前我在状态图中遇到过路径无解的问题,这让我意识到模型本身也可能有 bug,而且同样需要调试。

整体来看,我的测试思维从“找几个 hack 点”,逐渐变成了“围绕设计风险构造验证”。


六、课程收获

这门课最大的收获,是让我真正理解了面向对象思想,也就是围绕职责、状态和协作来组织系统。

1. 更重视职责边界

第一单元中 Poly 过重,第二单元中调度器和电梯线程边界需要清楚,第三单元中 UserVideoNetwork 的状态不能混乱,第四单元中 BookBookCopy 必须区分。

很多 bug 的根源不是某一行代码写错,而是一开始没有想清楚状态应该属于谁、职责应该由谁承担。

2. 学会用迭代眼光看设计

每个单元都是三次作业逐步扩展。如果一开始只为当前需求写死,后面就会非常痛苦。好的架构不一定一开始完美,但应该能随着需求变化自然演化。

3. 对复杂度更加敏感

Unit1 的输出优化和 Unit3 的 queryMutualFollowingSum 都让我认识到:结果正确不代表实现可用。高频查询、重复计算和不合适的数据结构都会导致性能问题。后来我逐渐习惯使用增量维护、缓存、HashMapHashSet 等方式降低复杂度。

4. 更谨慎地使用大模型

大模型在查语法、解释概念、生成测试思路和分析评测反馈时很有帮助,但不能完全代替自己做架构判断。如果整体设计不是自己想清楚的,代码会变得不像自己的,debug 时反而更痛苦。

我现在更倾向于把大模型当作辅助工具:用它整理需求、解释反馈、生成检查清单,而不是让它直接决定整体架构。


七、结语

四个单元分别从不同角度训练了我的 OO 能力:

  • Unit1 让我学会解析和抽象;
  • Unit2 让我理解并发和协作;
  • Unit3 让我理解规格和状态一致性;
  • Unit4 让我理解建模和追踪关系。

OO的征程已经结束,但对面向对象设计思想的学习不会止步。
未来还很长,还有更多比多线程电梯调度更难过的关,希望我也能够保持那份心气,继续勇敢走下去,仰望星空脚踏实地,拥抱闪闪发光的自己。