309
社区成员
发帖
与我相关
我的任务
分享本文围绕 2026 年面向对象设计与构造课程的四个单元展开,总结自己从“把功能写出来”到“先建模、再实现、再追踪”的思维变化。文章重点分析第四单元图书馆管理系统中的正向建模实践、两阶类图的作用、最终代码与 UML 模型之间的追踪关系,以及大模型在复杂架构设计中的辅助方式。
回顾整个 OO 课程,我最大的感受是:四个单元并不是简单地逐步增加题目难度,而是从四个角度训练我们管理软件复杂度的能力。
第一单元是表达式解析、展开与求导。它的复杂度主要来自语法结构:表达式、项、因子、函数调用、选择式、递推函数和求导算子之间存在天然的层次关系。如果只用字符串替换和局部判断来完成需求,第一次作业可能还能勉强通过,但到后续加入双变量、dx、dy、grad 和递推函数时,程序会迅速失控。因此第一单元让我意识到:对象划分要贴合问题本身的结构。
第二单元是多线程电梯。它的复杂度主要来自并发协作和时序约束。一个电梯系统不仅要考虑每部电梯如何运行,还要考虑输入线程、调度器、请求队列、电梯线程之间如何协作,以及如何避免轮询、死锁、请求丢失和线程提前结束。到了最后一次作业,临时检修、双轿厢改造、双轿厢回收等机制进一步把问题从“单个对象怎么做”推向了“多个对象如何在时间轴上协同”。
第三单元是 JML 规格化设计。它的复杂度主要来自契约约束。方法的前置条件、后置条件、副作用范围、异常行为都由规格描述清楚,实现时不能只追求返回值正确,还要保证对象状态变化符合契约。尤其在推荐系统、异常类和 JUnit 测试中,我开始把“方法到底承诺了什么”作为编码前的第一问题。
第四单元是图书馆管理系统和 UML 正向建模。它的复杂度主要来自模型、代码和行为的一致性。题目不再只要求写出一个能运行的程序,还要求提交一阶类图、二阶类图、状态图和顺序图。也就是说,我们不仅要让代码正确,还要说明代码为什么这样组织、对象之间如何协作、状态如何流转,以及最终实现能否追踪回最初的设计模型。
这四个单元合起来,使我的设计思维经历了从语法建模、并发建模、规格建模到 UML 正向建模的变化。第四单元是这种变化最集中、最明显的一次实践。
第四单元的背景是图书馆管理系统。图书馆中有 A、B、C 三类图书,每个 ISBN 号可能有多个副本;系统需要处理借书、还书、预约、取书、查询、阅读、归还、评分、续订、信用查询以及开闭馆整理等流程。最终阶段还引入了用户信用分、借阅期限和续订操作,使图书状态、用户状态和日期状态交织在一起。
我的第四单元最终代码大致可以拆成四层:
| 层次 | 主要类 | 核心职责 |
|---|---|---|
| 入口与总控层 | Main、LibraryManager | 初始化馆藏,读取官方包命令,区分开馆、闭馆、请求、信用查询等输入,并调用具体服务对象 |
| 数据与实体层 | LibraryRepository、Book、BookIsbn、BookCopy、User、Reservation、LoanRecord、MovingTrace | 保存系统中稳定存在的领域状态,如图书副本、用户、预约、借阅记录和流转轨迹 |
| 场所层 | Bookshelf、TreasuredBookshelf、AppointmentOffice、BorrowReturnOffice、ReadingRoom | 对应题面中的普通书架、精品书架、预约处、借还处、阅览室,负责局部的图书存取动作 |
| 业务服务层 | CommandProcessor、ArrangeService、BorrowLimitPolicy | 处理请求命令、整理流程和借阅/预约/阅读权限判断 |
这个结构的核心思想是:实体类保存状态,场所类表达局部动作,服务类组织跨对象流程,策略类隔离规则判断。
在实现前,我的一阶类图首先确定了比较稳定的领域对象:Book、BookCopy、User、Reservation、Bookshelf、AppointmentOffice、BorrowReturnOffice、ReadingRoom、LibraryManager 等。这个阶段的设计主要回答“系统中有哪些对象”和“它们大致承担什么职责”。
在实现过程中,随着信用分、借阅期限、续订、精品书架整理等细节逐渐清晰,我又对一阶类图做了补充和修正。最终二阶类图与代码保持一致:新增或强化了 LoanRecord、BorrowLimitPolicy、ArrangeService、CommandProcessor 等类的地位,把原本可能堆在 LibraryManager 中的流程拆分出去。这样既降低了单个管理类的复杂度,也让 UML 模型能够解释最终代码为什么这样组织。
第四单元最有价值的机制之一是两阶类图。经过这次实践,我对一阶类图和二阶类图的理解可以概括为:
如果在第四单元一开始就直接写代码,最容易出现的问题是把所有逻辑都塞进一个巨大的 LibraryManager 中。例如借书要查找图书、检查信用、检查借阅数量、移动副本、更新用户状态、输出结果;预约要维护预约记录、分配图书、判断失效;闭馆整理又要处理过期预约、未归还阅览室图书、逾期扣分和书架归位。如果这些逻辑全部集中在一个类中,后续新增需求时会非常难维护。
一阶类图的作用就是在写代码前先给架构画出边界。我在建模时先区分了几类对象:
BookIsbn 表示 ISBN,Book 表示同一 ISBN 的图书集合,BookCopy 表示具体副本。User 保存用户信用分、借阅记录、预约状态和当日阅读状态。Reservation 表示预约,LoanRecord 表示借阅记录。Bookshelf、TreasuredBookshelf、AppointmentOffice、BorrowReturnOffice、ReadingRoom 分别对应不同图书位置。LibraryManager 负责组织系统运行。这样,后续编码时每个业务规则都有了比较自然的落点。例如,图书副本的位置变化属于 BookCopy,借阅限制属于 BorrowLimitPolicy,预约处取书属于 AppointmentOffice,开闭馆整理属于 ArrangeService。
一阶类图不可能一次性预见所有实现细节。二阶类图的价值在于,它要求我们在代码完成后回头检查:最终代码是否偏离了最初设计?新增类和新增关系是否应该被纳入模型?UML 中的字段、方法和关联关系是否能在代码中找到对应?
我的二阶类图主要补充了以下内容:
LibraryManager 中分离为 CommandProcessor。这样 LibraryManager 只负责系统生命周期,具体命令分发和业务处理交给 CommandProcessor。ArrangeService。开馆前整理和闭馆后整理都通过它完成,避免整理逻辑散落在多个类中。BorrowLimitPolicy。信用分、A/B/C 类书限制、阅读权限、借阅期限等判断集中在这里。LoanRecord。它维护 borrowDate、dueDate、active 和 overduePenaltyApplied,避免用户类直接处理所有日期细节。BookCopy 和 MovingTrace 中,使状态图能够追踪到具体代码。二阶类图的意义不只是为了通过评测的 R1-R5 检查,更重要的是让最终代码具备可解释性。通过二阶类图,我可以清楚地说明:哪些类是真正的领域实体,哪些类是服务对象,哪些关系是组合或关联,哪些方法承担了状态迁移职责。
两阶类图让我意识到,UML 不应该是写完代码后用工具逆向生成的“截图”,而应该是参与开发过程的设计工具。一阶类图让开发不至于一开始就失控,二阶类图让迭代后的实现不至于和模型脱节。
更具体地说,一阶类图解决的是“我要怎样组织系统”,二阶类图解决的是“我是否真的按这个组织方式完成了系统”。二者之间的差异也有价值:如果差异很小,说明一开始的领域抽象比较稳定;如果差异很大,就需要说明为什么重构,哪些需求迫使模型变化,变化后的职责边界是否更合理。
这一部分重点分析我的最终代码设计如何追踪到 UML 模型。
| UML / 代码类 | 主要职责 | 关键属性或方法 | 追踪说明 |
|---|---|---|---|
Main | 程序入口 | main | 创建 LibraryManager 并启动系统 |
LibraryManager | 系统生命周期管理 | initializeLibrary、run、open、close | 对应 UML 中的总控对象,维护仓库、场所、策略、整理服务和命令处理器 |
LibraryRepository | 全局数据仓库 | books、bookCopies、users、reservations | 统一保存图书、图书副本、用户和预约记录 |
BookIsbn | ISBN 封装 | isTypeA、isTypeB、isTypeC | 屏蔽官方包 LibraryBookIsbn 的细节,提供类别判断 |
Book | 同 ISBN 图书集合与评分 | bookCopies、grade、getAverageGrade、isTreasured | 负责评分统计,并决定图书是否属于精品图书 |
BookCopy | 单本副本状态 | currentPlace、movingTraces、moveTo | 对应状态图中的核心对象,记录副本位置和移动轨迹 |
User | 用户状态 | activeLoans、activeReservation、readingBookCopy、creditScore | 维护信用分、当前借阅、当前预约和当日阅读 |
LoanRecord | 借阅记录 | borrowDate、dueDate、renew、applyOverduePenalty | 封装借阅期限、续订和逾期扣分状态 |
Reservation | 预约记录 | reservedBookCopy、expireDate、isPending、isDelivered | 区分“已预约但未分配”和“已送达预约处”两种状态 |
BorrowLimitPolicy | 权限与数量限制 | canBorrowBook、canOrderBook、canPickBook、canReadBook | 将信用分和 A/B/C 类图书限制集中管理 |
CommandProcessor | 开馆请求处理 | processCommand、borrowBook、orderNewBook、getOrderedBook、renewBook | 对应顺序图中的主要消息流 |
ArrangeService | 开闭馆整理 | arrangeBeforeOpen、arrangeAfterClose、deliverReservedBooks、moveBook | 对应整理流程和状态迁移 |
Bookshelf | 普通书架 | putBookOnShelf、takeBookFromShelf、drainBooks | 保存普通图书副本 |
TreasuredBookshelf | 精品书架 | putTreasuredBook、takeBookFromShelf、drainBooks | 保存评分达到精品标准的图书副本 |
AppointmentOffice | 预约处 | order、addReservedBook、pickReservedBook、clearExpiredBooks | 管理预约记录和已送达预约书 |
BorrowReturnOffice | 借还处 | receiveReturnedBook、drainReturnedBooks | 接收还书和阅读归还后的图书 |
ReadingRoom | 阅览室 | readBook、restoreBook、drainReadingBooks | 管理阅读状态和当日归还 |
MovingTrace | 图书移动轨迹 | toLibraryTrace | 将内部移动记录转为官方包需要的查询输出 |
从这个表可以看出,最终代码和 UML 模型之间基本保持同名、同责、同关系。尤其是 LibraryManager 对各个服务与场所对象的组合关系、User 对 LoanRecord 和 Reservation 的关联、BookCopy 对 MovingTrace 的聚合关系,都能直接在代码字段中找到对应。
直接借书的代码链路可以概括为:
LibraryManager.run
-> CommandProcessor.processCommand
-> borrowBook
-> BorrowLimitPolicy.canBorrowBook
-> takeBookFromShelves
-> BookCopy.moveTo(USER)
-> User.addLoan(LoanRecord)
-> PRINTER.accept
这里的职责分配比较清晰:
CommandProcessor 负责组织请求处理流程;BorrowLimitPolicy 负责判断用户是否具备借阅资格;Bookshelf 和 TreasuredBookshelf 负责提供可借副本;BookCopy 负责状态迁移;User 和 LoanRecord 负责记录借阅状态和到期日。这个流程也对应状态图中从 Bookshelf 或 TreasuredBookshelf 到 User 的迁移。代码中 borrowBook 方法上使用了 @Trigger(from = "Bookshelf", to = "User") 和 @Trigger(from = "TreasuredBookshelf", to = "User"),使状态图与代码之间存在明确的追踪点。
预约和取书是顺序图评测重点关注的场景。我的实现中,预约的入口方法是 orderNewBook,取书的入口方法是 getOrderedBook。这两个方法同时也是顺序图中可以直接使用的消息名。
预约流程大致为:
CommandProcessor.orderNewBook
-> BorrowLimitPolicy.canOrderBook
-> new Reservation(user, isbn)
-> User.setActiveReservation
-> LibraryRepository.addReservation
-> AppointmentOffice.order
-> PRINTER.accept
取书流程大致为:
CommandProcessor.getOrderedBook
-> AppointmentOffice.query
-> BorrowLimitPolicy.canPickBook
-> AppointmentOffice.pickReservedBook
-> BookCopy.moveTo(USER)
-> User.addLoan(LoanRecord)
-> Reservation.completePick
-> User.clearActiveReservation
-> PRINTER.accept
这两条链路体现了预约状态的两阶段:
Reservation,此时预约处还不一定有书;ArrangeService.deliverReservedBooks 选择可用副本送到 AppointmentOffice;这种设计避免了把预约理解成“立即占有图书”。Reservation 的 isPending 和 isDelivered 方法分别表达“等待分配”和“已经送达”的两种状态,使业务语义更清楚。
整理流程是第四单元最容易写散的部分。我的代码把它集中在 ArrangeService 中,并把开馆前整理和闭馆后整理统一到 arrange(LocalDate today, boolean isCloseArrange) 中。
整理流程大致按以下顺序执行:
clearExpiredBooks 清理预约处过期图书,并处理预约不取扣分
settleOverdueLoans 检查借阅逾期并扣分,避免重复扣分
penalizeUnrestoredReading(仅闭馆后)处理阅读后不归还扣分
deliverReservedBooks 为等待中的预约分配图书
moveReadingBooks 将阅览室图书整理回目标书架
moveReturnedBooks 开馆前将借还处图书整理回目标书架
arrangeShelfCategories 按评分结果调整普通书架和精品书架
这个设计的优势是:整理流程不再只是若干 if 的堆叠,而是一个有稳定顺序的批处理管线。每个步骤负责一种规则,moveBook 负责统一触发 BookCopy.moveTo,从而记录图书移动轨迹并生成官方输出所需的 LibraryMoveInfo。
BookCopy 的追踪关系状态图围绕图书副本展开,核心状态包括:
BOOKSHELF:普通书架;TREASURED_BOOKSHELF:精品书架;APPOINTMENT_OFFICE:预约处;BORROW_RETURN_OFFICE:借还处;READING_ROOM:阅览室;USER:用户持有。代码中,BookCopy 使用 currentPlace 记录当前状态,使用 movingTraces 记录历史轨迹。所有状态变化都通过 moveTo 完成:
public MovingTrace moveTo(LocalDate date, LibraryBookState targetPlace) {
recordMoveTarget(currentPlace, targetPlace);
MovingTrace trace = new MovingTrace(date, this, currentPlace, targetPlace);
movingTraces.add(trace);
currentPlace = targetPlace;
return trace;
}
这段设计体现了状态图与代码的直接对应:状态图中的每条迁移最终都会落到 BookCopy.moveTo 上,而查询历史轨迹时则通过 getLibraryTraces 将内部记录转换为官方输出。
第四单元中,我尝试使用大模型辅助理解需求、整理类职责和检查 UML—代码一致性。我的结论是:大模型适合做“结构化辅助”,但不能替代设计判断。
我认为比较有效的引导方式有四种。
第一,让大模型先做领域名词提取,而不是直接生成代码。例如要求它从题面中列出实体、场所、动作、状态和约束。对图书馆系统来说,实体包括图书、图书副本、用户、预约、借阅记录;场所包括普通书架、精品书架、预约处、借还处、阅览室;动作包括借、还、预约、取、读、归还、评分、续订、整理。
第二,让大模型输出职责表。我会要求它说明每个职责应该落在哪个类中,以及为什么。例如信用分应属于 User,但信用分判断规则应放进 BorrowLimitPolicy;图书状态应属于 BookCopy,但整理动作应由 ArrangeService 统一组织。
第三,让大模型做模型—代码追踪检查。给出类图类名、代码类名、字段、方法和关联关系,让它检查是否存在模型中有但代码没有、代码中有但模型没有、关联方向不清晰、方法可见性不满足顺序图规则等问题。
第四,用具体场景驱动它找遗漏。例如给出“某用户预约 C 类书,闭馆后整理送达预约处,用户五天内未取,预约失效并扣分,图书回到精品书架”的场景,让它检查涉及哪些类、哪些状态、哪些日期边界、哪些输出。
大模型的问题也很明显。
它可能会给出看似完整但不符合评测规则的 UML 设计。例如顺序图跨类消息不能调用 private 方法,状态图的 Trigger 名称必须和代码注解对应,Guard 条件还要满足文法限制。这些规则如果不显式告诉大模型,它很容易按一般软件工程经验给出不合格答案。
它也可能忽略官方包的特殊要求。例如官方输入输出接口一次读入必须对应一次输出,图书移动的输出位置、预约保留期、信用分边界值、续订失败条件等都必须严格回到题面确认。大模型适合帮助组织结构,但最终正确性仍然取决于自己是否认真核对题面和代码。
对于复杂架构任务,我认为比较稳定的提示方式是:
你现在是一个严格的 OO/UML 架构审查者。
请基于以下题面和 Java 类列表,完成:
1. 提取领域实体、场所、流程、状态和约束;
2. 给出类职责表,说明每个职责为什么属于该类;
3. 检查类图与代码之间是否存在缺失类、缺失字段、缺失方法或错误关联;
4. 以“预约成功”和“取书成功”为场景,生成顺序图消息链;
5. 注意跨类消息不能调用 private 方法,状态图 Trigger 必须与 @Trigger 注解方法名一致。
这个提示的关键不是写得长,而是把任务拆成可验证的步骤,并把课程评测的硬约束明确写进去。
第一单元让我意识到,表达式问题本质上不是字符串问题,而是语法树或层次结构问题。表达式由项组成,项由因子组成,因子又可能是常数、幂函数、指数函数、表达式因子、自定义函数、选择式或求导因子。
在这个单元中,我逐渐形成了“解析—建模—化简—输出”分层的意识。解析器负责把输入转换为结构化对象;表达式对象负责代数运算;求导逻辑通过分层派发完成;递推函数通过展开和缓存降低重复计算。这个阶段的重点是让对象结构贴合文法结构。
第二单元的电梯系统让我意识到,多线程程序的难点不在某一个类内部,而在对象之间如何协作。输入线程不断投放请求,调度器要分配请求,电梯线程要执行动作,请求队列要支持等待、唤醒和结束判断。
因此,我的设计重点从“类怎么封装数据”转向“对象之间如何通信”。例如生产者—消费者模式、wait/notify 机制、共享队列的互斥访问、线程终止信号、调度策略和执行策略的分离,都是这个单元的核心。最后一次作业中临时检修、双轿厢改造和回收机制进一步说明:并发系统不仅要有类图,还要有清晰的状态机和时序约束。
第三单元的 JML 规格化设计改变了我对方法的理解。过去我更关心“输入后返回什么”,而 JML 要求我同时关注:什么情况下方法可以被调用、调用后对象状态如何变化、哪些属性不能被修改、哪些异常应该被抛出。
在在线视频平台系统中,User、Network、Video 的功能不断扩展,新增推荐视频、推荐 up 主、查询影响力、查询用户兴趣等方法。此时如果只按功能堆代码,Network 很容易膨胀。规格化设计促使我把数据结构维护、异常处理、推荐计算和测试覆盖分开考虑。
这个单元让我真正理解到:规格不是代码之外的负担,而是类职责边界的一部分。
第四单元则进一步要求“先有模型,再有代码”。类图帮助我确定系统静态结构,状态图帮助我检查图书副本的状态流转,顺序图帮助我检查对象间的消息链路。
相比前三个单元,第四单元的架构设计不再只是为了降低代码复杂度,也是为了让代码能够被模型解释。一个类为什么存在、一个字段为什么属于这个类、一个方法为什么是 public、一个状态迁移由哪个方法触发,都需要能追踪到 UML 模型。
总体来看,我的架构设计思维经历了四个阶段:
字符串与过程处理
-> 语法结构驱动的对象建模
-> 并发对象协作
-> JML 契约驱动实现
-> UML 模型驱动开发
这门课给我最大的收获,是让我真正理解了“复杂度管理”在软件开发中的意义。很多程序不是因为某一行代码写错而失败,而是因为一开始没有划清职责边界,导致后续需求一来,旧代码无处安放新规则。
第一单元让我学会用层次化对象处理复杂语法,第二单元让我学会在并发环境中考虑对象协作和生命周期,第三单元让我学会尊重规格和契约,第四单元让我学会用 UML 模型指导开发并追踪实现。
我也更加理解了迭代开发中的可扩展性。一个设计在第一次作业中能通过,不代表它能承受第二次和第三次迭代。真正稳定的设计应该让新增需求尽量落在自然的位置,而不是每次都推翻重来。第四单元的图书馆系统就是典型例子:当新增信用分、借阅期限、续订、精品书架、阅读和预约失效时,如果已经把图书副本、用户、预约、借阅记录、地点和整理服务分清楚,扩展就主要表现为局部增强,而不是整体重写。
最后,我对大模型辅助编程也有了更理性的认识。大模型可以帮助梳理需求、生成候选类、整理 UML 方法签名、构造测试场景、检查追踪关系,但它不能替代我对题面、官方接口和评测规则的判断。越是复杂的场景,越需要把任务拆成结构化步骤,并用具体约束限制大模型输出。
完成四个单元后,我对 OO 的理解从“多写几个类”变成了“让类的职责、对象的状态、系统的交互和需求的变化保持一致”。这也是我认为这门课最重要的训练价值:它不是单纯训练 Java 语法,而是训练面对复杂需求时仍然保持结构清晰、责任明确、可追踪、可验证的软件设计能力。
感谢您能看到这里。
整个OO冒险,从自己刚进6系学习OOpre时面对陌生Java语言的焦虑,到OO颁奖课上同时斩获“得意门生”和“狼人奖”,真是一段奇妙的旅程。
如果问我有什么感言,回望过去,我实在有太多遗憾和不舍,有太多未竟的事、太多错过的人,踩过很多坑走了很多弯路。是6系的同窗和热心的学长姐在我灰暗的心里种下了一颗名为希望的种子。星爷说过,“一万年太久,只争朝夕”。为了心之向往,我正在路上。你呢?
最后的最后,感谢OO课程组、助教团队、诸彤宇老师。OO的故事告一段落了,我们的故事仍在继续,未来已来,未来可期。