面向对象第四单元总结:从正向建模到设计驱动开发

杨璞-24371240 2026-06-19 19:59:55

第四单元的三次作业以一个图书馆管理系统为背景,逐步迭代:hw13 建立借阅、还书、预约、取书、查询等核心流程;hw14 引入精品书架、阅读、归还和评分系统;hw15 加入用户信用分、借阅期限限制和续订功能。这一单元与前三个单元最大的区别在于:不再是读懂规格然后实现,而是先设计类图,再写代码,并且要保证两者的关系可以检验。

一、正向建模与类图在开发中的作用

正向建模的核心体验可以简单概括为:把问题域的概念直接翻译成类,而不是从实现出发然后再补图。

拿 hw13 的第一阶段来说,拿到指导书以后我的第一步不是打开 IDE,而是把题目里出现的名词过一遍——图书馆、书架、借还处、预约处、图书(ISBN)、书本(副本)、用户、预约、移动记录。这些名词里,有些对应实体类(BookBookCopyUserReservation),有些对应地点类(BookshelfBorrowReturnOfficeAppointmentOffice),有些对应服务类(LibraryManagerRequestProcessorArrangementService)。从这个视角出发,类图很快能成型。

两阶类图的意义

这一单元的评测分一阶类图和二阶类图两次提交。一阶类图要在代码开始写之前交,这个约束很有意思:它迫使你在没有任何实现的情况下,先把架构想清楚。

我的体会是,这个强制前置的设计约束确实有效。在设计一阶类图的时候,我意识到了一些如果直接写代码可能会忽略的问题:

  1. 位置抽象:书本在不同时间处于不同"地点",这个状态需要显式建模。我抽出了 BookLocation 枚举和 LibraryPlace 基类,书架、借还处、预约处都继承自它。这个抽象让后来 hw14 加入阅览室、精品书架时只需新增子类,不需要修改核心逻辑。

  2. 移动轨迹:查询指令要求输出一本书的历史移动轨迹,这意味着需要一个单独的 TraceRecorder 对象,把每次位置变化都记下来。如果不在设计阶段考虑,写代码时可能会把轨迹记录散落在各种方法里,很难维护。

  3. 借阅限制策略:A/B/C 三类书的限制规则不同,我把它们抽到了 BorrowLimitPolicy 类里,而不是在 RequestProcessor 里写一堆 if。这样后来 hw15 引入信用分限制时,只需要在 BorrowLimitPolicy 里加一个条件,不用动 RequestProcessor

二阶类图则是在代码完成后同步更新的,它的价值在于让类图和代码保持一致,而不是代码写完了随便画个图应付。从一阶到二阶,我的改动主要集中在方法签名和一些新增的辅助方法上,整体架构保持稳定,这说明前期设计的质量还算可以。

二、三次作业的架构设计与代码—UML追踪关系

整体架构

三次作业共享同一套核心架构,核心类图大致如下:

LibraryManager
  ├── Library (聚合地点和书本,提供移动接口)
  │     ├── Bookshelf
  │     ├── BorrowReturnOffice
  │     ├── AppointmentOffice
  │     ├── TraceRecorder
  │     ├── Map<BookId, BookCopy>
  │     └── Map<String, User>
  ├── RequestProcessor (处理用户请求)
  ├── ArrangementService (整理流程)
  ├── BorrowLimitPolicy (借阅规则)
  └── OutputAdapter (格式化输出)

Library 是整个系统的状态中心,所有对书本位置的变更必须通过它的 moveBook 系列方法进行,这样移动记录能够自动被 TraceRecorder 捕获。RequestProcessor 只做规则判断和流程分发,不直接修改状态。ArrangementService 负责开馆前和闭馆后的整理逻辑,也只调用 Library 的移动方法。

这种设计的好处体现在迭代时:hw14 加入阅览室和精品书架,只需要在 Library 里加对应的地点类,并在 ArrangementService.arrangeBeforeOpen 里增加 moveReadingRoomBooksadjustShelves 两个步骤。原有的借书、还书逻辑几乎没有动。

代码与 UML 的追踪关系

评测系统会检查 R2-R5,即类图中的类、属性、方法、关系要与代码对应。在实际操作中,这个检查让我养成了一个习惯:写完一个类以后马上更新类图,而不是等全部代码写完再补。

比较典型的例子是 BookCopy 类。在 hw13 时,它只有 bookIdlocationholderIdreservedFor 等属性;到 hw15 增加了 borrowDatedueDateoverduePenaltyApplied 等与借阅期限相关的字段,以及 renewisOverdueneedsOverduePenalty 等方法。这些都需要同步到二阶类图里。

关联关系的维护是最容易出问题的地方。Library 持有 Bookshelf,意味着类图里应该有从 LibraryBookshelf 的关联箭头;BookCopy 持有 Book 的引用,意味着 BookCopyBook 也有关联。如果类图里漏了这些,R5 检查就会报错。我的经验是,每次新增一个类或者修改成员变量时,顺手在 StarUML 里检查关联是否更新,成本比事后批量补要低很多。

各次作业的增量变化

hw13 → hw14 的主要变化是地点类扩张。增加了 ReadingRoomTreasuredBookshelfBookLocation 枚举新增对应值,ArrangementService 的整理逻辑多了阅览室清空和书架调整。Book 类增加了 scoreSumscoreCountisTreasured 方法,Library 增加了 gradeBook。二阶类图里对应增加了这些类和关联,同时添加了状态图来描述图书的生命周期。

hw14 → hw15 是改动最大的一次。User 增加了 credit 字段和 changeCreditcheckOverduePenalty 等方法;BookCopy 增加了借阅期限相关的字段和方法;BorrowLimitPolicy.canBorrow 开始检查信用分;RequestProcessor 增加了 renewBookhandleCreditQuery 等处理函数。ArrangementService.arrangeBeforeOpen 在最前面加上了 library.checkAllOverduePenalties(date) 的调用。这次作业还需要提交顺序图,描述预约和取书的交互流程,让 UML 建模的层次从结构图扩展到了行为图。

三、大模型辅助正向建模的体验与方法

在三次作业的设计过程中,我都有用大模型辅助建模,主要集中在两个阶段:从题目到领域模型,以及从领域模型到类图细化

有效的引导方式

大模型在"提取名词、划分职责"这一步很好用。给它完整的题目描述,让它列出候选的类和职责,通常能覆盖大部分核心实体,也会提一些你容易漏掉的辅助类。对 hw13,它提示了需要一个独立的移动轨迹记录器,这个我在自己浏览题目时确实没有第一时间想到。

但大模型在关系建模上容易出偏差。让它直接生成类图或者 UML 描述,它倾向于把所有东西都往 LibraryManager 上堆,或者把地点类和服务类的职责混在一起。所以我的策略是:先让它出候选类列表,然后自己把关系和职责确认一遍,再让它帮我补全各类的属性和方法。

对于复杂的迭代场景,比较有效的引导方式是给它提供已有的一阶类图结构,然后告诉它新增的需求点,让它分析"需要在哪里增加什么"。这样它的输出更聚焦,也更容易验证。

需要警惕的问题

有两类问题需要特别注意。

第一类是过度设计。大模型倾向于引入更多的接口、抽象类和设计模式,比如为每种操作类型生成对应的命令类,为每种地点生成对应的策略类。对于本单元这种规模的作业,这些抽象不仅不必要,还会让类图和 R2-R5 的追踪关系更难维护。应该告诉它"只需要满足题目要求的最小设计,不要引入不必要的抽象"。

第二类是方法参数和返回值的细节。大模型生成的方法签名经常和实际需要的不一致,比如参数顺序、可见性、是否有返回值等。类图评测对这些细节有要求,所以方法签名最终还是要自己确认。

总体来说,大模型在正向建模中更适合作为检查清单和思路发散工具,而不是"帮我直接生成设计"的替代品。最终的类图设计决策,特别是关于职责划分和关系建模的部分,还是要自己做。

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

回头看四个单元,架构设计思维的变化是比较明显的。

第一单元(表达式解析)的架构是自底向上的。先确定数学底座(归一化多项式),再设计解析层,类之间是单向依赖。这个阶段我对"架构"的理解还比较朴素,更多是在解决"怎么把问题拆开"的问题,还没有特别意识到扩展性的重要性。

第二单元(多线程电梯)的核心约束是线程安全。架构设计的重心是"共享资源怎么保护、锁的粒度怎么定"。这个单元让我理解了分层的意义:输入层、调度层、执行层各自管自己的锁,不相互嵌套,降低了死锁风险。但这个阶段的架构还是比较面向具体问题的,没有太多复用性的考虑。

第三单元(JML规格)的架构是规格驱动的。给什么接口,就实现什么方法,关键是状态一致性——所有容器要同步更新,pure 方法不能有副作用。这个单元让我养成了"先问清楚 assignable 范围再写代码"的习惯。

第四单元(UML正向建模)是这四个单元里最接近"先设计后实现"的一次。设计时必须把所有概念都显式建模,不能有隐含的依赖;实现时要保证代码和设计的追踪关系。这个单元让我理解了一件事:好的类图不是代码写完以后的文档,而是让代码更容易写正确的指导。

四个单元加在一起,我对架构的理解从"把代码组织整齐"变成了更接近"提前思考变化点,把变化隔离在合适的位置"。第四单元里,BorrowLimitPolicy 把借阅规则独立出来,ArrangementService 把整理逻辑独立出来,都是为了让后续的需求变更不会扩散到不相关的类里。这种思路在前三个单元里也有体现,但不是那么系统。

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

第一单元的测试是"构造特殊表达式,手算结果对比"。测试的主要目标是覆盖语法分支,比如空括号、负号嵌套、函数调用嵌套等。这个阶段基本是靠直觉和随机构造,没有很系统的方法。

第二单元(多线程)让测试变得难多了。功能正确性还好验证,但并发问题(死锁、条件竞争)很难通过构造特定输入稳定复现。这个单元我的主要策略是压力测试——大量随机输入反复跑,希望把偶发问题暴露出来。事后来看,这个方法对某些类型的并发 bug 有用,但对逻辑 bug 的覆盖不够系统。

第三单元的 JUnit 测试让我第一次认真思考"测试的目标是什么"。JML 给了每个方法精确的规格,测试要覆盖的不只是"正常情况返回正确值",还要检查异常分支、副作用、边界条件。我开始使用快照方式验证 pure 方法的副作用,这个思路比单纯对返回值 assert 要好很多。

第四单元本身没有强制要求 JUnit 测试,但这个单元的交互式评测机制(还书和取书请求动态生成)天然是一种端到端测试。我的测试策略更多是构造包含各种路径的完整场景:从预约到取书再到还书、从阅读到不归还触发罚分、逾期借阅的信用分扣减,等等。与前三个单元相比,这个单元的"测试"更像是业务场景验证,而不是技术层面的单元测试。

从整体来看,测试思维的演进大概是:覆盖语法分支 → 压力测试并发 → 规格驱动的系统测试 → 场景驱动的集成测试。每个单元的问题域不一样,测试的重心也不一样。但有一个共同的体会是:测试的质量取决于你对"什么是正确"的理解深度。把规格、规则和边界条件想清楚,测试才能真正有效。

六、课程收获

OO 课程的四个单元,每个单元都在不同角度告诉我"好的代码"意味着什么。

第一单元告诉我,清晰的数学模型可以让实现干净很多。第二单元告诉我,共享状态和同步是多线程程序里最难隐藏的问题,最好的办法是把它们限制在少数几个地方。第三单元告诉我,规格是接口和实现之间的契约,把这个契约写清楚比写完代码再补文档有价值得多。第四单元告诉我,先设计再实现不只是一个流程建议,它实际上会影响你发现问题的时机——设计阶段发现的问题比实现阶段发现的代价小得多。

比较具体的收获是:

对继承和组合的理解。第四单元里,LibraryPlace 用组合而不是继承来处理地点类的公共行为,因为不同地点的差异主要在"有没有特殊操作"而不是"是什么"。这个选择在 hw14 加入新地点类时体现出了价值。

对分层的理解。从第二单元开始,分层就是一个核心原则。但每个单元里"层"的含义不一样:多线程里是 I/O 层、调度层、执行层;图书馆系统里是请求处理层、业务逻辑层、状态管理层。分层的意义不是为了形式上的"多几个类",而是让变化局部化。

对设计文档的态度。以前觉得 UML 图是写完代码以后补的文档,这一单元强制要求先交类图,让我改变了这个看法。类图在设计阶段的价值是帮助你发现职责边界不清晰的地方,这比写完代码再发现要有用得多。

当然也有一些做得不够好的地方。方法命名上有时候不够精确,比如 arrangeBeforeOpen 内部做的事情其实包括整理、移动、为预约配书等多个步骤,方法名并不完全体现这些。如果时间充裕,这类方法应该进一步拆分。另外,对大模型生成代码的依赖有时候会让我跳过"想清楚为什么"的步骤,直接接受一个"能跑"的实现,这个习惯在后期调试时会带来额外成本。

整体来看,这门课让我对"面向对象"的理解从语法层面(封装、继承、多态)深入到了设计层面:类的职责边界在哪里、变化点应该隔离在什么位置、设计的可追踪性如何保证。这些问题没有标准答案,但能问出这些问题,已经是比较大的收获了。

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

308

社区成员

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

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