目录
0. 写在前面
在收到最后一次的强测结果后,悬着的心逐渐放了下来:历时一整个学期的OO课程终于结束了。衷心感谢老师对我们作业提交情况的关系,以及助教耐心答疑解惑,伴我走完这段坎坷的路。
1. 正向建模分析
通过查阅资料,我了解到,正向建模是从需求出发,“采用自顶向下的方式,从全局角度出发,逐步细化到具体的功能和实现细节”的范式。具体到本单元,就是先画类图,再依据类图写代码。
写完最后一次作业时,我分析了这样设计的优点。首先,我回顾了一下第一、二单元和本单元的设计结构,我设计的代码主要都包括以下以下这四个层次,形成一个金字塔的形状:
在第一、二单元中,我通常习惯于先从最底层开始,例如建立一个名为ds
的包,存放所有数据结构(泛型),包括第二单元的多楼层队列MultiFifoMap
和第三单元的无向图UndirectedGraph
等。这样做首先需要在大脑中对作业的架构有一个轮廓,知道要用什么算法和数据结构。而底层的内容一旦成型,向上层编程时就会感到越写越轻松,因为只需组装现有的零件即可。但是,这样做有一个明显的问题,就是如果上层发现设计出现缺陷,例如没考虑某些功能,就要返回修改底层,弃用甚至推翻,造成工作的浪费。
而正向建模有效解决了上述问题。虽然先画类图并没有改变我从底层数据组织开始设计的习惯,但是如果中间发现缺陷后修改的代价会很低。例如,我刚刚拿到作业时,想到既然书架、借还处和预约处都有移动和管理书的职能,那么可以将三者抽象出Storage
统一管理。最初的设计如下:
但是后来设计预约流程时发现预约处的行为和其他两者的行为有很大差异:需要按学生分类且遵循先进先出的结构才能满足指导书的规范。于是放弃了其继承关系。
而设计好了类图,再填代码,在架构的金字塔中无论按照那种顺序都是可以的,因为各个层次都是按照类图作为统一的规范的。
另一方面,至于状态图和顺序图的要求,我还是保留一些质疑,也是为后续课程提供一点建议:毕竟架构都设计好了,总不能因为新增了状态图的要求,为了满足先画图的要求,就依照状态图重写一遍代码吧;顺序图也是如此,又新增了必须使用orederNewBook
等函数名的要求,只能在第三次迭代中把代码和相关类图、状态图的方法全部改名,未免落于“为了画图而画图”的困境。所以这里建议课程组调整状态图和顺序图的要求,在第一次迭代就一起画,这样才能更好地践行UML对代码辅助作用。
2.架构分析
本单元作业的设计特点就是尽可能地简化。我曾经一度崇拜一些精妙的设计,但是后来在OO和其他项目的开发中,发现在工程上,简单的设计并不可耻,因为它意味着更短的周期、更好的可读性和可维护性。
2.1类的建立与数据组织
最终的类图如下:
其中,除了数据表项DataBaseEntry
和Storage
以外,其他类都是静态的单例模式,方便关联。在我的设计中,没有给学生和图书新开类,因为我认为书籍身上没有可提供的操作,在真实的图书馆中图书也只是抽象成了一条数据记录,所以为了模拟真实的图书馆,我设计了DataBase
, 并以DataBaseEntry
作为底层的ADT。而另一方面,由于没有涉及多线程,所有请求都是同步的,也没有理由站在借阅者的视角来进行相关操作,如果增开读者作为对象反而显得冗余。
图书馆最重要的操作就是查找,因此选择HashMap
组织大部分数据即可,例如维护和查询学生是否借阅某本书,使用学号→学生表项;索书号→书籍
即可。
2.2 整理流程
为了设计简便,一切整理都在开馆时,事实证明省了不少麻烦。唯一需要注意的就是要满足以下约束:
- 必须积极整理,即预约处和借还处不能有多余的书;
- 必须尽可能满足预约请求,即向书架到预约处的移动必须在从其他位置向书架的移动之后;
- 必须先扣分后加分。
至于图书馆的规则是在闭馆时刻立即扣分,但是在开馆时处理,只要满足上述三条约束,仍可以满足所有输入和输出规范,这也是“需求与实现分离”的一个体现。而如果晚上做相关的移动,不能在满足上述约束的前提下实现移动次数最少。
具体的整理流程如下图:
3. 四个单元中设计思维的演进
读书可以“先读厚,后读薄”,而我四个单元的设计,是由简单到复杂,而后回归简单。这一方面是课程难度的变化,另一方面也是我对不同设计风格的探索最后改变的结果。
注:unit3代码不含junit测试(大于400行),以及jml。
Unit 1
第一单元的设计,是中规中矩的方法,选用最普遍的语法分析架构,几乎没有设计模式和接口。实现时,由于模型较简单,采用“先构思后代码,先测试后提交”的路线,是唯一每次作业只交一版的一个单元。
Unit 2
第二单元,是最为激进的一次设计,也是唯一出bug互测被刀的一次。从算法上的影子电梯、DFS递归模拟,到设计上的策略模式、状态建模等,再到实现时候大量的函数接口、Lamba表达式和泛型等高级语法。尽管并不完美,这次作业仍然是我一学期的OO设计中最为自豪的一个设计。
更重要的一点是多线程设计出乎意料的顺利。在作业的中测和强测甚至互测中,没有任何的竞争、死锁等情况。现在回想,能达到这种成就归因于如下两点,一是上学期机组的实验建立了并行设计的思维,二是敲代码前通过大量的画图,严密地分析了所有可能的时序问题,代码中从未写出**任何一条多余的synchronized
,在复杂的影子电梯设计中唯一一次嵌套了两层synchronized
。这是非常令我自豪的。
但是,如此庞大复杂的设计,背后是大量用于维护而花费的精力。例如,我用了从状态枚举到相应函数接口规避了“if-else”的语法,虽然代码封装地很漂亮,但是甚至在调试的时候都无法定位函数的调用栈,带来了很大的困难。当然还有复杂的“影子电梯复制逻辑”出现的漏洞。
Unit 3
进入了后半学期,再也没有如此充沛的时间倾注于OO课程的设计,因此我逐渐放弃了曾经追求复杂设计和高级语法的风格,尝试在代码的简化作出尝试和思考。
第三单元抛开JML的外衣,实际上是考验算法的设计。我在设计时思考为什么Java没有提供图等数据结构,能否仿照Java的util包自制泛型,于是泛型的设计在这一单元达到了顶峰:路径压缩、按秩合并O(n)删边的不相交集,支持bfs、维护分支数的无向图是本单元的关键。得益于底层的铺垫,上层社交网络的业务代码非常简单,基本只是消息的传递。
另一方面,在算法涉及时也重点考察了实现难度。例如在优化query_couple_sum
,一种想法是维护,虽然能达到O(1)的查询复杂度,但是由于图中动态变化的元素太多,我果断放弃了这个想法,正是吸取了之前的教训:复杂的设计将为程序埋下隐患。
Unit 4
第四单元面临着两门课程的期末考试,留给OO作业的精力进一步减少,由此催生“简化设计”的理念进一步的发展。如果没有画类图的要求,如果有更充足的时间,我的程序可能还会多上两三个接口。一方面,类图的绘制从主观和客观上促进了类组织的精简,虽不是刻意钻空子,但是没人愿意设计冗余的类来为自己找麻烦。因此,纵观第四单元的类图,没有任何的多余。
第四单元,我也首次采用的敏捷开发的思路,直接立足于当下的需求,没有刻意构想迭代空间,走一步看一步。事实上,前三单元预留的空间就作业的迭代而言没有很大的帮助,因为无法预测规则的变化。这种开发思路也是适应当前unit特征和当前课业压力的,在其他单元并不一定适用。
4. 测试思路的演进
第一单元,由于数据生成较为简单,验证也容易,因此完全采用随机化数据和黑箱测试即可。
第二单元,涉及多线程的测试,第一次互测被刀,让我认识到了数据测试必须要进行压力测试,在短时间内输入大量数据;另一方面,多线程的测试还要在于重复,对可能出问题的数据点使用脚本进行百次或千次重复,不能放弃任何偶然的bug。
第三单元引入的jml和junit引发了我对测试思路的思考。一个程序设计出现的两个gap,一个是实际的需求和开发者想象中的需求之间的gap,另一个则是想象中的需求和实际实现之间的gap。回顾整个OO课程和pre的课程,我设计的代码中主要的bug都出在第一个gap中,例如第三单元有queryAgeVar
的需求中,中间使用int型存储导致精度损失,但是我没有注意到这点,使用
E
(
x
2
)
−
E
(
x
)
2
E(x^2)-E(x)^2
E(x2)−E(x)2计算导致结果与需求不符。如果面向自己的设计进行测试,永远也找不出来这个bug。因此,个人认为大数据量的黑箱测试,对于本课程,才是最高效的测试。
第四单元基本沿用前三单元的测试思路,不再赘述。
5. 课程收获
-
课程中明显的收获就是第二单元的多线程的编程,学会了对象之间异步的信息交互。更重要的一点,是让我认识到了程序设计远不止曾经的所学,而是还有很长的路要走,Java虚拟机的底层原理还有待探索;
-
通过OO课程的训练,我还收获了清晰的设计思路。我在先导课及以前都是直接写代码,走一步看一步,经常出现的大面积重构。经过电梯等复杂模型的设计,以及第四单元的正向建模的训练,写代码时都能拥有一个整体性的思维,能够知道每一步的意义,即使是在搁置一段时间后继续开发也能保持思维的连续。这对开发的效率和质量的提高都有很大的帮助。
-
另一点收获,是在不同场景的设计中逐渐形成了一套自己的编程风格。整个课程中,我一直持有多学习、多尝试的观念,我不想拿着先导课和大一程序设计基础所学的那些基本语法闭门造车,而是选择查阅一些能满足当下需求的高级语言特性,在尝试后选择接收还是放弃,使代码写得更简洁。
-
课程中还体验了不同的测试思路,接触了Junit测试,jml以及uml等技术和语言。