309
社区成员
发帖
与我相关
我的任务
分享正向建模是指在编码实现之前,先通过UML等建模语言完成系统设计,再依据模型指导编码实现的开发方式。本单元的三次作业严格遵循"设计程序架构 → 绘制UML类图 → 编程实现"的工作流,是对正向建模能力的一次完整训练。
本单元的类图设计并非一蹴而就,而是经历了一个"两阶段"的迭代过程:
第一阶段:需求驱动的初步设计
在拿到作业要求后,首先根据需求描述抽象出核心概念:
| 需求概念 | 对应类 | 职责 |
|---|---|---|
| 图书馆 | Library | 系统的中枢控制器,协调所有子模块 |
| 书架 | BookShelf | 管理图书的库存与副本 |
| 借还处 | BorrowReturnOffice | 暂存归还的图书 |
| 预约处 | AppointmentOffice | 管理预约请求与预留图书 |
| 学生/用户 | Student | 维护用户的借阅状态与限制 |
| 图书 | Book | 图书领域对象(书号+预约日期) |
| 阅览室 | ReadingRoom (v2新增) | 管理当日阅读中的图书 |
| 评分状态 | BookStatus (v2新增) | 跟踪ISBN维度的评分与精品状态 |
| 借阅期限 | BorrowLimit (v3新增) | 封装借阅期限计算与逾期判定 |
这一阶段重点解决的是**"有哪些类"和"类之间如何关联"**的问题。比如Library组合了BookShelf、AppointmentOffice、BorrowReturnOffice,而Book作为值对象在这些模块间流动——这些关系必须在开工前就确定下来。
第二阶段:实现反馈的迭代修正
在编码过程中,会发现初步设计中存在不合理的部分,需要回头修正类图:
borrowBookB字段最初设计为LibraryBookId,而borrowBookC是一个HashSet<LibraryBookId>。这个设计一直沿用到HW14。borrowBookB需要从简单的LibraryBookId升级为主持借阅期限信息的BorrowLimit对象,而borrowBookC也变成了HashMap<LibraryBookId, BorrowLimit>。这一改动涉及Student、Library、BorrowLimit三个类的协同变更,类图的变更是必不可少的导航工具。type字段(0=普通,1=精品),并新增getBookShelf(isbn)方法来动态路由。作用一:职责的可视化分派
面对一个包含借书、还书、预约、取书、评分、阅读、续订等十余种操作的复杂系统,将所有逻辑塞进一个"上帝类"是最容易的陷阱。类图强制你思考:这个操作应该由哪个类来执行? 例如:
Library负责流程编排(openArrangeBooks),但不直接管理图书副本——这交给BookShelfBookShelf负责副本的增删查,但不关心借阅规则——这由Student.canBorrow()判定BorrowLimit封装逾期计算,使得Student和Library都不需要直接操作日期运算这种"高内聚、低耦合"的分层在类图中一目了然。
作用二:增量开发
三次作业的功能叠加是递增的:
HW13: 借书/还书/预约/取书/查询(4种位置状态)
└→ HW14: +阅读/归还/评分/精品书架(6种位置状态)
└→ HW15: +信用分/借阅期限/续订(6种位置+信用维度)
每次迭代都需要修改多处代码。类图在这里有着重要作用——通过查看类图中被修改类的关联关系,可以预判哪些类会受到波及。例如HW15新增信用分系统时,类图告诉我们:Student持有credit字段,Library中所有涉及借阅、预约、阅读、逾期处理的方法都需要检查信用分,而BookShelf和BorrowReturnOffice则完全不需要改动——这就是类图的导航价值。
作用三:UML到代码的双向一致性
类图不是画完就束之高阁的装饰品。本单元要求类图与最终代码保持一致,这意味着编码过程中的每一次调整都需要回写到图里。这种"图即文档、文档即图"的实践,建立在代码与UML之间可追溯的基础上(详见第二部分)。
经过三次迭代,最终的小型图书馆管理系统形成了以下架构:
Main (入口,命令分发)
└── Library (中央控制器)
├── BookShelf (普通书架) ── 管理副本库存
├── BookShelf (精品书架) ── 管理精品书副本
├── BorrowReturnOffice ── 暂存归还图书
├── AppointmentOffice ── 管理预约与预留
├── ReadingRoom ── 管理阅读中的图书
├── HashMap<isbn, BookStatus> ── 按ISBN维护评分状态
├── HashMap<string, Student> ── 按学号维护用户状态
│ ├── BorrowLimit (B类书借阅期限)
│ └── HashMap<id, BorrowLimit> (C类书借阅期限)
└── HashMap<id, List<Trace>> ── 图书移动轨迹日志
这是一个典型的中心调度+功能模块架构。Library作为Facade门面,对外暴露所有业务接口(borrowBook、returnBook、orderBook、pickBook、readBook、restoreBook、gradedBook、queryBook、renewBook、queryCredit),对内协调各功能模块完成任务。每个功能模块是自治的——它们持有自己的数据并对数据的一致性负责。
虽然没有显式使用GoF设计模式,但代码中自然体现了多种设计原则:
BookShelf只管理库存,BookStatus只维护评分,BorrowLimit只计算期限。BookStatus.isChange()判断精品状态变化,BorrowLimit.isOverdue()判断逾期——数据和行为在同一类中。Main调度Library,Library调度各模块,模块之间不直接通信,由上层协调。这是本单元的核心考察点之一:UML模型与代码实现之间必须建立可验证的追踪链。
每一个UML类在代码中对应一个.java源文件:
| UML类图元素 | 源代码文件 | 备注 |
|---|---|---|
| Main | Main.java | 入口类(3个版本均有) |
| Library | Library.java | 核心控制器(逐步膨胀) |
| BookShelf | BookShelf.java | v1起存在,v2增加type字段 |
| Book | Book.java | 值对象(3个版本未变) |
| Student | Student.java | v3从简单持有升级为带信用分 |
| BorrowReturnOffice | BorrowReturnOffice.java | 3个版本基本未变 |
| AppointmentOffice | AppointmentOffice.java | 3个版本基本未变 |
| BookStatus | BookStatus.java | v2新增,v3沿用 |
| ReadingRoom | ReadingRoom.java | v2新增,v3沿用 |
| BorrowLimit | BorrowLimit.java | v3新增 |
UML中每一个属性字段在代码中有对应声明。以Student的HW15版本为例:
| UML属性 | 代码声明 | 类型 |
|---|---|---|
| studentId | private String studentId | String |
| borrowBookB | private LibraryBookId borrowBookB | LibraryBookId |
| borrowDateB | private BorrowLimit borrowDateB | BorrowLimit |
| borrowBookC | private HashMap<LibraryBookId, BorrowLimit> borrowBookC | HashMap |
| credit | private int credit (初始值100) | int |
UML中的每个公共方法在代码中有对应实现:
| UML方法 (Library) | 代码方法 | @Trigger注解 |
|---|---|---|
| borrowBook() | public void borrowBook(LibraryReqCmd) | BOOKSHELF→USER |
| returnBook() | public void returnBook(LibraryReqCmd) | USER→BRO |
| orderBook() | public void orderBook(LibraryReqCmd) | - |
| pickBook() | public void pickBook(LibraryReqCmd) | AO→USER |
| readBook() | public void readBook(LibraryReqCmd) | SHELF→READING_ROOM |
| restoreBook() | public void restoreBook(LibraryReqCmd) | READING_ROOM→BRO |
| gradedBook() | public void gradedBook(LibraryReqCmd) | - |
| renewBook() | public void renewBook(LibraryReqCmd) (v3新增) | - |
| queryCredit() | public void queryCredit(LibraryQcsCmd) (v3新增) | - |
这表示无论从普通书架还是精品书架借书,最终都会转移到USER状态——这与状态图中的转移定义完全一致。
在对比UML和代码时,我也发现了一些值得关注的差异:
getBookShelf()方法在代码中确实存在,但它在UML类图中标注为private而代码中实际是private——这是一致的。HashSet<LibraryBookId>到HW15的HashMap<LibraryBookId, BorrowLimit>,这是一个类型层面的"破坏性变更",必须同时在类图和代码中反映。orderNewBook()方法是一个空的占位方法,代码中有定义但业务逻辑未实现——这是为了满足某种接口约束的预留,在UML中同样需要标注。| 维度 | HW13 | HW14 | HW15 |
|---|---|---|---|
| 类数量 | 7 | 10 | 11 |
| 位置状态数 | 4 | 6 | 6 |
| 操作类型 | 5 | 8 | 10 |
| UML图类型 | 类图 | 类图+状态图 | 类图+状态图+顺序图 |
| 核心抽象 | 图书流通 | +评分/精品 | +信用/期限 |
| Student复杂度 | 简单hashset持有 | 不变 | 引入credit+BorrowLimit |
可以看到,架构的增长符合"开闭原则"——每次迭代主要做加法而非修改已有的稳定模块。BookShelf、BorrowReturnOffice、AppointmentOffice、Book这些类在三次迭代中几乎没变,而Library和Student逐步承接了新的复杂度。这种稳定-变化分离的设计使得每次迭代的改动范围可控。
在本单元的正向建模过程中,我尝试了使用大模型辅助完成以下几个环节:
需求分析到类图草稿的转换:将中文需求文档(如"题目要求.md")输入大模型,让它帮梳理出初始类结构。
从UML描述生成代码框架:在StarUML中完成类图后,让大模型根据类图描述生成带方法签名的Java骨架代码。
代码与UML的一致性检查:让大模型读取.java文件和.mdj文件两端内容,对比差异并指出不一致之处。
状态图的生成与验证:使用staruml-skills工具链(staruml_state_tool)配合大模型,从代码中的@Trigger注解反向生成状态图,验证状态图与实现的吻合度。
| 阶段 | 大模型表现 | 典型问题 |
|---|---|---|
| 需求抽象 | 较好 | 能准确识别"书架""借还处""预约处"等实体,但易遗漏隐式约束(如"用户只能借一本B类书") |
| 类图设计 | 一般 | 能产出基本结构,但对性能敏感的细节(如BookShelf用HashSet<Integer>管理副本号以支持O(1)移除最小副本)缺乏考量 |
| 代码生成 | 较好 | 给定清晰的类图和接口描述,能快速产出可编译的代码骨架 |
| 一致性检查 | 一般 | 能发现明显的缺失/多余方法,但对类型变化(如HashSet→HashMap的演进)的语义分析不够精准 |
| 重构建议 | 较弱 | 倾向于"教科书式"重构建议(如"提取接口""使用策略模式"),缺乏针对具体业务约束的工程判断 |
经过本单元的实践,我总结出以下几条引导大模型完成架构设计任务的有效方法:
不要期望一次性给出完整需求就能得到完美设计。更有效的方式是"多轮对话-逐层细化":
第一轮:请根据以下需求,识别系统中的核心实体(领域类)
第二轮:请分析这些实体之间的关联关系和多重性
第三轮:请为每个实体设计方法和属性
第四轮:请检查设计是否满足以下约束条件...
每轮聚焦一个问题维度(实体识别→关系→方法→约束),大模型的输出质量和一致性显著高于一次性生成。
大模型"自由发挥"时容易产生边界模糊的设计。使用明确的格式约束可以有效强制模型的条理性:
请按以下表格格式输出类设计:
| 类名 | 职责(一句话) | 关键属性 | 关键方法 | 关联类 |
这种结构化输出使得设计结果便于审查和迭代,也更容易从中提取代码骨架。
大模型对自然语言中的"隐含规则"捕捉能力有限。对于图书馆系统这类规则密集的场景,将业务规则用if-then形式化语言明确列出,能显著提升设计质量:
规则1:IF 图书为A类 THEN 不可借阅 AND 不可预约
规则2:IF 用户已持有一本B类书 THEN 不可再借B类书
规则3:IF 用户已持有某ISBN号的C类书 THEN 不可再借该ISBN的C类书
在给出这段规则列表后再要求生成类设计,大模型会更自然地将规则分配到对应的类方法中(如规则2和3归入Student.canBorrow())。
在完成编码后,让大模型逆向检查:
请对比以下UML类图描述和Java源代码,列出所有不一致之处:
1. UML有但代码无的属性/方法
2. 代码有但UML无的属性/方法
3. 类型或签名不匹配的方法
这种双向验证比单向生成更有价值,也能帮助发现手工编码中偏离设计的部分。
尽管大模型在以上场景中表现可用,但本单元的实践也让我清醒认识到其局限:
缺乏领域状态机直觉:对于"整理流程中每本书只能移动一次,但移往预约处不计入限制"这类复杂的流程约束,大模型需要多轮澄清才能正确建模
增量修改容易迷失:在三轮迭代中,大模型更擅长"从零生成"而非"在已有代码基础上精确定位修改点"。它倾向于重写大段代码而非最小化修改。
物理世界建模的前提不清:大模型不知道"为什么B类书借阅期限是15天、C类是30天"——这些规则背后的"为什么"是人类设计者在需求分析阶段要完成的,大模型只能执行而不能质疑。
抽象思维:第一单元实现了多项式化简得的功能。其中我在解析初始字符串时,将其抽象为了exp,term和factor三个层次的概念。同时为每一个层次设计了toPoly()方法来完成化简的功能。这种分层抽象的思维方式使得代码结构清晰,职责分明。而面对表达式因子我们又在其toPoly()方法中使用了递归调用的方式来处理括号内的表达式,这也是一种抽象思维的体现。
生产者-消费者模式:第二单元是一个多线程电梯。我使用了经典的生产者-消费者模式来设计电梯的调度系统。输入解析线程作为生产者,不断从输入中解析出电梯请求并放入请求队列;电梯调度线程作为消费者,从请求队列中取出请求并执行相应的电梯操作。这种模式使得输入解析和电梯调度解耦,提高了系统的灵活性和可维护性。
时间复杂度:这个单元的结构其实是按照给定的JML规范来实现的。但是其中的具体数据结构等是要求自己设计的,同时这个单元相比于前面的单元对于时间复杂度要求更高,让我学会了在设计数据结构以及方法具体实现时,都要考虑时间复杂度的问题。
第四单元又回到了自己独立设计架构的模式,在这个单元中我学会了如何进行正向建模以及迭代开发。通过三次作业的不断迭代,我逐步完善了小型图书馆管理系统的架构设计,并且在每次迭代中都保持了UML类图与代码实现的一致性。这种正向建模的方法使得我的设计更加清晰和合理,同时也提高了代码的可维护性和扩展性。