北航面向对象第四单元总结

2025-06-12

Java实现简单的图书管理系统,可借阅、预订和阅读书籍 本单元作为面向对象课程的收官之作,聚焦于 UML 建模与正向开发。通过设计一个图书馆管理系统,将前三个单元学习的架构设计、规格化设计等知识融会贯通,完整地体验了从需求分析、模型设计到代码实现的全过程。

一、正向建模与开发

本单元的核心是实践正向建模与开发,通俗来说,就是用模型驱动设计。与前几个单元先编码后总结的模式不同,本单元要求我们先使用 UML 对系统进行建模,再依据模型进行编码。

  • 我认为完全按照这个步骤进行实践,是过于死板的,值得肯定的是,正向建模后编写代码,会使得代码编写有迹可循,但显然,不能保证在建模阶段将所有因素考虑完全,设计好的UML图在代码编写过程中,可能会根据实际对代码进行较大的修改,此时就需要再次修改UML图,这无疑是低效的。
  • 更好的方法是,把手弄脏,在初期建模时不必将UML图完全画好,可以留下一些空白内容,UML图只是编码的一个辅助,不应该因为UML图导致编码过程束手束脚,我的正向建模流程如下:
  1. 需求分析:首先,通读指导书,识别出系统的核心实体(如 BookUserLibrary)、关键行为(如借阅 borrow、预约 order、归还 return)和它们之间的关系。
  2. UML 初步建模
    • 状态图:为核心实体 Book 绘制状态图。一本书的状态会从“在书架上,流转到被预约、被借阅、在阅览室等,但不必将状态转移方法的名称全部写上,可以后续补充。
    • 顺序图:为用户预约书籍的交互过程绘制顺序图,明确了对象之间的消息传递顺序和职责划分,同样的,不必将消息的名称全部写上,可以后续补充。
    • 类图:基于上述分析,初步绘制出系统的静态结构,将系统划分为几个明确的模块,作为后续编码的蓝图,但不必将类属性和方法全部写上,可以后续补充。
  3. 编码实现:遵循设计的 UML 模型进行编码,类、属性、方法都与类图一一对应。顺序图和状态图则成为具体方法实现的逻辑依据。
  4. 模型验证:按照 UML 草图完成编码后,使用 StarUML 的逆向工程功能生成最终的类图,确保模型与代码的一致性,顺序图和状态图也要根据代码逻辑和名称进行补全,代码中对应方法添加必要的 @Trigger 装饰器用于通过评测。

这种设计先行的方法,一方面避免 UML 类图导致的束手束脚,减少了前期正向建模所耗费的不必要的时间,另一方面,也发挥了 UML 图在代码编写时的指导性作用,平衡了开发效率和代码质量的矛盾,同时,前期深思熟虑的架构设计避免了后期大规模重构的风险。

二、架构设计与追踪

1. UML图

(1)类图

class

  • 整个架构主要包括了三层:最顶层的 Execute 类用于存储当前时间戳,管理 Library 类,Library 类内管理图书馆的所有部门,第二层是各种图书馆的部门,每个图书馆的部门以特定的容器高效管理书籍,第三层是 Book 类,受图书馆部门和用户管理,同时可以在各图书馆部门和用户手中之间转移。

  • 除此之外,User 类作为用户类,通过接收 Execute 类的指令,与图书馆的各个部门进行交互,同时能够通过借阅或归还来管理自身持有的书籍。

  • Library 类看起来有些多余,这其实是在设计的过程中,我通过阅读往年博客发现,可能会有馆际借阅这一操作,为了代码的可拓展性和可维护性,我从第一次迭代开始,就保留了 Library 类,方便后续迭代。

(2)状态转移图

state

2. 追踪关系

(1)类图

  • 元素的实现:图中的每一个类、接口或枚举,都必须在代码中找到其同名实现
    • 属性:名称、可见性(public, private)和数据类型必须完全一致。
    • 方法:方法签名(名称、参数、返回值类型)和可见性必须严格对应。
  • 关系的实现
    • 图中的一条泛化或实现箭头,直接对应代码中的 extendsimplements 关键字。
    • 图中两个类之间的关联、聚合、组合、依赖关系,也需要按照定义与代码逻辑对应。

(2)状态图

  • 状态转移与方法:状态之间的每一次转移,都由一个特定的方法触发。通过在代码方法上使用 @Trigger 注解,建立了从方法调用到状态转移的链接。
  • 条件的同步:如果一次条件转移是有条件的,那么图上状态转移的箭头旁应该有条件表达式,其逻辑表达式必须与对应方法内部的条件判断条件保持一致。这确保了状态变更的时机和条件都与设计完全一致。

(3)顺序图

  • 代码一致性:每个生命线都应该在代码中有对应的类与之对应,同时每个消息传递都是一个方法调用,因此每个消息的名称,都应该在代码中找到对应的成员方法。

  • 交互合法性:顺序图还隐含了约束。图中任意两个生命线之间若发生消息传递,那么它们所代表的类在类图上也必须存在合理的关联关系。确保了系统的交互不是随意的,而是基于其静态结构所允许的合法路径。

三、大模型辅助

在第三单元,我探索了如何利用大模型处理 JML 规格。本单元,我尝试引导它辅助进行更宏观的架构设计。在这一过程中,我认为引导大模型的关键在于将你自己的设计思维过程“教授”给它:分解问题、迭代求精、并提供清晰的反馈。它是一个强大的辅助工具,而不应该把它作为一个替代思考的轮椅。

1. 分步引导

架构设计是一个非线性的复杂过程,要求模型一次性输出所有内容,可能导致信息的丢失和逻辑的混乱。我们需要像庖丁解牛一样,将宏大的设计目标分解为一系列定义明确、前后衔接的子任务,不要直接向大模型抛出“请为我设计一个图书馆系统”这样的宏大问题。

  • Step1.识别核心元素:将核心需求发送给大模型,让它帮忙识别核心类和它们的基本职责。
  • Step2.生成代码架构:基于前一步输出的核心类与职责,让模型为每个类生成成员属性和方法。
  • Step3.聚焦复杂交互:针对类与类之间不同的关系的定义,逐步引导模型挖掘不同类之间潜在的联系,完成正向建模。

这种模式极大地降低了模型在每一步的思考负荷,使其能专注于解决当前问题,保证了每个阶段输出的深度和准确性,通过一步步迭代提示,引导大模型走向更好的结果。

2. 注入上下文

我在流程中让模型扮演了不同的角色:初始阶段是“经验丰富的系统分析师”,负责需求理解;精炼阶段是“注重设计质量的代码审查者”,负责批判性思考和优化。在提问时,提供尽可能多的上下文,可以给出前置的设计(如已有的代码或UML图),然后要求模型在此基础上进行扩展或细化,这比让它从零开始效果更好。角色扮演能让模型的输出风格、术语使用、乃至思考问题的角度都更有针对性和方向性。

3. 引导模型思考

我们最担心的就是模型“一本正经地胡说八道”。为了避免这种情况,我们需要引导它进行“过程性思考”,即要求它在给出答案的同时,也展示其推理步骤。

  • 在每个关键Prompt中,告诉模型应该“先做什么,再做什么”,例如“先识别名词 -> 再挖掘属性 -> 最后思考关系”,更进一步,可以使用课上给出的RTF框架、ROSES框架或COT模式,更系统性的对提示词进行设计。
  • 这种提示方法,迫使模型遵循一个逻辑严谨的分析路径,使其推理过程白盒化,不仅能显著提升最终答案的质量和可靠性,也便于我们快速定位其思考链路中可能出现的偏差。

4. 输出格式约束

在架构设计中,要求大模型按照特定格式输出能够使得正向建模更加严谨,加快建模速度。

  • 一方面,可以要求大模型输出一些可以用代码表示的图语言,如PlantUML,也可以像课上实验一样,要求所有输出都必须遵循预定义的JSON格式。
  • 约束特定的格式能让模型确切地知道完成的标准是什么,如果是精心设计的工作流,上一步的输出可以无缝成为下一步的输入,能够高效的进行迭代询问,另一方面,结构化的输出(如PlantUML)可以直接被其他工具等直接使用,进一步加快正向建模。

四、架构设计思维

1. 第一单元

第一单元的核心是表达式解析。我的架构采用了“一切皆多项式”(Poly -> Mono -> Expr)的思想,通过统一数据类型来简化运算,符合高内聚低耦合,但这种设计可扩展性不强,类复杂度和方法复杂度均较高,本质上还是面向数据结构和算法的设计,架构思维尚在萌芽阶段。

2. 第二单元

第二单元引入了多线程电梯。架构设计立刻转向了经典的生产者-消费者模式。输入线程是生产者,电梯是消费者,中间通过一个线程安全的请求队列解耦。思维的演进在于从设计数据结构转向了应用成熟的设计模式来解决并发和交互问题,开始关注线程安全、同步互斥和调度策略。

3. 第三单元

第三单元是基于 JML 的社交网络,这一单元引入了JML规格化语言,思维的核心转变为规格与实现分离。架构设计必须严格遵循 JML“做什么”的规定,而性能优化的关键则在于“怎么做”,即选择高效的数据结构(如并查集、自定义链表)和算法(动态维护变量)来满足性能要求。这让我认识到架构不仅要功能正确,还必须为性能服务,是在严格约束下寻求最优解。

4. 第四单元

第四单元是 UML 建模,是架构思维的升华。我第一次完整地实践了模型驱动的顶层设计。思维从“代码怎么写”转变为“系统应该是什么结构”。通过预先设计状态图、顺序图和类图,整个系统的宏观结构和微观交互都变得清晰可见。这是一种自顶向下、先宏观后微观的系统化设计思维,确保了架构的稳定性和一致性。

五、测试思维

1. 第一单元

最初,测试思维比较朴素,依赖手动构造边界数据(如正负号、空白符)和一定的运气。在第三次作业因一个 pos++ 的小错误而翻车后,我深刻认识到自动化测试的必要性,单纯的手动测试在复杂逻辑面前是不可靠的。

2. 第二单元

面对多线程的复杂性,测试思维被迫升级,由于本单元在中测环节我就出现了很多问题,于是我开始编写数据生成器,通过生成大量随机、密集、稀疏的数据来对系统进行压力测试和并发测试。同时,对于评测机的数据生成和正确性判断,随着作业逐渐变得复杂,我对评测机也进行了多次迭代,从简单的字符串匹配演变为模拟电梯状态进行合法性检查,这一过程中,测试思维的演进在于从静态的、孤立的测试用例转向动态的、并发的、状态驱动的测试。

3. 第三单元

JML 单元带来了规格驱动测试的思维。对于一些复杂的、可能出错的方法,我学会了编写 JUnit 单元测试,针对 JML 的前置条件、后置条件和不変式来设计测试用例,验证方法的正确性。同时,本单元我也编写了简单的评测机,通过与同学对拍,用自动化的方式对比不同实现的输出,极大地提高了测试效率和覆盖率。这是一种更加严谨和形式化的测试思维。

4. 第四单元

在第四单元,一方面由于进入了考期时间不足,另一方面,本单元的作业相对于前三个单元复杂度较低,而且与第三单元相比,本单元的全部架构和代码都是我通过正向建模亲手编写的,逻辑性较为严密,虽然我并没有主动进行测试,但整体上没有出bug。

六、课程收获

面向对象构造与设计这一课程到此也接近尾声了,在这一过程中,有中测屡次不过时的焦虑,有互测成功hack时的喜悦,也有强测挂掉时的后悔,四个单元的旅程,我的收获远超预期,不仅在于技术上的提升,更是思维模式上的升华。

  1. 抽象与封装:从第一单元将表达式抽象为多项式,到第四单元将复杂系统抽象为清晰的 UML 模型,我真正理解了如何将现实问题映射为高内聚、低耦合的对象世界。
  2. 系统化开发流程:四个单元中我经历了从“代码驱动”到“模式驱动”,再到“规格驱动”和“模型驱动”的转变,明白了高质量的软件诞生于严谨的设计,而非临时的奇思妙想。
  3. 测试的价值:对于测试,从最初的轻视,到后来的依赖,再到将测试融入设计,我对软件质量保障的理解发生了质的飞跃。自动化测试、单元测试、对拍等方法已成为我工具箱中不可或缺的一部分,同时这也提升了我编写Python脚本的能力。
  4. 代码能力:从一开始只能写百行左右的代码,到最后轻松完成近千行代码,在这门课程的学习过程中,我的代码能力以及调试能力有了巨大的提升。

高质量的软件不仅仅是能用,更是可验证的正确和可维护的清晰,这门课程教会我的,正是如何去打造这样的软件。感谢老师和助教们的辛勤付出,这段充满挑战与成长的经历将是我未来职业生涯中宝贵的财富。