BUAA-OO第四单元总结
一、第四单元设计架构
1.1 第一次作业
第一次作业实现 UML 解析器,支持对 UML 类图的分析,通过输入相应指令查询类图信息。将需要实现的方法放在 MyUserApi 里,构造时完成对 UML 类图元素的解析。由于各个元素之间有引用关系( parentId ),而传入的参数中元素是乱序的,所以不能直接按照传进来的 elements 数组的顺序解析,需要多次遍历,每次遍历解析一类元素,并且要先解析可以被引用的元素。
对于每一类 UmlElement,按照指令需要构建对应的 MyElement,以保存必要的元素信息,对于需要信息不多的元素,只在 MyUserApi 用 HashMap 存放便于查找。除构建 UmlModel 的框架外,根据指令要求建立各元素之间的关系,比如记录类的数量,为类添加子类、父类、属性、方法、实现的接口等。总体上把握完全按照需求写代码的宗旨,逐一实现各个指令,在指令需要时才添加额外信息。
部分指令需要逐层向上遍历如查询继承的接口、属性耦合度、类继承深度等,保证所有查询都只遍历一次,具体实现为:把要查询的量初始化为 null,查询过程中赋值,每次查询判断只有为 null 是才进行,否则直接返回。例如 CLASS_IMPLEMENT_INTERFACE_LIST 指令实现如下:
public List<String> getInterfaceList() {
if (interfaceList == null) {
HashSet<String> temp = new HashSet<String>() {
{
addAll(interfaces);
if (parent != null) {//没有循环继承 parent != null可作为终止条件
addAll(parent.getInterfaceList());
}
}
};
interfaceList = new ArrayList<>();
interfaceList.addAll(temp);
}
return interfaceList;
}
java 的流操作功能强大可以使代码更简洁,如 CLASS_ATTR_COUPLING_DEGREE 的实现:
public int getCoupling() {
if (coupling == -1) {
updRefAttributes(); //获得所有继承来的引用类型属性 无重复
Stream<String> stream = refAttributes.stream();
coupling = (int) stream.filter(s -> (!s.equals(this.getId()))).count(); //先去除引用类型是当前类的属性,再统计数量
}
return coupling;
}
1.2 第二次作业
第二次作业新增了对状态图和顺序图的查询,对UmlElement 的解析仍遵循第一次作业,针对指令保存信息、建立元素之间的联系。
判断关键状态采用暴力dfs:
private int dfs(State state, String delete, HashSet<String> visit) {//state为当前遍历到的状态,delete为需要删除的状态
if (finalStates.containsKey(state.getId())) {
return 1; //存在final state可达,即delete不是关键状态
}
visit.add(state.getId());
for (State s : state.getTargets()) {
String id = s.getId();
if ((!visit.contains(id)) && (!id.equals(delete))) {
int r = dfs(s, delete, visit);
if (r == 1) {
return 1;
}
}
}
return 0;//删除delete状态后,所有final state都不可达,即delete是关键状态
}
1.3 第三次作业
第三次作业增加了对 UML 模型有效性的检查。用 HashSet<Integer> errors
来存放模型中包含的错误类型,checkForUml00x
时,如果 errors.contains(x)
即代表模型存在该类错误。大多数检查可以在前两次作业解析的过程中加入判断来完成对errors 中错误的添加,R003、R004、R009 在所有元素都解析完之后,依次遍历 Class/Interface/State 来判断是否有错。R003 循环继承和 R004 重复继承较复杂,都采用 dfs 方法。
第三次作业很容易代码行数超出 500 行,解决方法是把解析过程放在 PreCheck 类里,MyUserApi 类继承 PreCheck,依然是封装的思想。
二、四个单元中架构设计思维及OO方法理解的演进
第一单元是表达式化简,我最终把形如 a*x**b*sin(element)**c*...*cos(element)**d
的式子作为一个表达式的基本单元 element,然后实现 element 的加减乘除和化简合并。整体上采用训练给出的递归下降方法,先预处理,之后在递归下降的同时以 element 为单位完成计算合并。遗憾的是虽然第一单元训练很适合练习 OO 思维,但由于预习不充分,写出的代码仍然是面向过程的,甚至当时还以为自己写的是很能体现 OO 的代码,等第一单元结束后随着 OO 课的推进才意识到自己第一单元的代码很糟糕。
第二单元是多线程模拟电梯调度,整体上采用生产者-消费者模式维护线程安全,输出采用单例模式。后两次作业的生产者消费者的级数变多。在生产者-消费者模式中,托盘的存取方法、状态修改查询方法都需要加锁确保一个时刻只能有一个线程调用。所有同步块方法中只有取方法中可能会发生阻塞等待,存方法和状态修改方法中都需要有 notifyAll()
以唤醒等待的线程,状态查询方法都不需要加 notifyAll()
,否则可能造成轮询。第二单元的重点不再是 OO 设计结构,我曾试图提取出横向、纵向电梯/调度器的共同点实现电梯基类,但又因横纵电梯实现逻辑稍有不同,用继承反而会更复杂。
第三单元通过 JML 描述模拟社交网络,所有方法的具体操作都由JML明确规定,我们需要做的就是严格遵守规定。形式化语言的表述能保证准确性,但理解起来不像自然语言通俗易懂。这一单元还需要注重算法性能。
第四单元完成 UML 解析器,但只需要我们实现接口中的方法,在设计上要选取合适存储元素方式,建立起各个 UmlElement 的联系和层次结构。因为 checkstyle 的限制,需要把一些方法提取出来封装成新类,通过继承或其他方式调用。
纪老师在理论课上反复强调面向对象的特性:封装、继承、多态。在面向对象在一学期的 OO 课程中,继承、实现接口、重写、重载...代码中很多地方都体现了面向对象的思维,OO 的方法注重从需求里拆分出一个个"对象",每个对象在内部实现自己的功能,对外只提供必要的统一接口,具有模块化、层次化的特点。面向对象的代码可重用性强,也易于维护和拓展。
三、四个单元中测试理解与实践的演进
由于精力、时间有限,本学期没能完成成功的测评机搭建,在各个单元都主要通过手动造数据来测试,毫无疑问手造数据覆盖率低,测试不充分。第一单元手造复杂数据还可以测出bug,但对于第二单元多线程,手造数据基本上没能起到作用。第三单元通过对照指导书要求阅读代码检查测试,JML规格很明确,阅读代码往往能就发现bug。第四单元通过手动画类图,导出模型数据后,用程序根据模型数据生成指令,但手动画类图依然存在费时、测试不充分的问题。很遗憾没能搭建完整的测评机,但在研讨课上听同学们的分享以及课下阅读别人的测评机代码还是学到了很多关于评测的技能,希望以后能有机会搭建自己的测评机。
四、课程收获
- Java 语法知识的丰富
- 掌握面向对象编程思维
- 多种设计模式
- UML类图知识
- 在 checkstyle 帮助下养成良好的代码书写习惯
- 契约式编程的概念
- 提升 debug 技巧
- 保证线程安全的重要性以及实现线程安全的方法
- ....
- 增强抗压能力,锻炼好心态
五、改进建议
- 建议改进 OO 讨论区,让助教的回答紧跟在对应的提问后面,便于迅速查看,否则如果助教没有指明回答哪个问题,定位回答会很麻烦。
- 建议调整四个单元的顺序,刚开学时第一单元第一次作业难度属实大,可以改为unit3、unit1、unit2、unit4,通过 unit3 的规格训练熟练对java知识和OO编程思维的掌握,在后续训练的推进中也能得心应手。否则对于没有接触过面向对象的同学,很容易在第一单元被面向过程的思维禁锢,而在后续单元已经没有高浓度 OO 的训练了。
- 建议在 pre 环节增加 java 多线程的训练。
- 实验课代码训练可以提供很多好的设计思路,可以提前实验课的时间,否则可能走很多弯路。