309
社区成员
发帖
与我相关
我的任务
分享第四单元的三次作业都围绕图书馆管理系统展开,需求从最初的借书、还书、预约、取书和查询,逐步扩展到阅读、归还、评分、精品书架、信用分、借阅期限和续订。相比前三个单元,这一单元最明显的变化是,代码不再只是对输入输出规则的直接翻译,而是需要先建立一个相对完整的业务模型,再让实现围绕模型展开。
所谓正向建模,我的理解是先从问题域中抽象出稳定概念,再用 UML 描述这些概念之间的关系,最后依据模型进行编码。以图书馆系统为例,题面中反复出现的核心名词包括用户、图书副本、ISBN、书架、借还处、预约处、阅览室、预约记录、移动轨迹、信用分等。这些名词并不都应该变成类,但它们提示了系统中真正需要维护的状态和关系。比如“图书副本”需要记录当前位置和移动轨迹,“用户”需要记录借阅图书、预约状态、阅读状态和信用分,“预约记录”需要记录等待、送达、取走、失效等生命周期。
两阶类图在正向建模中起到了非常关键的作用。第一阶段类图更像是设计前的架构承诺,它要求我在写代码前先回答:系统由哪些核心类组成?每个类负责什么?类之间通过什么关系协作?这个过程可以避免一开始就把所有逻辑塞进 Main 或单个管理类中。第一阶段类图不一定完美,但它提供了一个初始框架,使后续编码有方向可循。
第二阶段类图则更像是对真实实现的校准。随着三次作业需求不断增加,第一阶段中没有暴露出来的细节会逐渐浮现。例如第十四次加入阅读、归还、评分、精品书架后,图书的位置状态明显不再只有书架、预约处、借还处和用户,还需要加入阅览室和精品书架。第十五次加入信用分、借阅期限和续订后,用户类中又需要维护信用分、应还日期、逾期扣分状态等信息。第二阶段类图要求我把这些变化重新纳入模型,而不是让代码和设计文档逐渐分离。
因此,两阶类图的价值不只是为了评测,而是帮助我完成从“预期设计”到“实现设计”的闭环。第一阶类图帮助我建立初始抽象,第二阶类图帮助我检查代码是否仍然保持清晰结构。两者之间的差异也反映了我对需求理解的变化。如果第二阶类图相比第一阶类图变化过大,往往说明第一阶段建模时没有抓住核心稳定概念;如果完全没有变化,也可能说明没有认真把实现中的新增职责反馈到模型中。
最终代码整体采用了以 LibrarySystem 为调度中心的架构。Main 负责读取官方包提供的命令,随后把命令交给 LibrarySystem 处理。LibrarySystem 根据命令类型分发到不同业务方法,例如借书、预约、还书、取书、阅读、归还、评分、续订和信用分查询。具体状态则分散在对应对象中维护。
BookCopy 表示一本具体副本,它是状态图中最核心的对象。图书可能从普通书架移动到用户,从精品书架移动到阅览室,从预约处移动到用户,也可能在整理流程中从借还处回到书架。所有这些变化都可以追踪到 BookCopy 中的移动方法和移动轨迹记录。状态图中的状态与代码中的位置枚举、移动方法基本对应,这使得查询移动轨迹时可以直接从对象历史记录生成输出。
User 负责维护用户相关状态,包括已经借阅的图书、预约列表、当前阅读的图书、信用分和借阅期限。第十五次作业中,用户对象的重要性明显上升,因为很多请求是否成功不只取决于图书是否存在,还取决于用户当前是否持有同类书、是否已有未完成预约、信用分是否满足权限、图书是否逾期等。
Reservation 则承载预约生命周期。预约不是一个简单的布尔值,而是有等待、送达、取走、失效等状态。这个对象的存在让预约逻辑更清晰,尤其是在处理预约送达后保留五天、过期后扣分、过期书籍重新整理等规则时,比只在用户类中放几个字段更容易维护。
Bookshelf、BorrowReturnOffice、AppointmentOffice 分别对应图书馆的不同部门。它们的职责比较单纯,主要是维护当前位置中的图书集合,以及提供查询、添加、删除等操作。这种设计让 LibrarySystem 可以专注于业务流程编排,而不是直接操作一堆散乱集合。
从 UML 模型到代码实现的追踪关系可以概括为:
| 模型元素 | 代码对应 | 追踪关系 |
|---|---|---|
| 类图中的用户类 | User | 维护借阅、预约、阅读、信用分 |
| 类图中的图书副本类 | BookCopy | 维护位置、状态变化、移动轨迹 |
| 类图中的预约类 | Reservation | 维护预约生命周期 |
| 类图中的部门类 | Bookshelf、BorrowReturnOffice、AppointmentOffice | 维护不同位置的图书 |
| 状态图中的状态迁移 | BookCopy 的移动方法 | 对应图书位置变化 |
| 顺序图中的预约场景 | handleOrder、moveForReserve | 对应用户预约与整理送书 |
| 顺序图中的取书场景 | handlePick | 对应预约处到用户的转移 |
当然,最终代码和 UML 模型并不是完全一一对应的。比如 LibrarySystem 在代码中承担了较多调度职责,许多流程判断集中在这个类中;而 UML 模型中看起来职责可能更均匀。这说明实际编码时,为了控制复杂度和输出顺序,有些编排逻辑会自然集中到系统层。关键在于,这种集中不能破坏领域对象的核心职责。图书自己的移动轨迹仍由 BookCopy 维护,用户自己的信用分和持有状态仍由 User 维护,预约生命周期仍由 Reservation 维护,这样整体结构仍然可追踪。
这次修复 bug 的过程也体现了模型追踪的重要性。原本代码把“借书”和“取预约书”都复用了 canBorrow 判断,但题面中二者规则并不完全相同。借书需要信用分大于 80,而取已经送达的预约书只需要满足持有数量限制。这个问题本质上是模型中没有区分“借阅权限”和“持有合法性”。修复后增加了只检查持有数量限制的方法,使代码职责更接近题面模型。
在第四单元中使用大模型辅助建模,我最大的体会是:大模型很适合帮助整理复杂需求,但不能直接把它当成最终架构设计者。复杂题面中有大量相似但不相同的流程,例如借书、取书、阅读都会让图书离开书架或预约处,但它们的权限限制、移动路径和后续影响并不一样。如果提示不够精确,大模型很容易把这些流程合并过度,产生看似简洁但语义错误的设计。
比较有效的使用方式是先让大模型做需求抽取。比如要求它列出所有实体、所有状态、所有操作、所有权限限制、所有信用分变化规则。这样可以帮助我快速建立题面的全局图景。接着,再让它把“需求规则”映射到“类、字段、方法”,形成一张追踪表。比如“预约后不取扣 15 分”应该对应 Reservation 的失效判断和 User 的信用分更新;“阅读后不归还扣 10 分”应该对应闭馆流程;“精品书架”应该对应 BookInfo 的评分统计和开馆前整理。
在架构设计阶段,我觉得应该这样引导大模型:
大模型在复杂场景中的另一个价值是帮助构造边界用例。比如这次取书 bug,如果只跑官方样例不一定暴露,但构造“用户预约成功后信用分下降,然后再取书”的场景,就能发现复用 canBorrow 是错误的。大模型可以帮助提出这种跨日期、跨状态的测试思路。
但大模型也有局限。它可能会默认“复用越多越好”,或者忽略题面中非常细的时间点,例如“闭馆后立即扣分”“保留期第 5 天闭馆后失效”“开馆前整理后借还处不应有书”。因此,使用大模型时不能只问“怎么设计”,还要追问“哪些规则最容易被写错”“哪些状态变化发生在开馆前,哪些发生在闭馆后”“哪些判断只适用于某个流程”。我认为这才是复杂场景下比较可靠的人机协作方式。
第一单元是表达式解析和化简。最开始我的架构设计思维还比较朴素,主要是把输入解析、表达式结构、求导和化简拆成不同类。那时我更关注“如何把功能做出来”,比如 Lexer 负责分词,Parser 负责递归下降,表达式树负责计算和输出。这个阶段让我第一次体会到对象可以承载递归结构,而不是所有逻辑都写成字符串处理。
第二单元是电梯多线程。这个单元让我意识到,架构不仅是静态类划分,更重要的是对象之间的协作协议。调度器、电梯线程、请求队列之间如何通信,什么时候等待,什么时候唤醒,什么时候结束,都需要设计清楚。相比第一单元,第二单元的难点不再是单个函数是否正确,而是多个对象在时间维度上能否稳定协作。
第三单元是 JML 规格。这个单元让我开始从“实现视角”转向“契约视角”。JML 中的前置条件、后置条件、不变量和异常要求,让我意识到一个方法的意义不只是它内部怎么写,还包括调用前后必须满足什么语义。这个阶段的架构设计更关注数据结构和规格之间的一致性。例如选择什么容器,不只是性能问题,也会影响异常判断、关系维护和查询行为。
第四单元是 UML 正向建模。相比前三个单元,它更强调从问题域出发进行抽象。类图描述静态结构,状态图描述对象生命周期,顺序图描述对象交互流程。通过这一单元,我逐渐形成了更完整的设计思路:先找稳定概念,再找状态变化,再找对象协作,最后才是具体代码实现。
回顾四个单元,我的架构思维大致经历了这样的变化:第一单元关注“怎么拆功能”,第二单元关注“怎么协作”,第三单元关注“怎么满足规格”,第四单元关注“怎么先建模再实现”。这四个层次叠加起来,才比较接近真正的面向对象设计。
第一单元的测试主要是输入输出对拍。表达式作业适合随机生成表达式,再用不同实现或数学工具比较结果是否等价。我当时关注的是括号、符号、指数、嵌套、化简边界等问题。测试目标比较明确,就是表达式是否解析正确、输出是否等价。
第二单元进入多线程后,测试思维发生了明显变化。电梯程序的输出可能不唯一,因此不能只比较固定答案,而要检查输出是否合法。更重要的是,并发程序可能出现死锁、线程提前结束、请求丢失、等待时间异常等问题。这个单元让我意识到,测试不仅要验证结果,也要验证过程是否满足约束。
第三单元的测试围绕 JML 规格展开。由于规格非常明确,测试可以更有针对性地检查每个方法的前置条件、后置条件和异常行为。我会更关注边界数据、重复元素、不存在元素、异常计数、关系一致性等。这个阶段的测试更像是在验证契约,而不是简单跑样例。
第四单元的测试最强调状态机和历史轨迹。图书馆系统中,当前命令是否成功往往取决于之前很多天发生过什么。比如用户是否已经有未完成预约,图书是否已经送到预约处,预约是否过期,用户信用分是否下降,图书是否在普通书架还是精品书架。测试必须构造连续场景,而不能只看单条命令。
我在第四单元逐渐形成了几类测试思路:
| 测试类型 | 典型场景 |
|---|---|
| 单流程测试 | 借书、还书、预约、取书、阅读、归还 |
| 跨日期测试 | 预约保留期、借阅期限、续订后归还 |
| 信用分测试 | 按时还书加分、逾期扣分、阅读未还扣分、预约未取扣分 |
| 状态迁移测试 | 查询图书从书架到用户、借还处、预约处、阅览室的轨迹 |
| 权限边界测试 | 信用分为 0、40、80 时不同操作是否允许 |
| 整理流程测试 | 开馆前和闭馆后移动是否符合约束 |
这说明测试思维也从“样例驱动”逐渐变成“规格驱动”和“状态驱动”。越复杂的系统,越需要围绕状态转移和不变量设计测试。
OO 课程最大的收获,是让我逐渐理解“面向对象”不是简单地多写几个类,而是用对象承载稳定概念,用状态表达业务规则,用协作完成复杂流程。一个类是否合理,不取决于它名字是否好看,而取决于它是否维护了清晰的职责和不变量。
第一单元让我理解了递归结构和抽象语法树,第二单元让我理解了并发协作和调度,第三单元让我理解了规格和契约,第四单元让我理解了模型和实现之间的追踪关系。四个单元合在一起,实际上构成了一条从代码能力到设计能力的训练路径。
这门课也让我更重视重构。很多时候,第一次写出来的代码并不是最合理的。随着需求增加,如果不及时调整结构,就会出现大量重复判断和隐藏 bug。比如图书馆作业中,如果不区分借书权限和取书后的持有合法性,就会因为错误复用导致强测失败。重构不是为了让代码显得复杂,而是为了让概念更准确。
另一个重要收获是测试意识。以前我可能认为通过样例就差不多了,但 OO 作业让我认识到样例只能说明最基本情况。真正容易出错的是边界状态、跨天状态、异常状态和多个规则叠加的场景。好的测试应该尽量覆盖状态变化路径,而不是只覆盖输入格式。
最后,UML 建模让我意识到,设计文档如果只是代码写完后的形式化补充,价值会很低;但如果它能参与编码前的思考,并在编码后反过来校准实现,就能真正帮助理解系统。第四单元虽然工作量不小,但它让我更清楚地看到:复杂系统需要模型,模型需要实现验证,实现又需要测试反馈。三者结合起来,才是比较完整的软件开发过程。