#BUAA-面向对象设计与构造 ——第四单元总结(UML)#
BUAA-面向对象设计与构造
——第四单元总结(UML)
单元主题
实现UML解析器,使其支持对 UML 类图、状态图和顺序图的分析,可以通过输入相应的指令来进行相关查询,并能根据 UML 规则进行一定的规范性验证。
架构设计
本次作业包含的内容较为繁杂,需要认真阅读指导书,明确作业需求,架构的关键是要理解三种图所包含的元素关系,理清脉络,所有的查询均在图构建完毕之后,因此为静态图。
类似于第一单元表达式的架构,按照层次逐渐分级,建立类图,状态图,顺序图的树。由于输入的UML元素并不能保证其上一级元素在其输入前已完成了输入,因此需要进行两轮元素的循环
- 在第一轮循环中,主要进行分类的工作,将指令以及后续可能会用到的元素以
id
为索引进行存储 - 在第二轮循环中,通过对元素种类的判断,依据
parentId
找出父级元素,建立相应的层次联系
将与某些指令相关的元素进行封装,把初始的UmlElement
作为其中的属性以保留性质,再根据需求添加相应属性,以MyState
为例
public class MyState {
private final UmlState umlState;
private boolean isCritical; // 标识是否为关键状态
private final HashMap<String, HashSet<MyTransition>> toStates;
private final HashMap<String, ArrayList<String>> events;
private final HashSet<MyTransition> allTransitions;
private final HashMap<String, String> triGuard; // 前面为event,后面为Guard
}
- 类图涉及的元素:
UmlModel
UmlClass
,UmlInterface
,UmlAttribute
,UmlOperation
,UmlParameter
UmlGeneralization
,UmlInterfaceRealization
,UmlAssociation
,UmlAssociationEnd
- 顺序图中涉及的元素:
UmlCollaboration
,UmlInteraction
,UmlLifeline
,UmlEndpoint
,UmlMessage
- 状态图中涉及的元素:
umlStateMachine
,umlRegion
,umlEvent
,umlOpaqueBehavior
,UmlPseudostate
,UmlState
,UmlFinalState
,UmlTransition
UmlEvent
难度较大的指令有:
-
找出类实现的全部接口以及类的继承深度
这两个问题的本质是类似的,即如何让接口找到所有继承的父接口,类如何找到所有继承的父类
这里我采用了递归的想法,在上述的第二层循环里,每个类只存储他的直接父类
directFather
,由于接口存在多继承,因此存储所有直接继承的接口List<MyInterface>
有关系:
类的继承深度 = 其直接父类的继承深度+1
接口继承的父接口 = 直接实现的接口 + 这些接口实现的所有接口
类实现的全部接口 = 自己直接实现的 + 所有父类实现的
public void findFathers() { HashMap<String,MyInterface> clone = new HashMap<>(this.fatherInterface); for (String string :clone.keySet()) { MyInterface father = clone.get(string); if (!father.getFindRoot()) { father.findFathers(); // 让父接口寻找它的所有父接口 } this.fatherInterface.putAll(father.getFatherInterface()); // 进行合并 } this.findRoot = true; // 标识此时this.fatherInterface已经包含了所有待求的父接口 }
也可以更改写成迭代的形式,以类寻找父类为例
public void findFathers() { MyClass father = this.directFather; // 正常情况下,不会出现循环继承,因此设置directFather = this MyClass son = this; ArrayList<MyClass> routine = new ArrayList<>(); // 存储找到根节点所经历的路径 routine.add(0, this); while (father != son) { routine.add(0, father); if (father.getFindRoot()) { break; } son = father; father = father.getDirectFather(); } if (father == son) { father.setFindRoot(true); } int i;outine.get(i - 1) // 在routine中有关系:routine.get(i)的father为outine.get(i - 1) for (i = 1; i < routine.size(); i++) { father = routine.get(i - 1); son = routine.get(i); son.setAllFathers(father.getAllFather()); son.setAllRefAttrs(father.getAllRefAttrs()); son.setInterfaces(father.getInterfaces()); son.setFindRoot(true); } routine.get(0).setFindRoot(true); }
显然递归形式的更为简洁和明了。
-
状态图中关键状态的寻找
本质是有向图的遍历,需要经历两次。
这里采用的是宽度优先搜索算法,一次宽度优先搜索可以遍历完一个弱连通分量
第一次从
initialState
出发,看是否可以到达所有的FinalState
第二次同样从
initialState
出发,设置循环,在循环中每次删除(伪)节点,再看是否可以到达所有的FinalState
核心函数算法如下
/* 查找一次 即可找到从某个点出发的所有可到达的节点 */ public void bfsTraversal(String id, HashMap<String, Boolean> visited, HashSet<String> nodes) { Deque<String> queue = new ArrayDeque<>(); // 双向链表,模拟队列 visited.put(id, true); queue.offerFirst(id); while (!queue.isEmpty()) { String cur = queue.pollFirst(); // 从队首出队 nodes.add(cur); for (String nextNode : edges.get(cur)) { // 遍历和cur相连的所有节点,如果他们没有被访问过则自队尾入队 if (!visited.get(nextNode)) { visited.put(nextNode, true); queue.offerLast(nextNode); } } } }
-
对于接口和类是否存在循环继承的检查
可以基于深度优先搜索,将遍历过的节点加入容器中,如果当前遍历的节点已经存在于容器里了,说明图中存在环
public void findCycle(String v, HashMap<String, Boolean> visited, HashSet<UmlClassOrInterface> exc, ArrayList<MyInterface> trace) { MyInterface myInterface = allInterfaces.get(v); if (visited.get(v)) { // v是初始节点 int j; if ((j = trace.indexOf(myInterface)) != -1) { while (j < trace.size()) { // exc为存储所有在环上节点的容器 exc.add(trace.get(j).getUmlInterface()); j++; } return; } return; } visited.put(v, true); trace.add(allInterfaces.get(v)); // 将v添加到路径中 HashMap<String, MyInterface> directFathers = myInterface.getDirectFathers(); for (String str : directFathers.keySet()) { findCycle(str, visited, exc, trace); } // 进行回溯时,退回到前一个结点 trace.remove(trace.size() - 1); }
架构设计思维及OO方法理解的演进
第一单元
第一单元的重点在于层次结构的设计,关键是理解递归下降的实现:
在第一次作业中险些"一main到底",以为自己写出的是面向对象的代码,实际上拆分出来的不同类,都是对代码片段使用的不同函数的封装,并不是不同的对象,所以本质上还是面向过程的代码。
如一个输出类Tostr
,我把所有的输出任务都交给了这一个类来完成,这样的话就会有很多循环和分支的判定,并且难以定位错误的位置,概括而言就是没有"各司其职"。在后续的作业中,我重新提取了因子,在每个类中均重写ToString
方法,这样就做到了把一个大的任务分给了不同的角色去完成,只要保证每个模块的正确性,就能保证这个大任务的正确性,降低了代码维护的难度
第二单元
电梯单元的主题是多线程编程,通过《图解多线程设计模式》这本书,了解到了多线程的经典设计模式,大部分都是用抽象的角色去实现具体的功能,这些抽象的角色运用到实际的代码中,就是不同的对象。
电梯单元的核心思路就是生产者消费者模式,以及和操作系统课程密切相关的进程的同步和互斥
,需要在不同线程对临界区进行访问的时候加锁,以保证线程安全,此外还需要注意wait
,notify
等函数的使用,避免轮询产生
第三单元
这一单元是按照JML规格来编写代码,模拟社交网络。
JML
作为一种形式化语言使遵循了JML
规格的代码的正确性,避免了自然语言表述所带来的歧义。
这个单元的内容包括两方面
- 按照
JML
规格编写代码,这个过程并不难,甚至考察的内容更偏向于算法优化的方面,而不是面向对象的思维 - 编写
JML
代码,要求我们要考虑完备,用精炼正确的规格表达需求
第四单元
最后一个单元有种首尾呼应的感觉,我们整体需要做的框架和第一单元很像,要把元素封装,建立层次结构,把大任务分成子任务,交给不同的类去完成
在编写代码的过程中,可以明显的感受到相比第一单元时更加得心应手了,明白应该把哪些功能封装起来,把哪些具有相同性质的子类进行提取,构建超类。
面向对象的代码把需求分封成了一个个的小方块,每个对象在方块内部实现自己的功能,留给外部可调用的接口和方法,增强了代码的可维护性和健壮性
四个单元中测试理解与实践的演进
由于假期没有做好预习工作,导致这一学期的oo课程都显得有些吃力,并且限于精力和其他科目的压力,始终采用的还是手搓数据的方式。
第一单元复用解析时采取的思路,每次测试一个抽象层面的类,先测试不同的基础因子能否通过测试,再将它们向上构造,利于保证完备性;第二单元靠手搓数据很难构建测试样例,得益于中测的强度,没有在强测中出现大的问题;第三单元中了解到了Junit
的工具,并进行了相应的尝试;第四单元主要依靠在StarUml
里绘制相应指令的图像,再导出以进行数据测试。
虽然始终没能完成评测机的构建,但通过研讨课和博客了解到了很多大佬构建评测机和测试的心得,希望在下个假期里好好提升自己,学习评测机的构建,在以后的课程中能有所应用。
课程收获
通过一学期的披荆斩棘,我掌握了Java
的基础语法,并能够利用这门语言实现一定规模的工程,接触到了面向对象的程序设计思维和方法,了解到了很多设计模式,大大提升了自己的编程能力。
同时复习了很多当时数据结构并没有进行具体实践的图论算法。在多线程单元接触到的知识,为操作系统的进程部分的学习打下了相应的基础,此外,通过checkStyle
工具约束了自己的编程习惯,提升了代码的美观和可读性。
(心态和抗压能力继计组之后继续提升
改进建议
- 希望能够调整课程的顺序,四个单元的难度差异明显。在收假后就加上来的第一单元的压力着实太大了,建议可以调整为
UML
第一单元
电梯单元
JML
- 如果可以的话,可以用几节课介绍评测机的具体构建方法
- 理论课感觉过于理论了,在与单元作业结合的过程中有点难以衔接,希望理论课和作业能够结合的更为紧密
- 降低开学难度 降低开学难度 降低开学难度
(希望的事情说三遍