BUAA_OO_Unit4阶段总结暨OO课程总结
OO_Unit4阶段总结暨OO课程总结
前言
四个月的OO课程(四个月的折磨)终于迎来了尾声,在这侧重分明但又相互联系的四个单元中,我在同学以及助教团队的帮助下克服了一个又一个困难,收获颇丰。
第四单元的正向建模与开发
先上类图
有一说一,这是我第一次在单元总结贴上自己画的类图,而不是照抄idea生成的类图,
有点感动
本单元作业以现实中的图书馆管理系统为背景,在其中融入了“借书”“预约书”“捐赠书”“信誉分”等操作。
我将题目中的“学生”“借还处”“书架”“预约台”“图书漂流角”等类抽象成了BookContainer
这个接口,在其中定义了几个共性的方法,如下所示
public interface BookContainer {
int queryBook(LibraryBookId libraryBookId);
void removeBook(LibraryBookId libraryBookId);
void addBook(LibraryBookId libraryBookId);
}
然后我在使这些类实现这个接口。由于各个类的运行逻辑并不完全相同,因此各个类的接口实现也大相径庭。
例如在BookShelf
类中,需要对初始图书类型以及数目进行初始化,因此需要init()
方法;在BookShelf
类中,存储书的数据类型是一个“ID对数目”的HashMap
,而在Student
类中,存储书的数据类型是一个"ID对日期"的HashMap
,因此addBook
的实现也有所不同
其中BookShelf
,AppointmnetOffice
,BookDriftCorner
,BorrowAndReturnOffice
采用了单例模式,并提供了静态变量方便外部类访问,如下
// 以BookShelf为例
public static final BookShelf BOOKSHELF = new BookShelf();
private BookShelf() {
}
这些采取了单例模式的类被组装到Library
类中,并在Library
类中统一进行访问和控制。Library主体代码如下
public void run() {
init();
while (true) {
LibraryCommand command = LibrarySystem.SCANNER.nextCommand();
if (command == null) {
break;
}
LocalDate date = command.getDate();
if (command instanceof LibraryOpenCmd) {
open(date);
} else if (command instanceof LibraryCloseCmd) {
close(date);
} else {
if (command instanceof LibraryQcsCmd) {
queryBook(command);
continue;
}
LibraryReqCmd req = (LibraryReqCmd) command;
LibraryRequest.Type type = req.getType();
if (type == LibraryRequest.Type.QUERIED) {
queryBook(command);
} else if (type == LibraryRequest.Type.BORROWED) {
borrowBook(command);
} else if (type == LibraryRequest.Type.RETURNED) {
returnBook(command);
} else if (type == LibraryRequest.Type.ORDERED) {
orderNewBook(command);
} else if (type == LibraryRequest.Type.PICKED) {
pickBook(command);
} else if (type == LibraryRequest.Type.RENEWED) {
renewBook(command);
} else if (type == LibraryRequest.Type.DONATED) {
donateBook(command);
}
}
}
}
这样的设计层次分明,可拓展性强,为我的编程过程提供了莫大的便利。
代码设计和UML模型设计
说实话,在最开始做本单元作业时,我的的确确想像课程组推荐的那样,先画好比较完备的UML类图后再动手码代码。然而,就当我花了整整一个晚上画好UML类图后,却发现很多代码的实现难以和预先画好的类图相匹配,而且课程组提供的官方包过于强大,使得我在类图上的许多内容变得不必要。如果坚持先画UML类图再码代码,那么为了适配代码的变化需要频繁地回过头来修改类图,效率大大降低。
所以最后我屈服了,我开始写码代码再画图,一般是花一个晚上码代码,花一个晚上画图。
由于是先码代码再画图,相当于先有箭后有靶子,因此代码设计和UML模型设计的追踪关系也就无从谈起了
不过由于我在码代码前就已经基本规划好了大致的架构,码代码和画UML图时具有思维上的连贯性,因此事实上代码和UML图的一致性保持地比较好。
状态图
类图在上文已经提供并分析过了,以下是状态图,画的比较丑,请见谅
本状态图追踪的是“书”的状态,即书所在的位置
个人认为“逻辑上状态图还是比较清晰的”,以ao
到各个其它直接相连的状态为例
- 调用
pickBook()
方法后,书有可能被预约的学生取走; - 调用
open()
方法后,书有可能在开馆的图书整理中被从书架移动到预约台,也有可能从预约台移动到书架;
但是相信你们也能够看出来,本状态图存在极大的漏洞,即“对于Trigger相同的方法,没有加guard条件加以区分”,其实我也想加上guard条件,但是课程组所给的guard条件的约束过于苛刻,要求guard条件的填写符合一定的文法,且guard条件中出现的所有变量必须是某个类的成员变量。尤其是第二个条件,若我想要满足第二个条件,则必须大规模修改我的整体架构,在其中加上一堆“原本并不必要”的代码。这也是我认为绘制状态图要求的不合理之处。在权衡之下,我决定牺牲状态图一定的可读性而换取更好的架构完整性。
顺序图
以下是顺序图
说实话,我觉得在一个单线程的程序中画顺序图挺奇怪的,而且事实上我并不认为这些类中传递了什么消息,顶多就是方法间的嵌套调用存在顺序关系。因此课程组的“绘制状态图”的要求一度让我感到为难。
但是好在课程组只要求对借书过程中的orderNewBook()
和getOrderBook()
两个过程进行绘制,这两个过程还算结构清晰(课程组,你好善良),因此绘制起来难度并不大。
我以orderNewBook()
过程为例,介绍顺序图的逻辑
- Library收到请求,调用
orderNewBook()
方法,“提醒”BookShelf
; BookShelf
收到“提醒”,orderNewBook()
方法调用结束;- 在开馆整理中,调用
AppointmentOffice
的addBook()
方法,对应书籍移动到AppointmentOffice
中;
四个单元中架构设计思维的演进
在第一个单元,也许是由于初次使用面向对象的思想处理比较大型的项目,因此架构设计并不成熟。例如我没有对具体的数字,变量,指数函数和表达式,项,因子进行进一步的抽象;我甚至还将所有与计算相关的方法全部放在Cal
类中,造成代码臃肿。这都是不成熟的体现;
在第二单元,我开始有意识地进行架构设计,然而由于我“思维比较独特”,我并没有设计类似于“输入流”的类,然而将“添加输入”的任务下放到每一个电梯对象之中。这在第五次作业中给我带来了便利,但是在第七次作业中我因这种设计造成了大量死锁bug,而且由于复现困难,我不得不花费整个清明假期去debug。但总体而言,我在第二单元中形成了自认为比较成熟的设计思维
第三单元聚焦于规格化设计,并不存在什么设计架构思维,深究起来,也许将点集抽象成并查集算架构设计?就此略过。
第四单元架构比较成熟,也是我个人目前最为满意的架构设计。架构在上文已作说明,不再赘述。
四个单元中测试思维的演进
上完OO四个单元,我最大的感受之一便是**“评测机真的很重要”**,我个人也与gpf同学合作开发评测机,评测机项目已在github开源
项目地址:solor-wind/BUAA_OO_TEST: BUAA OO课程的评测机 (github.com)
麻烦点个star,谢谢喵~~ (๑ơ ₃ ơ)♥
在这个过程中,我们通过合作开发代码,让我积累了许多团队协作方面的经验,让我受益良多。
说会正题,你在写完代码后不会知道你的程序有多少Bug,因此大量的评测十分重要
在搭评测机的过程中,我们会刻意避免使用与java程序一样的思路(毕竟如果我java都写错了,难道python还能写个对的?),在第一单元中我们使用了sympy
库进行正确性检验,在第二、四单元我们使用模拟进行正确性检验,在第三单元我们使用了XNetwork
这个python中用于计算图论的库进行正确性检验。
至于数据生成,我们将“调整数据生成参数”这一过程下放给用户,用户可以调整config.json
来改变测试的侧重点和强度,以下是第二单元的config.json
示例
{
"in_path": "input.exe", // 官方数据投喂器路径
"jar_path": "myJar.jar", // jar路径
"delete_temp_files": true, // 是否删除临时文件,用于检验数据生成强度
"set_clock": true, // 是否定时
"clock_time": 120, // 定时时间
"my_input" : false, // 是否采用自定义的输入
"max_thread_num": 20, // 最大同时运行评测线程数目
"test_num": 1000, // 测试次数
"time_limit": 20, // 数据所给的时间范围
"command_limit": 200, // 指令条数
"fault_tolerance": 0.01,
"elevator_num": 6, // 电梯个数
"default_floor": 1, // 默认初始楼层
"min_floor": 1, // 最底层
"max_floor": 11, // 最高层
"move_time": 0.4, // 每层移动时间
"open_time": 0.2, // 开门时间
"close_time": 0.2, // 关门时间
"capacity": 6, // 电梯容量
"reset_prob": 0.1, // 生成reset指令的概率
"DC_reset_prob": 0.3 // 在生成reset指令的前提下,生成DC_reset指令的概率
}
生成数据也是一门学问。在第一单元和第二单元中尚且可以随机生成,但第三单元和第四单元则必须“刻意”地进行数据生成。在第三单元中,我们采取了“将生成图的过程拆分,并随机再排序”的方式基本保证了生成数据的强度;在第四单元中我们则采取了“交互式”评测,依据用户程序返回的输出动态调整输入,也取得了不错的效果。
但是评测机并不是万能的,它无法覆盖一些极端的,边缘的数据。我在第二单元的互测中就因“评测机测试一万次未出现bug”而过于自信,被极端数据hack导致互测失分。因此,我认为,在使用评测机大量评测的前提下,手动捏造极端数据进行测试才是进行测试的正确方式。
OO课程收获
总体而言,我在OO课收获很多。
- 在第一单元中,我掌握了面向对象的基本思维和程序的架构设计;
- 在第二单元中,我基本掌握了多线程程序的特点以及编程方法,学会了“对抗”死锁的方法;
- 在第三单元中,我掌握了规格化设计的方法,学会了“戴着镣铐跳舞”,并且使用Junit对程序进行测试;
- 在第四单元中,我则学会了基本的UML图的绘制方法,懂得了保持程序架构和UML图的一致性;
每一次克服课程组布置的挑战,看到在强测中取得的好成绩时,我的心头总会涌起一股激动的暖流;而在课下同学们相互交流,彼此出谋划策攻克难关也让我感慨万分。
我想,这也是这门课的魅力所在。