308
社区成员
发帖
与我相关
我的任务
分享本单元在图书管理系统上实践正向建模:从需求描述出发,先建立 UML 模型,再据此实现代码,并通过官方包提供的 @Trigger、@SendMessage 等注解,使模型与实现保持可追踪的一致关系。
本单元的开发流程可概括为:
LibraryManager 为控制中心,按类图职责划分实现各类。@Trigger、@SendMessage 等注解与官方工具校验模型与代码的一致性。本单元的两阶架构设计指建模工作在时间上的两次展开,而非分析类图与设计类图两种抽象层次。两次设计围绕同一份架构类图迭代,状态图与顺序图在第二次才补全:
| 阶段 | 时机 | 产出 | 作用 |
|---|---|---|---|
| 第一次 | 写代码之前 | 架构设计类图 | 在编码前完成详细结构设计,明确类职责、关键属性与方法,作为实现依据 |
| 第二次 | 写完代码之后 | 微调后的类图 + 状态图 + 顺序图 | 根据实现修正类图细节,并刻画动态行为:状态转移、对象协作 |
第一次架构设计的核心目标是先想清楚再动手。此时只画类图,不急于画状态图和顺序图,重点包括:
Book、User、各书架与各部门 Bookshelf、TreasuredBookshelf、BorrowandreturnDesk、ReservationDesk、ReadingRoom,以及控制类 LibraryManager、工具类 LibraryManagerTools、辅助类 ReservationRequest、Rating 等。LibraryManager.run()、handleBorrow()、processSorting(),以及 moveToUser() 等移动方法的大致分工;整理子流程暂可落在 LibraryManagerTools 的静态方法上。holdings、图书位置状态 state、信用分 credit、借阅期限 dueDate、预约信息 reservedFor、reserveUntil 等字段归属哪一类;各 Desk/Bookshelf 与 LibraryManager 的组合关系如何表达。User.canHold() 中体现,开馆请求与闭馆整理分离应在 handleRequest() 与 processSorting() 中体现,避免写代码时临时拍脑袋拆类。第一次设计无状态图和顺序图,我感觉是因为此时对动态交互的细节尚未经过实现验证;先把静态结构定稳,可减少编码过程中的大规模重构。
代码写完后,对架构的理解往往比设计初期更具体。第二次架构设计在此基础上完成三件事:
processOverduePenalty()、handleExpiredReservations() 等下沉到 LibraryManagerTools;或补充 MoveAction 接口以统一整理回调——这些调整反映到更新后的类图中。moveToUser()、moveBetweenPlaces() 等方法落实;此时为 Book 补画状态图,并用 @Trigger 注解将转移与具体方法绑定,使「哪些方法引起哪些状态变化」一目了然。@SendMessage 标注 Main → LibraryManager 及各场所之间的消息,与类图中的方法调用对应。第二次设计的价值在于:类图从设计意图收敛为与代码一致的架构记录;状态图、顺序图则把第一次类图中隐含的动态行为显式化,形成完整的 UML 制品集。
系统采用以 LibraryManager 为中心的分层式架构:
Main
└── LibraryManager:控制层,命令循环、请求处理、整理编排
├── Book / User / Rating / ReservationRequest:领域实体
├── Bookshelf / TreasuredBookshelf:书架容器
├── BorrowandreturnDesk / ReservationDesk / ReadingRoom:业务场所
├── LibraryManagerTools:整理与规则工具
└── 官方包类型:LibraryBookId、LibraryBookState、LibraryTrace 等
Main 仅创建 LibraryManager 并调用 run()。LibraryManager 持有全部书籍与用户映射、各场所实例,以及预约队列、评分表、移动轨迹等全局索引。Book 承载单本副本的状态:位置、持有人、应还日、预约保留信息;User 承载信用分、持有集、预约与阅读状态。LibraryBookId 集合,提供 add()、remove()、findAvailable() 等操作。LibraryManagerTools 提供静态方法,处理整理子流程与跨实体规则:逾期、精品书架判定等。开馆请求处理:run() 循环读取 LibraryCommand,按类型分发至 handleBorrow()、handleOrder()、handlePick()、handleRead()、handleReturn()、handleRestore()、handleGrade()、handleRenew()、handleQuery() 等。每个 handler 完成校验 → 状态变更 → 输出响应。
整理流程:processSorting() 在 OPEN/CLOSE 时触发,顺序执行:逾期罚分 → 闭馆时未归还阅读罚分 → 预约失效处理 → 待预约分配 → 精品/普通书架重平衡 → 开馆前借还处清空 → 阅览室清空。移动通过 moveBetweenPlaces() 统一完成,保证轨迹记录与容器同步更新。
状态与移动:图书位置变更统一经过带 @Trigger 注解的方法 moveToUser()、moveToReadingRoom()、moveToBorrowDesk()、moveBetweenPlaces() 等,与状态图中 bs/tbs/ao/bro/rr/user 之间的转移一一对应。
本单元 UML 与源码通过官方包的注解机制建立双向可追踪关系:代码中的 @Trigger、@SendMessage 标注状态转移与消息发送,工具据此生成类图、状态图、顺序图中的对应元素。
| UML 类 | 源码类 | 追踪关系 |
|---|---|---|
| Main | Main.java | 操作 main() 与关联 creates → LibraryManager 一致 |
| LibraryManager | LibraryManager.java | 属性 books、users、各 desk 与全部 handler、processSorting() 操作一致 |
| Book | Book.java | 8 个私有属性 id、state、holderUser、dueDate 等与 getter/setter 完全对应 |
| User | User.java | 信用分常量、holdings、预约与阅读字段及 canHold()、canReserve() 等方法一致 |
| Bookshelf / TreasuredBookshelf | 同名源文件 | findAvailable() 操作与 UML 一致 |
| BorrowandreturnDesk / ReservationDesk / ReadingRoom | 同名源文件 | 容器职责一致 |
| Rating / ReservationRequest | 同名源文件 | 一一对应 |
| LibraryManagerTools | LibraryManagerTools.java | 静态整理与规则方法 + 内部类 MoveAction |
| LibraryManagerTools.MoveAction | 接口 MoveAction | 函数式接口,用于整理回调 |
组合关联:UML 中 LibraryManager 以组合关系持有各 Desk/Bookshelf,与代码中 private final Bookshelf bookshelf = new Bookshelf() 等实例字段一致。
第一次类图与第二次微调之间的变化:LibraryManagerTools、MoveAction 等往往在编码过程中才从 LibraryManager 中拆分或补充;第二次类图记录的是实现后的最终职责划分,而非第一次设计时就能完全预见。
状态图以 Book 的位置为状态,节点为 bs、tbs、ao、bro、rr、user,与 LibraryBookState 枚举及 @Trigger 注解严格对应:
| 状态图转移名 | 代码方法 | Trigger 注解 |
|---|---|---|
| moveToUser | moveToUser() | from bs/tbs/ao → user |
| moveToReadingRoom | moveToReadingRoom() | from bs/tbs → rr |
| moveToBorrowDesk | moveToBorrowDesk() | from user → bro |
| moveToBorrowDeskFromReading | moveToBorrowDeskFromReading() | from rr → bro |
| moveBetweenPlaces | moveBetweenPlaces() | 整理时五处之间的多种转移 |
initInventory() 在状态图中对应从初始到 bs 的建库转移。状态图不单独建模 Book 的 dueDate、reservedFor 等子状态,这些信息作为状态内的附加属性存在于类图中——这是常见的位置状态机 + 属性约束组合模式。
顺序图 SequenceDiagram1 刻画 Main 向 LibraryManager 发送消息启动系统,以及开馆、预约、取书、整理等典型场景中的对象协作。代码中 @SendMessage 及各处 from/to 标注,如 bs→user、ao→user,与顺序图中的消息箭头对应。
顺序图侧重动态协作,类图侧重静态结构,状态图侧重单对象生命周期——三者通过同名方法名 handleOrder()、processSorting()、moveToUser() 形成垂直追踪链:顺序图消息 → 控制类方法 → 状态图转移/类图操作。
引导大模型时明确要求同时产出:
LibraryBookState、LibraryReqCmd.Type 的映射表。将大模型第一版类图/代码与需求条文逐条对照,把不匹配项作为下一轮输入,例如:需求说预约失效当日闭馆后扣 15 分,请说明应在 handleExpiredReservations() 还是闭馆 processSorting() 中实现,并更新类图职责。差异驱动的迭代比泛泛的请优化更能收敛到正确架构。
大模型在以下方面仍需人工把关:
从 OOpre 过渡到正课第一单元,我开始更主动地做功能拆分和职责划分。表达式求值天然适合递归下降:把字符串拆成项、因子等对象,每层只处理本层语法,上层通过组合完成整体语义。入口类负责读入输出,解析与求值逻辑下沉到表达式树节点。
电梯场景把设计重心从静态分层转向模块协作与并发协同。电梯、请求、调度器、楼层状态各自是活跃对象,彼此通过共享状态耦合;调度策略需要从门开关、方向判断中抽离,生产者—消费者式的请求流转也贯穿其中。实现过程中我系统接触了锁、同步块、等待与唤醒,明白线程边界就是设计边界,并发不变式应在架构阶段想清楚,而不是靠事后加锁试错。
JML 促使我在写实现前先想清楚模块对外承诺什么。前置条件、后置条件、不变式把口头约定变成可检查的规格,对人员、关系、消息、群组等操作尤其适用。我的体会是:JML 的价值不在于逐行照着规格填代码,而在于项目初期就逼问系统整体架构——这个方法成功改变什么、失败保持什么、类级别有哪些约束。
本单元强调架构设计对工程开发的支撑作用:需求涉及开馆请求、闭馆整理、信用分、预约保留期等多条规则交叉,需要自行划分模块职责并保证整体协同。我采用以 LibraryManager 为中心、场所类管容器、Book 承载位置与借阅状态的方案;两阶架构设计,加上 @Trigger、@SendMessage 追踪,使图评测能清晰展示系统结构与类间交互,并持续校验设计与实现是否一致。
OOpre 里的 JUnit 主要验证单个方法、追求代码覆盖率;第一单元实现表达式化简后,单靠手工样例已不够,我转向自作评测机,用随机输入长时间跑测来逼近正确性。测试仍偏靠体量和复杂度碰运气,但已经建立了边界意识:嵌套括号、优先级、单元素表达式等用例往往对应语法某一层的 bug。
第二单元电梯调度的 bug 常具随机性——同样输入在不同调度时机下走不同路径。我开始做多线程交错测试和压力测试,模拟大量请求观察响应与稳定性,并用日志、状态快照辅助复现死锁、请求丢失等问题。这一单元让我认识到:并发场景下要验证的是状态机在各种交错下仍满足不变式,而不只是终态输出对不对。
第三单元测试与 JML 契约对齐:合法调用验证后置条件,非法调用验证是否按约定拒绝或保持状态。我用手工构造极端图(空图、完全图、重复边等),加随机生成数据应对中测 JUnit。规格化让测试有了明确靶心,不必与实现行为盲目对齐。
首先是大模型辅助开发,大模型工具能提高效率,但不能替代对需求和架构的理解。其次是测试思维的提升从中测、强测到互测,测试策略从随机对拍、并发压测,对测试的理解逐渐加深。OO课程以 OOpre 的 Java 与面向对象思维作先导,再以一学期理论课和四次大作业完成设计与构造的入门:从会拆分、会协作、会约束到会建模。对我而言,这更像一段方法的积累,面向对象的路,还在后面继续走。