272
社区成员




伴随着四个单元的不断迭代与挑战,面向对象程序设计课程也终于接近了尾声。从起初的表达式求值到图模型处理、再到多线程模拟、最终在复杂业务规则驱动的图书馆系统中落地,这门课程逐步引导我们逐步成长为一个面向对象的系统设计者。每个单元都不仅仅是编程能力的锻炼,更是面向对象思想和建模能力的洗礼。
本单元作为课程的最后一项作业任务,要求我们搭建了一个小型的图书馆模拟系统,并且是在基于类图、状态图与顺序图的基础上实现代码,考验了我们对系统行为建模的能力,对模块之间的协作逻辑、状态管理一致性也提出了更高要求。
这一单元是我很喜欢的一个单元,虽然一开始使用一个新工具去设计类图时,我感到不知所措。
在以前的博客要求中我经常见到给出代码的UML类图这样的描述,但我从来没有意识到过这个所谓类图的重要性,一直将其视为写完代码之后可以自动生成的代码架构的一种表现形式。
在这一单元的探索与实践中,我终于认识到——原来UML类图应该是一个项目的起点,而不是终点。
我选择的正向建模策略是:
User
(用户)、Book
(图书)、AppointmentOffice
(预约处)、LibrarySystem
(统一管理系统)等。先来看最终类图——
我的图书馆系统实现主要划分为以下几大模块:
用户相关信息和操作集中在 User
类中,用户拥有借阅书、预约书、当前积分等状态字段;通过 UserContainer
管理用户全体,提供按学号检索、统一遍历等接口。信用分系统的增减逻辑与操作行为绑定,统一在 CreditScore
内处理。
图书由 Book
类建模,包含类属信息(类别号、序列号、副本号)、当前位置、移动历史等。系统对每一本书的物理副本都做精细建模,并在借阅、预约、阅读等行为中精确移动。书籍的位置变化由 Trace
类记录轨迹,用于支持“查询轨迹”类请求。
所有可容纳图书的位置模块(如书架、借还处、阅览室、预约处等)都实现了统一的 BookHolder
接口,其统一暴露的 addBook(Book)
和 removeBook(Book)
方法用于图书移动。这些模块分别由如 Bookshelf
、HotBookShelf
、BorrowReturnOffice
、ReadingRoom
、AppointmentOffice
等类实现,具备各自规则逻辑。
同时,所有书籍归属单元(如位置与用户)都被抽象为 LibraryUnit
的子类,从而保证所有“位置变化”都有统一的状态记录机制。
图书馆的复杂规则体系,如预约限制、借阅数量上限、信用等级权限、逾期判断等被集中封装在 PermitChecker
类中。这个模块充当了系统的“守门人”,避免了在多个地方重复书写判断逻辑,极大提升了可维护性。
LibrarySystem
类作为主控制器,负责接受输入、调度模块并生成统一输出。它实现了一个事件驱动风格的架构,主逻辑由“开馆 / 请求 / 闭馆”驱动,分别唤起图书整理、用户操作、状态更新等行为。
为了实现对图书馆中多个存放区域(如书架、阅览室、预约处、借还处等)的统一管理,我特别设计了一个通用的容器抽象结构:BookContainer
。
该类封装了一组对图书集合进行增删、查询、遍历等操作的通用逻辑,内部使用 ArrayList<Book>
存储图书,并提供如:
addBook(Book book)
removeBook(Book book)
getBooksByIsbn(String isbn)
getBookByIsbn(String isbn)
(可选返回一个余本)contains(Book book)
等方法这些方法为多个 BookHolder
类的子模块提供了基础能力,避免了大量冗余代码。
BookContainer
还通过引入 Java 函数式接口 Predicate<Book>
的方式,实现了对图书的灵活筛选机制。我设计了如下两个函数,分别用于获取所有符合条件的图书或任意一本图书:
public List<Book> getAllMatchingBooks(Predicate<Book> condition);
public Book getAnyFreeCopy(Predicate<Book> condition);
借助这两个方法,在整理流程或预约处理等模块中,我可以通过传入简单的 Lambda 表达式快速筛选出:
这不仅提升了代码的表达力与可读性,更将复杂逻辑从业务模块中抽离出来,大大便利了我的代码实现与之后的拓展。
我的类图和实际代码在整体架构和细节上大致保持一致,但也经历了从抽象到具体的双向修正过程。
我在项目初期就确立了几个关键的抽象实体,如 User
、Book
、AppointmentOffice
、LibrarySystem
等,并通过 BookHolder
接口统一抽象了图书位置的行为逻辑。在代码实现中,这些类与接口基本保持了类图中的层级结构与继承关系,例如:
Bookshelf
、HotBookShelf
、ReadingRoom
等类均实现了 BookHolder
接口;LibraryUnit
成为统一的父类,承担图书位置变更所需的共享字段和方法;PermitChecker
用于处理权限判断,实际在代码中也独立成类,成为多个模块共享的逻辑判断工具。可以说,在大部分结构性设计上,我的代码是严格追踪类图的产物。
虽然类图为我搭建起了基础架构,但其实大部分具体方法仍为实现出来,其中类之间的交互关系与一些细节需求在空想过程中并不能直观地显现出来。
在实现类图的过程中,我就及时回头修改类图,将新增的方法和属性补充进去,尽量保证建模与实现之间的对应关系。
正因为前期类图梳理了系统的模块职责与耦合边界,使得后期我在编码时可以“按模块完成”而非“按功能堆叠”。比如预约相关逻辑几乎集中在 AppointmentOffice
与 OrderRequest
中实现,权限判断几乎全部委托 PermitChecker
,而 LibrarySystem
则只是调用各模块对输入进行调度。
维护好类图也让我在面对后期复杂的测试数据(如预约+整理+取书+信用扣分等流程)时,能准确根据类图中设计的关系判断出某一问题应该归责于哪个模块。
其实我更青睐大模型的代码补全能力,而不是架构设计能力。大模型的确可以迅速阅读指导书并进行迅速的实体抽象,但是我依然觉得它的设计与推理能力并不如我自己认真去思考。
在这一过程中我使用大模型辅助正向建模基本上都是在自己设计中向大模型提出我的想法,并听取大模型的建议与评价,辅助我评估我的架构设计。
感觉大模型并不擅长个性化地设计,而是擅长设计本身就有一定模式的可复用的类,例如容器。我设计容器时也询问了大模型怎样设计一个容器是合理的,大模型给出了一套比我想象中完善许多的方法字段,完善地让我吃惊,大大加速了我的容器设计。
大模型更多地用于我的代码实现,例如对于一些容器add
,remove
方法,对于一些实体类的hashcode
,equals
这类实现方式有一定模式的方法,我就可以让大模型为我生成,其中效果也非常好。
犹记得第一个单元我写博客时锐评自己的代码是依托答辩,当时每一次功能迭代我都要吭哧吭哧写将二十几个小时改代码。但到第四个单元我看向自己的类图时,已经非常自豪,我的每一次功能迭代连上画新的UML图总共只需要不到3个小时。
在这四次作业,我的架构设计能力的增长是非常可观的。
那时我对“架构”还没有形成清晰概念。所有的类几乎都是为了解决某一局部问题而仓促设立,职责划分不清,调用路径混乱,连类名的语义性都稍显模糊。我只能在实现的过程中不断 patch,不断缺什么补什么。功能实现固然达成了,但任何一个变更都会牵一发而动全身,整体的可维护性堪忧。
可以说,这一阶段我还处于“写代码”而非“设计系统”的阶段。
第二单元的电梯系统是我第一次正视“模块协作”这个问题。多线程场景让我意识到,一个程序不能再是顺序执行的脚本,而是多个实体并行协作的整体系统。我开始尝试抽出公共逻辑、封装状态、在类之间建立清晰的边界。例如请求队列、共享状态、电梯运行逻辑、调度控制开始以对象形式呈现,线程间通过明确接口进行通信。
尽管其中还是出现了不少逻辑交叉、耦合混乱的 bug,但我在调试痛苦中逐渐体会到“职责分离”的意义。架构在这一单元不再是附带思考,而成为我主动设计的一部分。
这次是以JML规格为主导的代码实现。它让我看到,什么是一个好的模型架构,我更多考虑地是代码如何实现而不是去设计架构。
这个单元,是一个学习的过程,为我树立了一个先锋模范。
第四单元要求我们使用UML建模。图书馆系统的构建,是我第一次真正以建模作为起点去组织和规划代码结构。面对层层叠加的业务规则与错综复杂的模块交互,我不再急于编码,而是用类图搭建出整个系统的骨架,从整体视角思考系统的职责划分与运行逻辑。从“以实现为导向”的开发方式,转向了“以模型为驱动”的系统设计。
在这个项目中,我尝试成为一个机制的设计者。从 BookHolder
对位置模块的统一抽象,到 PermitChecker
中权限判断逻辑的集中封装;从灵活高效的 BookContainer
抽象容器设计,到 CreditScore
对信用系统的规则建模——每一个模块都不再只是某个功能点的临时实现,而是承载着一类职责的、可拓展的结构单元。
我觉得我逐渐触碰到了面向对象思想的精髓:
让每个模块只关心它该关心的事
用层次结构分解复杂功能,把不变的抽象出来,把变化的封装起来
各模块各司其职,彼此协作通信,让系统有序运转
中测十几行,强测几千行的评测方法可以说让我每一次作业都焦虑无比,每一次周二打开页面查看强测成绩时我都心惊胆战。
四次作业,我的测试方法都各不一致。
第一单元还是我比较自信的时候。我没有考虑搭建评测机这样的事情,但是我注意到,如果能够写出一个把表达式化至相对最简的java项目,就可以用该项目对拍所有人的代码实现。
我主要还是与同学协作,互相分享彼此捏的数据,互相测试代码,进行一个简单的对拍。
这一单元把我盲目的自信干得稀碎。
多线程可选策略的单元设计是没办法对拍的,但是我注意到这一单元有明确的正确性约束。在这一单元我开始自己搭建评测机,基本思路就是借助睿睿助教给的数据头危机,生成输入数据文件后将该文件重定位到标准输入,抓取标准输出到文件,根据输出模拟电梯与乘客状态,根据该状态结合各指令的正确性约束规则判断输出的正误。
相当于进行一个黑盒测试。被前两次作业打得痛哭流涕之后,我完全手搓了这个评测机,其实也是我头一次设计出相对较好的代码架构,在这里贴出我的一个简洁的类图:
这一单元我们可谓是学习了各类不同的测试思想。虽然最后这一单元的博客我们探讨了所谓单元测试,功能测试,集成测试等不同测试方法。但是其实这一单元我对所谓黑白灰盒测试学习理解到的东西最多:
测试方法 | |
---|---|
黑盒测试 | 测试者完全不看代码,只根据规格说明设计输入,观察输出是否符合预期。比如说明文档说“输入 A,应该返回 B”,那就输入各种合法、非法、边界值,看输出对不对。它把系统当作黑箱,靠输入输出的对应关系来验证正确性。 |
白盒测试 | 测试者深入代码内部,分析每个分支、路径、循环、变量变化,设计输入去覆盖所有代码执行路径。例如使用语句覆盖、分支覆盖等手段确保每一段代码都运行过。它通过代码结构指导测试设计,追求“内部逻辑全跑一遍”。 |
灰盒测试 | 测试者虽不完全依赖代码细节,但了解系统的设计结构,如模块职责、接口定义、数据流向。他会结合这些内部信息设计更聪明的黑盒测试用例,比如专门测一个接口在边界数据下的行为。 它借助知道一点内部的优势,对外部行为进行有针对性的验证。 |
这一单元里,我不仅编写了作业要求的JUnit的测试,也写了一个对拍评测机,这一次作业所有人的作业输出具有唯一性,可以走对拍测试的方法。我采取的数据编写策略大概是类似“灰盒测试”一些。我在评测机内部对指令的生成比例进行了控制,这些比例都是可调的,让我可以进行各个方面的压力测试与集成测试。
我不仅控制生成指令类别的控制,也对指令正常与异常以及什么类型的异常这样的比例进行了控制。我的生成器内部维护了多个容器(如 persons
、tags
、messages
、emoji_ids
等),模拟了系统应该拥有的状态,每一条指令的生成都基于这些状态是否合法。我会以随机的方式并根据一定的比例,确定这一次选择生成规格中哪一条分支的相应指令,然后基于当前状态进行正确的构造。例如 addMessage
的生成器中覆盖了重复 ID、非法 tag、sender 与 receiver 相同等所有异常情况。每条合法指令执行后还会同步更新状态容器。
这就是在一定程度上输入数据可控的灰盒测试,对我的代码测试产生了很大的帮助。也让我进一步掌握了自动化测试的能力。
这一单元又回到最原始的状态了。因为懒还有对架构的信任,我依然走了根据指导书内容手搓测试数据的方法,可谓是返璞归真。
但是由于之前的测试经验,我不再像第一次作业时那么盲目,而是开始思考覆盖每一个分支的测试。这倒是更像一个白盒测试了。
非常感慨,面向对象课程,最终来到了尾声。
一路走来,真的非常不容易,在此也要特别感谢助教与老师们的辛苦努力,与同学之间的互帮互助。
我在旅途中的收获,也是很多。
我终于触碰到了所谓”高内聚,低耦合“的精髓,领悟到了什么是面向对象。
与同学之间的协作也让我无比快乐,一次讨论、一轮复盘、一次深夜联调,都是架构落地的一部分。
也是逐渐发现,所谓评测机并没有那么高深,原来仔细设计,我也可以搭得不错。
大模型更是成为了我的良师益友,加速了我的编程效率。
既然已经到了这里,那么——
完结!撒花!!!!