【BUAA-OO-2026】第四单元:UML建模语言

【BUAA-OO-2026】第四单元:UML建模语言
文章目录前言正向建模与两阶类图本单元架构设计第十三次作业第十四次作业第十五次作业最终架构代码设计与 UML 模型的追踪关系类图与代码的对应状态图与代码的对应顺序图与代码的对应追踪关系总结大模型辅助正向建模的体验架构设计思维的演进第一单元从表达式层次中理解对象第二单元从对象协作中理解并发系统第三单元从规格中理解状态维护第四单元从 UML 中理解正向建模测试思维的演进第一单元手造样例和 IDEA 调试第二单元评测机和批量对拍第三单元JUnit 和规格检查第四单元业务流程和模型一致性课程收获参考资料前言面向对象第四单元的主题是“UML建模语言”。和前三个单元相比本单元的开发体验有一个很明显的变化我不再是写完代码后再回头总结架构而是需要在编码前先用 UML 类图对系统进行正向建模并在实现过程中不断校正模型使最终代码和 UML 模型保持一致。本单元三次作业都以图书馆管理系统为背景。经过三次迭代之后最终版代码共22个类项目结构如下所示。src ├── Main.java ├── LibraryManager.java ├── BookManager.java ├── UserManager.java ├── OrderManager.java ├── ArrangeService.java ├── BorrowLimitPolicy.java ├── Book.java ├── BookCopy.java ├── BookIsbn.java ├── MovingTrace.java ├── User.java ├── Order.java ├── Location.java ├── Bookshelf.java ├── TreasuredBookshelf.java ├── BorrowAndReturnOffice.java ├── AppointmentOffice.java ├── ReadingRoom.java ├── BookCategory.java ├── BookLocation.java └── ArrangeType.java从代码规模来看第四单元并没有像第二单元那样需要处理复杂的并发问题也不像第三单元那样需要在性能上做大量缓存和增量维护。但它对“先设计再实现”的要求更强尤其是两阶类图、状态图和顺序图的引入让我对架构设计和代码实现之间的对应关系有了更直接的感受。正向建模与两阶类图我理解的正向建模与开发是指从需求出发先抽象系统中的对象、关系和行为再依据模型实现代码。也就是说UML 在这里不是“写完代码后的截图”而是编码之前用于思考和约束设计的工具。本届第四单元采用了两阶段提交的形式。每次作业第一阶段提交一阶类图也就是在具体编码之前完成的初始设计第二阶段提交代码和对应的二阶类图也就是在实现过程中修正后的最终设计。两阶类图对我的帮助主要体现在三个方面。一阶类图强迫我在写代码前先进行领域抽象虽然可以在第一阶段就把代码写好然后逆向生成类图。以图书馆系统为例如果直接上手写代码很容易把所有逻辑都堆在一个LibraryManager中。但在画类图时我必须先思考“图书副本”“用户”“预约单”“图书位置”“整理服务”等概念是否应该独立存在。最终我将系统拆成了BookCopy、User、Order、Location、各类Manager和ArrangeService等对象这些对象也基本延续到了最终代码中。二阶类图给了模型回填和修正的机会。一阶类图毕竟是在编码前完成的很多细节只有真正实现时才会暴露出来。比如在第十四次作业中新增阅览室和精品书架后原本的Location抽象就变得很自然Bookshelf、TreasuredBookshelf、BorrowAndReturnOffice、AppointmentOffice、ReadingRoom都可以看成持有若干图书副本的位置。此时二阶类图的作用不是推翻原来的设计而是把这些在实现中变得更清楚的关系重新表达出来。两阶类图让我能更清楚地检查“模型是否真的指导了代码”。如果一阶类图和二阶类图相差很大说明初始设计可能没有充分理解需求如果二阶类图和代码相差很大说明 UML 只是事后补图没有形成真正的设计约束。我的三次作业中类级别结构基本比较稳定更多变化发生在方法和字段层面这说明一开始的领域划分大体是合理的。本单元架构设计【三次迭代回顾】第十三次实现基础图书馆系统支持借书、还书、预约、取书和查询。第十四次新增阅读、归还、评分和精品书架机制并引入状态图。第十五次新增用户信用分、借阅期限和续借机制并引入顺序图。第十三次作业第十三次作业是本单元的起点主要目标是搭建图书馆系统的基础领域模型。此时需要处理的业务包括借书、还书、预约、取书、查询和开闭馆整理。我在这一版中采用了一个比较直接的分层结构LibraryManager负责读入命令、调用各个模块并输出结果BookManager负责管理所有图书副本UserManager负责管理用户OrderManager负责管理预约ArrangeService负责开馆前和闭馆后的整理流程。对于业务中的实体则分别使用BookCopy、BookIsbn、User、Order和MovingTrace表示。这一版中比较关键的设计是区分BookIsbn和BookCopy。BookIsbn表示某一种书而BookCopy表示具体的一本副本。借书、还书、预约取书和查询轨迹等操作实际改变的是某个副本的位置因此BookCopy需要维护自己的BookLocation和MovingTrace而借阅限制、评分、是否精品等规则则更多和 ISBN 层面的图书种类有关。第十四次作业第十四次作业新增了阅读、归还、评分和精品书架。相比第十三次作业这一轮没有改变基础架构而是在原有结构上增加了两个新的地点类ReadingRoom和TreasuredBookshelf。这一轮让我比较明显地感受到抽象的价值。由于第十三次作业中已经把“地点”抽象成了Location新增地点时只需要让ReadingRoom和TreasuredBookshelf继承Location并补充少量特定方法即可。比如ReadingRoom负责记录正在阅览室中的图书TreasuredBookshelf则表示评分达到要求后的精品图书所在位置。同时评分机制被放在BookManager中维护。BookManager通过scoreSums、scoreCounts和gradeScores记录每个 ISBN 的评分情况ArrangeService在开馆前整理时根据BookManager.isTreasuredBook()决定图书应该回到普通书架还是精品书架。这样评分规则没有侵入Location子类地点类仍然只负责“持有图书”这一件事。第十五次作业第十五次作业新增用户信用分、借阅期限和续借操作。和第十四次相比这一轮最终没有新增核心类而是扩展了已有对象的状态和规则。在User中我加入了creditScore并用increaseCreditScore()和decreaseCreditScore()控制信用分上下界。用户是否能借阅、预约或阅读也可以通过canBorrowByCredit()、canRead()等方法进行判断。在BookCopy中我加入了dueDate和overdueDeducted用于表示图书的应还日期以及是否已经因为逾期扣过分。借书和取书成功时调用startBorrow()设置借阅期限续借时调用renew()开馆前和还书时则通过isOverdue()、needsOverduePenalty()判断是否需要扣除信用分。在BorrowLimitPolicy中我把信用分限制和原有的借阅数量限制组合在一起。这样LibraryManager不需要直接知道 B 类、C 类图书的所有规则也不需要到处手写信用分判断而是通过策略类统一判断canBorrow()、canOrder()和canPick()。这一轮的体会是迭代不一定意味着不断加类。有些需求适合新增对象比如第十四次的阅览室和精品书架有些需求更适合补充已有对象的状态比如第十五次的信用分和借阅期限。判断的关键在于新需求有没有稳定独立的职责而不是它看起来“新不新”。最终架构最终架构可以大致分成五层层次主要类职责入口协调层Main,LibraryManager读取官方包命令分发请求组织输出管理层BookManager,UserManager,OrderManager管理图书、用户、预约等核心资源实体层BookCopy,BookIsbn,User,Order,MovingTrace保存业务对象自身状态地点层Location,Bookshelf,TreasuredBookshelf,BorrowAndReturnOffice,AppointmentOffice,ReadingRoom表示图书所在位置支持图书进出规则服务层BorrowLimitPolicy,ArrangeService封装借阅限制和整理策略其中LibraryManager是整个系统的门面和控制中心。它并不保存所有细节状态而是持有各个管理器和地点对象在处理请求时协调它们之间的交互。例如借书时LibraryManager会先通过UserManager获取用户再通过BorrowLimitPolicy判断是否满足借阅限制之后从Bookshelf或TreasuredBookshelf中取出可借图书最后更新BookCopy和User的状态。BookCopy是图书副本状态的核心承载者。它记录当前所在位置、预约信息、持有者、阅览者、借阅期限和移动轨迹。由于查询历史轨迹和状态图都围绕图书副本展开把这些状态集中在BookCopy中能够保证图书位置变化有统一入口。Location及其子类则把“某处持有哪些书”这一概念抽象出来。普通书架、精品书架、借还处、预约处和阅览室虽然业务规则不同但它们都需要支持添加图书、移除图书、按 ISBN 查询图书等基本操作。通过继承Location这些共性逻辑可以复用新增地点时也比较自然。ArrangeService是本单元中我比较满意的一个拆分。开馆前和闭馆后的整理流程涉及预约失效、阅览室归还、满足预约、借还处上架、普通书架和精品书架之间移动等多个步骤。如果把这些逻辑都塞进LibraryManager控制类会迅速膨胀。因此我将整理流程单独放入ArrangeService使LibraryManager更像入口和协调者而不是所有业务细节的容器。代码设计与 UML 模型的追踪关系本单元的一个重点是分析代码设计和 UML 模型设计之间的追踪关系。我的理解是追踪关系并不要求 UML 中出现代码里的每一行实现细节而是要求核心类、核心关系和核心行为能够在代码中找到稳定对应。类图与代码的对应UML 模型元素代码对应说明LibraryManagerLibraryManager.java系统入口控制类聚合各管理器和地点对象BookManagerBookManager.java维护所有副本、按 ISBN 索引副本、维护评分UserManagerUserManager.java维护用户集合并负责批量结算用户状态OrderManagerOrderManager.java维护预约集合和当前有效预约BookCopyBookCopy.java表示具体图书副本保存位置、预约、借阅期限和轨迹UserUser.java保存用户已借图书、当前预约、当前阅读图书和信用分OrderOrder.java表示预约请求及其是否已分配图书Location继承结构Location及其子类表示普通书架、精品书架、借还处、预约处、阅览室BorrowLimitPolicyBorrowLimitPolicy.java封装借阅、预约、取书限制ArrangeServiceArrangeService.java封装开馆前和闭馆后的整理流程可以看出最终代码中的核心类基本都能在类图中找到对应。类图中最重要的关系是LibraryManager对各管理器和地点对象的聚合关系、Location到各具体地点的继承关系以及BookCopy与BookIsbn、Order、MovingTrace之间的关联关系。状态图与代码的对应状态图主要描述图书副本的位置变化。在我的代码中这部分由BookCopy和BookLocation共同承担。图书副本的主要状态包括OnBookshelf位于普通书架。OnTreasuredBookshelf位于精品书架。InBorrowReturnOffice位于借还处。InAppointmentOffice位于预约处。InReadingRoom位于阅览室。BorrowedByUser被用户持有。这些状态转换在代码中对应BookCopy的一组moveTo*()方法moveToBookshelf()moveToTreasuredBookshelf()moveToAppointmentOffice()moveToBorrowReturnOffice()moveToReadingRoom()moveToUser()每个方法都通过Trigger注解和状态图中的迁移对应。真正执行状态变化的是recordMove()它会记录一条MovingTrace并更新location。也就是说状态图中的迁移不是孤立存在的它在代码中对应到一个明确的方法入口和一个明确的状态字段。这一点对我很有启发。以前我可能只会在代码里用一个枚举表示当前位置但状态图要求我进一步思考哪些状态是合法的哪些迁移是允许的一次业务操作会触发怎样的状态变化这使“图书位置变化”从一个普通字段更新变成了一个可以被单独检查和追踪的对象行为。顺序图与代码的对应顺序图主要描述对象之间的交互路径。第十五次作业中我主要围绕预约和取书两个场景理解顺序图。以预约为例用户发起ordered请求后LibraryManager先获取User和BookIsbn再通过OrderManager判断该用户是否已有有效预约通过BorrowLimitPolicy判断是否满足预约限制。如果预约成功OrderManager创建OrderUser记录当前有效预约。以取书为例用户发起picked请求后LibraryManager先从AppointmentOffice中查找为该用户预留的图书再通过BorrowLimitPolicy判断取书后是否满足借阅限制。若取书成功则需要同时更新AppointmentOffice、OrderManager、User和BookCopy的状态。代码中用于顺序图检查的SendMessage方法虽然本身不承载复杂逻辑但它提醒我顺序图关注的是“谁向谁发送消息”。相比类图的静态结构顺序图更强调跨对象协作相比状态图的单对象状态变化顺序图更强调一次业务流程中的对象调用顺序。追踪关系总结总体来看我最终的代码设计和 UML 模型设计之间具有比较稳定的追踪关系。类图对应静态架构状态图对应BookCopy的位置变化顺序图对应预约和取书中的对象协作。二阶类图和最终代码之间没有出现大规模结构偏移说明一阶类图阶段的领域拆分基本有效。当然这个架构仍然有可以改进的地方。比如LibraryManager中仍然包含较多handleXxx()方法控制逻辑偏集中如果作业继续迭代也许可以将借书、还书、阅读等流程进一步抽成命令处理器。但在本单元的规模下我认为当前设计在简洁性和可扩展性之间取得了一个可以接受的平衡。大模型辅助正向建模的体验本单元中我使用大模型辅助理解需求和讨论架构但没有完全让大模型直接生成代码。一个比较明显的体验是大模型在做架构设计时往往倾向于把系统拆得很细生成很多看起来很“专业”的类但其中一部分未必真的有必要。以第四单元的图书馆系统为例如果直接让大模型“设计一个图书馆管理系统”它很可能会继续拆出BorrowService、ReturnService、ReadService、CreditService、TraceService等大量服务类。这样的拆分在大型工程里可能有意义但在本单元的作业规模下过多的类反而会增加维护成本。我的最终实现只保留了ArrangeService和BorrowLimitPolicy两个偏服务/策略性质的类因为前者承载跨地点的整理流程后者承载多处复用的借阅限制其余流程仍由LibraryManager统一协调。因此我觉得在复杂场景中引导大模型完成架构设计关键不是让它“尽可能完整”而是让它“有边界地完整”。比较有效的方式包括先要求输出类-职责-状态-协作表。不要一开始就让大模型生成代码而是先让它说明每个类为什么存在、保存什么状态、和哪些类交互。限制新增类的条件。例如规定“一个新类必须拥有稳定状态或者承载被多个流程复用的规则否则优先合并到已有类中”。要求做简化审查。让大模型在给出初稿后再指出哪些类可能是过度设计哪些方法可以放回已有对象。用不变量约束架构。比如图书馆系统中可以明确要求一本图书副本任意时刻只能位于一个地点图书总数不能变化用户同一时刻的 B/C 类借阅限制必须始终满足。要求模型和代码双向追踪。让大模型说明类图中的每个核心类将落到哪些代码文件状态图中的每个状态迁移将落到哪些方法。可以使用类似下面的 prompt 来约束大模型请先根据需求给出类-职责-状态-协作表不要直接写代码。 每个新增类必须说明为什么不能合并到已有类中。 优先保持架构简单避免只被一个方法调用的一次性服务类。 最后列出这个设计需要维护的业务不变量以及这些不变量应该由哪些类负责。最近我还看到一个 GitHub 项目 andrej-karpathy-skills。这个项目给我的启发是与其每次临时手搓 prompt不如把对 coding agent 的要求沉淀成规则文档例如保持简单、少做无关改动、先澄清需求、重视验证等。我的理解是这类规则并不能完全解决大模型过度设计的问题但可以显著降低“AI 一上来就搭一个很大架子”的概率。也就是说大模型更像是一个能力很强但需要约束的合作者。它能帮助我补充视角、发现遗漏但最终的架构边界仍然需要由开发者把握。架构设计思维的演进四个单元下来我对架构设计的理解经历了一个逐步变化的过程。第一单元从表达式层次中理解对象第一单元主要是表达式解析和化简。那时我最重要的收获是理解了层次化设计输入字符串先经过预处理和词法分析再由Parser按文法解析成表达式结构最后转化为自定义多项式进行运算和输出。这一单元让我第一次感受到复杂问题可以通过拆层来降低难度。Expr、Term、Factor等对象不是随便拆出来的而是来自文法本身的层次。也就是说对象设计可以直接反映问题领域的结构。第二单元从对象协作中理解并发系统第二单元的重点是多线程电梯调度。和第一单元相比它不再只是处理一个静态输入而是要让多个线程围绕共享数据进行协作。这一单元让我开始关注对象之间的关系。RequestTable不只是一个容器它还是生产者和消费者之间的同步点DispatchThread不只是转发请求它还决定请求和电梯之间的分配关系Elevator也不只是保存状态而是一个持续运行的工作线程。也正是在这一单元我开始真正意识到封装不仅是把字段设成private更是把共享状态和同步策略一起封装起来。第三单元从规格中理解状态维护第三单元的主题是规格化设计。官方包已经给出了接口和 JML 规格我需要做的是读懂规格并选择合适的数据结构实现它。这一单元让我对“状态维护”有了更深的理解。比如queryMutualFollowingSum()可以由关注和取关操作增量维护queryMostPopularVideo()可以通过按类型分组和脏标记缓存优化assignable的变化则提示我哪些旧方法的副作用需要扩展。架构设计不再只是“类怎么拆”还包括“哪些状态应该被谁维护以及什么时候更新”。第四单元从 UML 中理解正向建模第四单元进一步把“设计”前置到了编码之前。类图让我提前思考对象和关系状态图让我明确图书副本的合法状态迁移顺序图让我关注对象之间的调用顺序。我觉得这是四个单元中架构设计思维最明显的一次转变从“写完后总结架构”变成“先用模型约束实现”。虽然画图本身有一些工具操作成本但它确实能迫使我在编码前回答一些重要问题系统里有哪些核心对象对象之间是组合、依赖还是继承某个状态变化应该由哪个对象负责一次业务流程中消息应该如何传递测试思维的演进四个单元中我的测试方式也在不断变化。第一单元手造样例和 IDEA 调试第一单元中我主要依靠 IDEA 的调试功能和手工构造数据点进行测试。测试重点是文法边界、括号嵌套、递归函数、选择表达式、输出格式等。那时的测试比较依赖人工经验能够覆盖一些显然的极端情况但系统性不足。第二单元评测机和批量对拍第二单元的输出和并发行为都比较复杂单靠人工看输出很难判断程序是否正确。因此我搭建了oo-tester通过数据生成、批量运行和 checker 检查来辅助测试。这一单元让我意识到复杂系统的测试需要自动化。尤其是多线程程序中很多 bug 不是每次都稳定复现如果没有批量运行和回归测试很容易出现“本地跑过一次就以为没问题”的错觉。第三单元JUnit 和规格检查第三单元开始使用 JUnit 测试。由于 JML 已经给出了方法规格测试也可以更系统地围绕规格展开检查返回值是否正确异常是否正确允许修改的状态是否正确改变不允许修改的状态是否保持不变。这一单元让我对测试的理解从“看输出”转向“看状态变化”。比如cleanSpamComments不仅要检查评论是否删除正确还要检查视频播放量、点赞数、硬币数等无关属性没有被误改。第四单元业务流程和模型一致性第四单元没有互测也没有像第二单元那样明显的性能压力但这并不意味着测试不重要。本单元更适合从业务流程和模型一致性角度进行测试。我认为比较重要的测试点包括图书总数始终不变一本BookCopy任意时刻只能位于一个地点。借书、预约、取书后用户持有的 B/C 类图书仍满足数量限制。开馆前整理后借还处不应有书预约处不应有过期书阅览室不应有书。预约在开馆前和闭馆后送达时失效日期的计算是否正确。阅读后未归还是否会在整理时回收并按要求扣除信用分。借阅期限、逾期扣分和续借逻辑是否覆盖边界日期。Trigger注解中的状态迁移是否能在状态图中找到对应。SendMessage注解中的消息是否能在顺序图中找到对应。相比前三个单元第四单元的测试更像是在检查系统不变量。只要图书位置、用户状态、预约状态和信用分这几类状态始终一致系统的大部分业务逻辑就比较稳。课程收获回顾整个 OO 课程我最大的收获是逐渐拥有了开发小项目的能力。刚开始面对复杂需求时我很容易不知道从哪里下手只能一边写一边改经过四个单元的训练后我更习惯先识别核心对象再拆分职责最后按模块逐步实现。第一单元让我理解了层次化设计知道可以通过文法结构拆解复杂表达式第二单元让我理解了对象协作和线程安全知道共享数据必须有明确的同步边界第三单元让我理解了规格和状态维护知道正确性不只是样例输出正确而是要满足完整契约第四单元让我理解了正向建模知道架构可以在编码前被表达、讨论和修正。我对面向对象的理解也更具体了。对象不只是“字段 方法”的组合而应该是某种业务状态和规则的归属地。比如BookCopy负责图书副本的位置和轨迹User负责用户持有图书和信用分Order负责预约生命周期Location负责地点中的图书集合。只有让状态和规则找到合适的归属代码才不容易变成一堆互相调用的过程函数。另外我也更重视测试和调试了。从第一单元的手工样例到第二单元的评测机再到第三单元的 JUnit 和第四单元的不变量检查我逐渐意识到测试不是写完代码后的附属工作而是设计本身的一部分。很多时候一个类应该承担什么职责可以通过“它应该保证哪些不变量”反推出来。当然我现在的架构设计能力仍然有不足。比如在控制类和服务类之间如何划分边界什么时候应该增加抽象什么时候应该保持简单这些问题都需要更多实践。但至少现在再面对一个稍复杂的需求时我不会像以前那样完全无从下手而是能够先画出对象关系找出核心状态再一步步实现和验证。参考资料andrej-karpathy-skills主要参考其“用规则文档约束 coding agent 行为”的思路。BUAA-OO-第四单元UML建模参考了第四单元博客的整体组织方式。「BUAA-OO」第四单元UML建模语言参考了从 UML 和课程体验角度进行总结的写作思路。以上两篇学长/学姐博客仅作为结构和反思角度参考正文内容均结合我自己的代码和本届作业要求撰写。