272
社区成员




本单元的核心是“正向开发”,即从需求分析开始,通过UML进行系统建模,最后根据模型编写代码。这一过程与我们之前“拿到需求直接开干”的模式截然不同。
1. 建模过程回顾
我的开发流程大致如下:
需求分析:仔细阅读指导书中的文字描述,这是所有工作的基础。我会将关键的业务规则、实体、操作都摘录出来。
静态结构建模 (UML类图):
识别实体: 从需求中识别出核心的名词,如“图书馆 (Library)”、“书籍副本 (Book)”、“学生 (Student)”、“书架 (Bookshelf)”、“预约处 (AppointmentOffice)”等。这些自然地成为了我们系统中的类。
定义属性与关系: 分析每个实体的属性(如Book
有ID、状态;Student
有姓名、已借阅列表)和它们之间的关系。例如,Library
“拥有”Bookshelf
、AppointmentOffice
等,这是一种强烈的“组合”关系;而 Bookshelf
“包含”多本 Book
,这是一种“聚合”关系。这些关系直接指导了代码中成员变量的设计。
动态行为建模 (UML状态图与顺序图):
状态图: 对于像Book
这样拥有复杂生命周期的核心对象,我为其绘制了状态图。一本书的状态从“在书架上” (bs
/hbs
),到被预约后移至“预约处” (ao
),再到被学生取走成为“用户持有” (user
),归还后进入“借还处” (bro
),最后在整理日回到书架。这个过程中的每一个状态转换(如borrowed
, returned
, picked
, restored
)都对应了代码中的一个具体方法或逻辑分支。
顺序图: 对于复杂的用户场景,如“学生预约并成功取书”,我会绘制顺序图来梳理对象间的交互流程。例如,Main
接收到ordered
指令,传递给Library
,Library
调用Student
检查资格,然后更新OrderManager
(如果将其分离),最后在整理日,Library
协调Bookshelf
和AppointmentOffice
完成书籍的物理移动。这清晰地定义了方法调用的顺序和依赖关系。
2. 正向开发的优势
通过这种自顶向下、从模型到代码的开发方式,我深切体会到其带来的好处:
结构清晰:在动手编码前,整个系统的宏观架构和核心对象的职责已经非常明确,避免了“代码写到一半发现结构不对推倒重来”的窘境。
逻辑完备性:建模过程强迫我们思考各种边界情况和状态转换的触发条件,减少了在实现阶段的逻辑遗漏。
易于协作与沟通:UML作为一种图形化的标准语言,提供了一个清晰、无歧义的沟通媒介。
本单元的三次作业在功能上层层递进,从一个基础的图书馆,到增加了热门书架和阅览室,我的架构也随之演化。
1. 最终架构设计
最终形成了一个职责分明的架构,这与我最初的UML模型高度对应:
Main.java
: 程序入口,负责接收官方IO的输入,并将指令派发给Library
对象。
Library.java
: 系统的“大脑”和总协调者。它不直接处理具体的位置管理,而是持有各个功能区的管理器实例,负责解析指令、协调各组件完成复杂的业务流程(如开闭馆整理)。
实体类:
Book.java
: 封装了单个书籍副本的所有状态和属性。
Student.java
: 封装了学生的状态,如图书持有情况、预约状态、阅读状态等。
位置管理器类:
Bookshelf.java
: 管理所有在书架上的书(包括物理上的普通和热门书架)。
BorrowReturnOffice.java
: 管理借还处的书籍。
AppointmentOffice.java
: 管理预约处的书籍。
ReadingRoom.java
: 管理阅览室的书籍。
服务类:
TraceManager.java
: 专门负责记录和提供所有书籍的移动轨迹。
这个架构遵循了“单一职责原则”,每个类都只做一件事,使得代码逻辑清晰,易于扩展和维护。
2. UML模型与代码的追踪关系
类图 -> 代码:
UML类图中的每一个类(Library
, Book
, Student
等)都直接对应一个.java
文件。
类图中的属性(如Book
的id
, currentState
)对应了类中的成员变量。
类图中的关系在代码中得到了体现:
组合关系: Library
类中持有 Bookshelf
, BorrowReturnOffice
等实例,体现了强烈的“拥有”关系。
聚合关系: Bookshelf
类中持有一个 Map<LibraryBookId, Book>
,体现了书架“包含”书籍的关系。
依赖关系: Book
类的setCurrentState
方法需要传入一个TraceManager
对象来记录轨迹,这体现了Book
对TraceManager
的依赖。
状态图 -> 代码:
以Book
的状态图为例,currentState
这个LibraryBookState
类型的成员变量就是状态图的直接实现。
状态图中的每一个事件 (Event),如borrowed
、returned
、picked
、restored
,都对应Library
类中的一个handle...
方法。
这些方法内部的逻辑,就是对**守护条件 (Guard)的判断(如if (!isLibraryOpen || isbn.isTypeA() || ...)
)和对动作 (Action)的执行(如book.setCurrentState(...)
, student.addBorrowedBook(...)
)。
顺序图 -> 代码:
以“学生成功阅读一本书”为例,其顺序图可能描绘了Main
-> Library
-> Student
-> Bookshelf
-> ReadingRoom
-> TraceManager
之间的消息传递。
这在代码中就体现为Main
调用library.processCommand()
,然后library.handleRequest()
调用handleRead()
,在handleRead()
内部,会依次调用student.canReadToday()
进行检查,调用bookshelf.findAvailableCopyForReading()
查找书籍,成功后调用bookshelf.removeBook()
和readingRoom.addBook()
,并最终通过book.setCurrentState()
调用traceManager.addTrace()
。代码的调用栈清晰地反映了顺序图中的交互时序。
本次作业我借助了大模型的帮助,这个过程让我对“AI编程”有了更深刻的认识。
引导AI进行架构设计的有效策略
顶层设计:由人来定义系统的宏观架构、核心模块及其职责。向AI提出类似“我们需要一个A类、B类、C类,A负责...,B负责...”这样的高级指令。
将需求转化为具体规则:将复杂的自然语言需求,拆解成清晰、无歧义的逻辑规则点,逐一喂给AI。
提供精确的反馈循环:使用“输入-你的输出-我的期望输出-差异点”的模式进行反馈。这是最有效的调试方式。
将UML作为沟通语言: 在更复杂的项目中,可以先让AI生成初步的PlantUML代码,然后由人来修改和确认UML模型,这能最大程度上保证架构的正确性。
第一单元(表达式解析):从面向过程到层次化设计
起初,面对复杂的表达式字符串,我的第一反应是“如何用一个复杂的正则表达式或一个冗长的方法解决所有问题”。这是一种典型的面向过程思维,试图用一个“万能函数”包揽一切,导致代码耦合度高,难以扩展和维护。在重构和迭代中,我被迫将表达式拆解。“递归下降” 的思想成为了我的启蒙。我学会了将“表达式”分解为“项”,将“项”分解为“因子”,并为每一个层次建立对应的类(Expression
, Term
, Factor
)。这种层次化、对象化的拆分,使得每个类的职责变得单一。
第二单元(多线程电梯):从静态结构到动态交互
进入多线程单元,我的思维惯性还停留在第一单元的静态数据结构上。我首先想到的是如何设计“电梯”这个类,而忽略了“请求”和“调度”的动态特性。真正的挑战在于如何处理多个独立运行的线程(电梯、请求放入者)之间的协作与资源争抢。我的架构核心从设计单个类,转向设计线程安全的共享对象和线程间的交互模式。“生产者-消费者”模式成为了整个架构的灵魂,我设计了中央的“调度器”(Scheduler
)作为“托盘”,所有请求者(生产者)向其中放入请求,所有电梯(消费者)从中获取请求。这让我认识到,好的架构不仅要设计优美的静态类图,更要规划清晰的动态协作流程。
第三单元(JML规格):从自由创造到“契约式设计”
在前两个单元,架构设计是自由的,目标是“解决问题”。面对JML,我最初把它当成一种“带注释的代码”,试图先写代码再去匹配JML。很快我发现这是本末倒置。JML的核心是“契约式设计”(Design by Contract)。我的思维从“我该怎么实现?”转变为“我必须遵守什么?”。架构设计不再是天马行空,而是基于JML接口进行“填空”。我开始首先深入理解JML描述的requires
, ensures
, assignable
等条款,思考如何选择最优的数据结构(如用HashMap
优化查询)来满足规格所要求的性能,并时刻维护数据不变量(invariant
)。这次演进让我懂得了接口与实现分离的深刻含义,架构的首要职责是定义和遵守契约,而非沉迷于实现细节。
第四单元(UML建模):从代码驱动到模型驱动
前三个单元的训练,让我习惯于直接面对代码和需求。UML图在初期对我来说,只是“代码的另一种表现形式”,是写完代码后补充的文档。本单元要求我们进行“正向建模”,彻底颠覆了我的开发流程。我学会了先用类图设计系统的静态骨架,用顺序图推演关键业务逻辑的交互流程,用状态图明确核心对象(如图书、借还机)的生命周期。我的设计工作从IDE转移到了建模工具。代码不再是设计的起点,而是高层模型的实现产物。这种从抽象模型到具体代码的“降维打击”,让我在编码前就对系统全貌了然于胸,极大地减少了后期重构的风险。
如果说架构设计是“建高楼”,那么测试就是“做质检”。我的测试思维同样经历了一场深刻的变革。
第一单元(表达式求导):从手动验证到自动化评测
最初的测试就是“人眼编译”,手动输入几个简单的样例,如x*x
, sin(x)
,看看结果对不对。随着表达式嵌套加深、空白符增多、出现非法格式,手动测试变得捉襟见肘。我开始编写Python脚本,自动生成海量、复杂的合法及非法测试数据。这让我体会到自动化测试和数据生成器的威力。我的测试思维从“点”的验证(几个特定样例)扩展到了“面”的覆盖(覆盖各种边界和异常情况)。
第三单元(JML规格):从黑盒猜测到规格驱动
在没有JML的时代,测试用例的设计很大程度上依赖于对代码实现的“猜测”,属于“灰盒测试”。我的测试思维转变为规格驱动测试。每一个JML方法都是一个测试单元,requires
子句定义了测试输入的边界和前提,ensures
子句定义了期望的输出和状态变化,signals
子句则明确了异常情况的测试。我系统地针对每个方法的每个条款编写JUnit单元测试,这是一种严谨的、可追溯的测试方法,确保了实现与规格的完全一致。
结语
OO课程带给我的不仅是知识,更是一种结构化、系统化、抽象化的思维方式。这种思维的锤炼,将是我未来面对任何挑战时,最宝贵的财富。