#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 UmlClassUmlInterfaceUmlAttribute,UmlOperation,UmlParameter UmlGeneralization ,UmlInterfaceRealization,UmlAssociation,UmlAssociationEnd
  • 顺序图中涉及的元素:UmlCollaborationUmlInteractionUmlLifelineUmlEndpointUmlMessage
  • 状态图中涉及的元素:umlStateMachineumlRegionumlEventumlOpaqueBehaviorUmlPseudostateUmlStateUmlFinalStateUmlTransition 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方法,这样就做到了把一个大的任务分给了不同的角色去完成,只要保证每个模块的正确性,就能保证这个大任务的正确性,降低了代码维护的难度

第二单元

电梯单元的主题是多线程编程,通过《图解多线程设计模式》这本书,了解到了多线程的经典设计模式,大部分都是用抽象的角色去实现具体的功能,这些抽象的角色运用到实际的代码中,就是不同的对象。

电梯单元的核心思路就是生产者消费者模式,以及和操作系统课程密切相关的进程的同步和互斥,需要在不同线程对临界区进行访问的时候加锁,以保证线程安全,此外还需要注意waitnotify等函数的使用,避免轮询产生

第三单元

这一单元是按照JML规格来编写代码,模拟社交网络。

JML作为一种形式化语言使遵循了JML规格的代码的正确性,避免了自然语言表述所带来的歧义。

这个单元的内容包括两方面

  • 按照JML规格编写代码,这个过程并不难,甚至考察的内容更偏向于算法优化的方面,而不是面向对象的思维
  • 编写JML代码,要求我们要考虑完备,用精炼正确的规格表达需求

第四单元

最后一个单元有种首尾呼应的感觉,我们整体需要做的框架和第一单元很像,要把元素封装,建立层次结构,把大任务分成子任务,交给不同的类去完成

在编写代码的过程中,可以明显的感受到相比第一单元时更加得心应手了,明白应该把哪些功能封装起来,把哪些具有相同性质的子类进行提取,构建超类。

面向对象的代码把需求分封成了一个个的小方块,每个对象在方块内部实现自己的功能,留给外部可调用的接口和方法,增强了代码的可维护性和健壮性

四个单元中测试理解与实践的演进

由于假期没有做好预习工作,导致这一学期的oo课程都显得有些吃力,并且限于精力和其他科目的压力,始终采用的还是手搓数据的方式。

第一单元复用解析时采取的思路,每次测试一个抽象层面的类,先测试不同的基础因子能否通过测试,再将它们向上构造,利于保证完备性;第二单元靠手搓数据很难构建测试样例,得益于中测的强度,没有在强测中出现大的问题;第三单元中了解到了Junit的工具,并进行了相应的尝试;第四单元主要依靠在StarUml里绘制相应指令的图像,再导出以进行数据测试。

虽然始终没能完成评测机的构建,但通过研讨课和博客了解到了很多大佬构建评测机和测试的心得,希望在下个假期里好好提升自己,学习评测机的构建,在以后的课程中能有所应用。

课程收获

通过一学期的披荆斩棘,我掌握了Java的基础语法,并能够利用这门语言实现一定规模的工程,接触到了面向对象的程序设计思维和方法,了解到了很多设计模式,大大提升了自己的编程能力。

同时复习了很多当时数据结构并没有进行具体实践的图论算法。在多线程单元接触到的知识,为操作系统的进程部分的学习打下了相应的基础,此外,通过checkStyle工具约束了自己的编程习惯,提升了代码的美观和可读性。

(心态和抗压能力继计组之后继续提升

改进建议

  • 希望能够调整课程的顺序,四个单元的难度差异明显。在收假后就加上来的第一单元的压力着实太大了,建议可以调整为UML 第一单元 电梯单元 JML
  • 如果可以的话,可以用几节课介绍评测机的具体构建方法
  • 理论课感觉过于理论了,在与单元作业结合的过程中有点难以衔接,希望理论课和作业能够结合的更为紧密
  • 降低开学难度 降低开学难度 降低开学难度(希望的事情说三遍
posted @ 2022-06-27 17:37  Tian_Kuang  阅读(37)  评论(0编辑  收藏  举报