308
社区成员
发帖
与我相关
我的任务
分享本单元采用了"先建模、后编码"的正向建模流程。三次作业都是先提交一阶类图 uml_pre.mdj框住整体思路,再提交二阶类图 uml_ultimate.mdj,形成了一个从抽象设计到具体实现的完整闭环。
一阶类图的作用:在一阶阶段,我们尚未编写任何代码,需要在纯设计层面思考图书馆系统的整体架构。这一阶段的核心价值在于迫使设计者在编码之前回答几个关键问题:系统中有哪些实体?它们之间是什么关系?职责应该如何划分?例如在 hw13 的设计中,我将系统划分为 Library(核心控制器)、User(用户实体)、BookCopy(图书副本)、BookIsbn(图书标识)、Location(地点接口)以及 BookShelf、BorrowReturnOffice、AppointmentOffice(具体地点实现)等类。一阶类图帮助我在没有代码实现压力的情况下,建立起清晰的职责边界。
二阶类图的作用:在编码过程中,实际实现往往会暴露出一阶设计中的不足。例如在 hw14 中,由于新增了阅读、评分、精品书架等功能,一阶类图中缺少的 ReadingRoom、TreasuredBookshelf、BookScore、ArrangeHandler、CommandProcessor、PendingOrderManager 等类需要补充。二阶类图需要记录这些修正,使得类图与代码保持一致。同时,两阶类图的相似度检查机制(要求相似度 ≥ 60%)也有效防止了"先画空图再补代码"的偷懒行为,确保一阶类图是真正有意义的架构设计。
从实践来看,两阶类图的流程形成了一种"设计→实现→修正→再设计"的迭代模式,这与软件工程中"架构设计不是一次性完成的"这一认知高度吻合。
本单元三次作业的架构呈现出清晰的演进脉络:
核心类图包含 11 个类:
Library:作为中央控制器,持有所有地点引用,直接处理借还、预约、取书等业务逻辑BookShelf、BorrowReturnOffice、AppointmentOffice:三个地点类,均实现 Location 接口BookCopy、BookIsbn、User、MovingTrace、Reservation:实体类Location:接口,定义地点的通用操作此时的架构特点是 Library 类承担了较多职责,既是业务逻辑的组织者,又是请求处理的入口,还内嵌了整理逻辑。很容易超出checkstyle的类行数限制。
新增 6 个类,总计 17 个类:
CommandProcessor:从 Main 中提取命令处理逻辑,形成独立的命令处理器,符合单一职责原则ArrangeHandler:从 Library 中提取整理流程逻辑,将开馆前整理和闭馆后整理统一管理PendingOrderManager:从 Library 中提取待处理预约管理逻辑ReadingRoom、TreasuredBookshelf、BookScore:新增地点类和评分实体将整理逻辑从 Library 中抽离。在 hw13 中,Library.open() 和 Library.close() 直接包含整理代码;在 hw14 中,这些逻辑被封装到 ArrangeHandler 中,Library 仅负责委托调用。这使得 Library 的代码量从 415 行降至 313 行,同时整理逻辑获得了独立的演进空间。
类的数量保持 17 个不变,但内部设计发生了重要变化:
User 类:新增 credit 信用分属性及 addCredit/deductCredit/canBorrowOrOrder/canReadCategoryA 等方法;borrowedBooks 和 readBooks 从 List<BookCopy> 改为 Map<BookCopy, LocalDate> 以记录借阅日期BookCopy 类:新增 borrowDate、dueDate、overdueDeducted 属性,新增 markBorrowed、isOverdue、renew 等方法ArrangeHandler:新增 deductCreditForOverdueBooks 和 deductCreditForUnreturnedReads 方法,实现逾期扣分和未归还扣分逻辑Library:新增 renew 方法,order 方法增加日期参数CommandProcessor:新增 doRenew 和 handleCreditQuery 处理代码设计与UML模型的追踪:在最终的 hw15 类图中,User 类的属性从 hw14 的 -studentId: String, -borrowedBooks: List<BookCopy>, -reservations: Map<BookIsbn, BookCopy>, -readBooks: List<BookCopy> 演变为 -studentId: String, -credit: int, -borrowedBooks: Map<BookCopy, LocalDate>, -readBooks: Map<BookCopy, LocalDate>,与代码实现完全对应。BookCopy 类的属性也从简单的 isbn/copyId/currentLocation/holder/movingTraces/borrowed/read 扩展为包含借阅日期和到期日期的完整状态模型。这种追踪关系体现了 UML 类图作为"设计蓝图"的核心价值——它不是代码的事后文档,而是代码的先验约束。
在本单元中,我使用大模型辅助完成了部分正向建模和代码实现工作。基于实际体验,总结以下引导策略:
1. 提供充分的上下文是关键:大模型在面对复杂场景时,最常出现的问题是"不了解全局"。有效的引导方式是将题目要求、已有的类图设计、代码实现等完整信息一次性提供,而不是分多次零散地输入。例如在 hw15 的实现中,将完整的题目描述、hw14 的代码、以及信用分系统的需求一并给出,大模型才能给出合理的架构建议。
2. 先让模型分析,再让它实现:在让大模型编写代码之前,先要求它分析现有架构、识别需要修改的地方、列出设计要点。这种"先思考后动手"的模式能显著提高输出质量。例如在 hw15 中,先让大模型分析 hw14 代码中哪些类需要修改、新增哪些方法,再开始编码。
3. 明确约束条件:大模型容易在复杂约束场景中遗漏边界条件。需要明确告诉它所有约束(如信用分阈值、借阅期限、逾期判断逻辑等),并要求它逐一检查。例如"用户信用分大于80才能借阅"这一约束,如果不明确指出,大模型可能将其与"大于0可以阅读"混淆。
4. 利用迭代修正:大模型的第一次输出往往不完美,需要通过多轮对话进行修正。关键是准确描述"哪里不对"和"应该怎样",而不是笼统地说"重新来"。
回顾四个单元的学习历程,我的架构设计思维经历了从"能跑就行"到"设计先行"的转变。
第一单元的核心任务是实现表达式解析与求导。hw1 的代码结构非常扁平:Lexer 负责词法分析,Parser 负责语法分析,Exp/Expr/Term/Factor/Mono/Poly 构成 AST(抽象语法树),Num/Var/PowerExpr/NegExpr/Selection 是具体的叶子节点。此时的类设计主要是为了满足解析需求而自然形成的——每个类对应语法产生式中的一个非终结符或终结符,职责边界由语法规则决定,而非人为划分。
到了 hw3,新增了 FuncFactor(函数因子,支持 sin/cos/sqrt)和 DerivativeFactor(求导因子),以及递归函数定义的支持。这次扩展让我体会到:好的 AST 设计天然具有可扩展性——新增一种表达式类型只需添加一个新的 Factor 子类,而不需要修改 Parser 的核心逻辑。这是"开闭原则"的第一次实践体验。
但此时的架构仍然是被动的——类的划分由外部约束(语法规则)驱动,而非主动的设计决策。
第二单元引入了多线程,架构设计的重要性第一次真正显现。
hw5 实现了 6 部电梯的基本调度:InputThread 读取输入,DispatchThread 分发请求,ElevatorThread 驱动每部电梯运行,Elevator 类封装电梯状态。同步机制采用 ReentrantReadWriteLock——Elevator 类的每个状态访问方法都包裹在读/写锁中。此时的架构特点是线程模型清晰,但类的职责边界模糊——ElevatorThread 既负责电梯运行逻辑,又负责乘客上下判断,代码耦合度较高。
hw6 新增了 ElevatorControl(电梯控制状态机)和 MaintQueue/MaintTask(维护任务队列)。ElevatorControl 使用 synchronized + wait()/notifyAll() 实现状态管理,将"电梯是否可以接收新任务"这一判断逻辑从 ElevatorThread 中抽离。这是第一次有意识地将控制逻辑与执行逻辑分离。
hw7 将电梯数量扩展到 12 部(6 主 + 6 副),引入了 ControlQueue/ControlTask/ControlType 实现统一的控制指令分发。架构从"每部电梯独立运行"演变为"中央调度 + 分布式执行"。这一阶段最深刻的教训是:并发场景下,架构设计的失误会直接导致死锁或竞态条件,且难以调试。例如 hw7 中主副梯配对逻辑如果放在错误的线程中,就会引发状态不一致。
第三单元以 JML 规格为核心,实现了社交网络/视频平台系统。
hw9 的架构非常简洁:Network(图的容器)、User(节点)、Video(视频实体),加上 MainClass 一共 5 个类。Network 类实现了 NetworkInterface,包含 addUser、followUser、unfollowUser、watchVideo、queryShortestPath 等方法。此时的架构是规格驱动的——JML 规格定义了接口,代码实现填充细节。
hw11 的架构发生了质变:从 Network 中提取出 GraphAlgorithms 工具类,封装最短路径(BFS)和最长递减序列(记忆化 DFS)算法;新增 PopularVideoCache 类,用空间换时间缓存每个类型的最热视频。User 类也从简单的属性容器扩展为包含兴趣画像(typeCounts)、影响力计算(getInfluence)、贡献者追踪(contributors)等复杂功能的实体。
这一单元教会我两个重要的架构原则:第一,关注点分离——图算法不应该混在 Network 的业务逻辑中,缓存逻辑不应该散落在各个操作里;第二,性能优化需要架构支撑——PopularVideoCache 的设计不是事后的"打补丁",而是在理解了"热度查询是高频操作"这一需求后做出的主动架构决策。
第四单元是架构设计思维的集大成。
hw13 建立了基础架构:11 个类,Library 作为中央控制器直接处理所有业务。Location 接口抽象了地点的通用操作,BookShelf/BorrowReturnOffice/AppointmentOffice 实现该接口。此时的 Library 代码量达 415 行,承担了借还、预约、取书、整理等所有职责。
hw14 是架构演进的关键转折:将 Library 拆分为 Library(313 行)+ CommandProcessor(115 行)+ ArrangeHandler(350 行)+ PendingOrderManager(70 行),新增 ReadingRoom、TreasuredBookshelf、BookScore 三个实体类。这次拆分不是因为 hw13 的设计"失败",而是因为 hw14 新增的阅读、评分、精品书架等功能使得 Library 的职责自然膨胀,拆分是需求扩展的必然结果。
hw15 在不增加新类的情况下,通过深化内部设计引入了信用分系统、借阅期限和续订功能。User 的 borrowedBooks 从 List<BookCopy> 改为 Map<BookCopy, LocalDate>,BookCopy 新增 dueDate/overdueDeducted 等状态字段。这体现了**"类的数量不变,但类的内部复杂度随需求增长"**这一真实的软件演进规律。
纵观四个单元,架构设计思维的演进遵循一条清晰的主线:
最深刻的体会是:好的架构不是一次性设计出来的,而是在迭代中逐步演化的,如hw13 的"大 Library"在 hw14 中被拆分,hw9 的"扁平 Network"在 hw11 中被模块化。这种"演进式架构"的理念,比任何具体的设计模式都更有价值。
第一单元的测试主要依赖手动构造测试用例 + 命令行验证。我通常只测试"正常路径"——输入一个合法的表达式,检查输出是否正确。对于边界情况(如空表达式、超大系数、嵌套深度极高的表达式)考虑不足。
这种测试方式的缺陷在互测中暴露无遗:别人用一个我没想到的边界输入就能找到 bug。例如 hw1 中对 exp() 函数的嵌套求导、hw3 中对递归函数的展开深度,都是我没有充分测试的场景。
第二单元引入多线程后,测试难度急剧上升。简单的单元测试无法覆盖竞态条件——两个线程同时访问同一个 Elevator 对象的状态,可能在特定的时序下产生错误。
我的应对策略是:
Elevator 类中添加断言,检查状态转换的合法性但即便如此,hw7 的主副梯配对逻辑仍然在互测中被发现了一个微妙的竞态 bug——在极少数时序下,副梯会在主梯尚未完全激活时就开始接收请求。这类 bug 只有在特定的线程调度顺序下才能触发,常规测试几乎不可能覆盖。
第三单元正式引入了 JUnit 测试框架,测试思维发生了质变。
**从"验证输出"到"验证行为"**:JML 规格为每个方法定义了前置条件(requires)和后置条件(ensures),测试的本质是验证"给定前置条件满足时,后置条件是否成立"。例如 followUser(id1, id2) 的测试需要覆盖:正常关注、自关注异常、重复关注异常、用户不存在异常。
**从"手动构造"到"系统化覆盖"**:我开始有意识地按照 JML 规格逐条编写测试用例,包括:
requires 时,ensures 是否成立)requires 时,是否抛出正确的异常)pure 方法调用前后,对象状态是否保持不变)缓存正确性测试:hw11 新增的 PopularVideoCache 需要特别的测试策略——缓存的更新必须与实际数据保持一致,测试需要验证"修改视频热度后,缓存是否正确更新"。
第四单元的交互式评测带来了全新的测试挑战。
输出驱动的测试模型:评测机只给出借阅、预约、查询、阅读、评分请求,而还书、取书、归还、续订请求是根据程序输出动态生成的。这意味着:如果程序的第一步输出有误(比如错误地批准了一个不该批准的借阅),后续所有动态生成的请求都会基于错误的前提,导致连锁错误。
端到端测试的必要性:由于请求之间存在因果关系,单个方法的单元测试已经不够——需要测试完整的业务流程。例如:预约 → 整理送书 → 取书 → 借阅 → 续订 → 还书,这条链路中任何一步出错都会影响后续所有操作。
状态一致性验证:我开始在测试中追踪系统状态——每次操作后,检查所有地点的图书数量是否守恒(图书总数不变)、用户的信用分是否正确更新、预约的保留期是否计算准确。
回顾整个课程,我的收获可以概括为以下五个方面:
课程名称是"面向对象设计与构造",但真正的"面向对象"不仅仅是使用类和对象。四个单元的学习让我理解了面向对象的核心是用对象来映射现实世界的实体和关系,用消息传递来模拟实体之间的交互。
Exp/Expr/Term/Factor 对应语法规则中的产生式,每个类封装一种表达式的解析和求导行为Elevator 封装电梯的物理状态,ElevatorThread 封装运行逻辑,ElevatorControl 封装控制状态机User/Video/Network 直接对应现实中的用户、视频、网络平台Library/BookShelf/AppointmentOffice/User 对应图书馆的各个实体和部门从 Unit 1 的"想到哪写到哪",到 Unit 4 的"先画类图再写代码",我建立起了系统化的架构设计思维:
Library 拆分为 Library + CommandProcessor + ArrangeHandler + PendingOrderManager)Location 接口统一了所有地点类的操作,Unit 3 的 NetworkInterface/UserInterface/VideoInterface 实现了规格与实现的分离ReentrantReadWriteLock 到 Unit 4 的 Map<BookCopy, LocalDate>,学会了用合适的数据结构管理复杂状态Unit 2 的电梯调度让我掌握了多线程编程的核心技能:
ReentrantReadWriteLock(hw5)、synchronized + wait()/notifyAll()(hw6 的 ElevatorControl)InputThread → ScheduleQueue → ElevatorThread)Unit 3 的 JML 让我体验了规格驱动开发的完整流程:
requires/ensures/assignable 子句清晰定义了每个方法的行为requires 子句明确异常抛出条件贯穿四个单元,我积累了使用大模型辅助开发的经验:
总体而言,大模型适合作为"代码助手"用于快速生成代码框架和基础测试用例,但在架构设计、性能优化、并发调试等需要深入思考的环节,仍需人工主导。