BUAA OO 期末总结

架构设计:

第四单元主要介绍了UML解析相关知识。总的来看,三次作业是迭代式的设计,逐步完善这个UML解析器的功能。

hw13要求仅针对类图进行解析,hw14中加入了时序图和状态图,hw15中增加了对UML的相关检验。

我的三次作业在实现的时候基本没有改动,所以我就迭代的来介绍相关的架构。

第一次作业:

实现一个 UML 解析器,使其支持对 UML 类图的分析,可以通过输入相应的指令来进行相关查询。

主要实现的类是MyImplementation.java。首先,注意到各种元素之间呈现树形关系,因此我采取的策略是利用树型结构对UMLElement进行读入并存储。采取这种方案,可以利用UmlElement之间的天然树状结构。

First Second Third
UML_CLASS,UML_INTERFACE UML_ATTRIBUTE, UML_OPERATION, UML_REALIZATION UML_GENERALIZATION,UML_INTERFACE_REALIZATION,UML_PARAMETER
// 树型遍历结构。
public MyImplementation(UmlElement... elements) {
        ArrayList<UmlElement> ClassOne = new ArrayList<>();
        ArrayList<UmlElement> ClassTwo = new ArrayList<>();
        //UML_CLASS、UML_INTERFACE和UML_ASSOCIATION
        for (UmlElement e : elements) {
            idToname.put(e.getId(), e.getName());
            switch (e.getElementType()) {
                case UML_CLASS:
                    break;
                case UML_INTERFACE:
            }
        }
        //UML_ATTRIBUTE、UML_OPERATION、UML_ASSOCIATION_END
        for (UmlElement e : ClassOne) {
            switch (e.getElementType()) {
                case UML_ATTRIBUTE:
                case UML_OPERATION:
            }
        }
        //UML_PARAMETER、UML_INTERFACE_REALIZATION、UML_GENERALIZATION。
        for (UmlElement e : ClassTwo) {
                    switch (e.getElementType()) {
            case UML_GENERALIZATION:
            case UML_INTERFACE_REALIZATION:
            case UML_PARAMETER:
        }

        }
    }

选择容器:

在存储信息方面,我基本都选择了HashMap。但是这里比较麻烦的就是,我们选择的存储方式一定要和需求紧密联系,也就是说,确定了在每条指令的需求,也就是大概应该如何实现后,我们才能确定最终的存储方式,而不能一上来就盲目的进行选择。比如下面的例子:

这里之所以选用 HashMap<String, HashMap<String, ArrayList<UmlOperation>>> 来存储操作是因为:

首先,操作都是类的操作,所以第一个Key需要是类的ID,其次:由于各条指令都是针对名字来进行相关查找或计算的比如getClassOperationCouplingDegree。所以需要用名字来作为第二个key。同时由于会出现重名的方法,所以需要采用ArrayList来存储同名的方法。

    private HashMap<String, HashMap<String, UmlAttribute>>
            eattribute = new HashMap<>(); // key1:classId,key2:name,接着是元素。
    private HashMap<String, HashMap<String, ArrayList<UmlOperation>>>
            eoperation = new HashMap<>(); // key1:classId,key2:name,
    private HashMap<String, HashMap<String, UmlParameter>>
            eparameter = new HashMap<>();//key1:方法id key2:参数id.方法的参数。

指令的实现:

在具体实现的时候,为了怕代码过于臃肿,我选择新建了一个工具类Tools,通过函数调用来简化一些指令的实现。

  1. 判断重复操作:首先需要明白参数列表相同的概念。两个操作的参数列表顺序可能不同,但只要两组传入参数之间存在某一一映射使得类型相同,就可以被判定为重复操作。

    在检查是否是错误类型或者是否是重复类型的时候,直接通过新实例化的工具类即可。

     			for (int i = 0; i < n; i++) {
                    String operationId = eoperation.get(id).get(methodName).get(i).getId();
                    if (!tools.checkError(className, methodName, operationId)) {
                        throw new MethodWrongTypeException(className, methodName);
                    }
                }
                // 判断是否重复操作。参数列表需要相同。
                for (int i = 0; i < n; i++) {
                    String operationId1 = eoperation.get(id).get(methodName).get(i).getId();
                    for (int j = i + 1; j < n; j++) {
                        String operationId2 = eoperation.get(id).get(methodName).get(j).getId();
                        if (!tools.checkDup(className, methodName, operationId1, operationId2)) {
                            throw new MethodDuplicatedException(className, methodName);
                        }
                    }
                }
    
  2. 查找类实现的全部接口。我采用了最朴素的bfs查找的方式。在存储信息的时候,记录下来每个类或接口继承了那些类或接口,然后一层一层向上寻找即可。同样,这里是由需求决定实现方式。需要先观察有哪些需求。

    private HashMap<String, ArrayList<String>> extendClass = new HashMap<>();
    

第二次作业:

第二次作业新增了对UML状态图和顺序图的相关指令,对于类图方面的需求基本没有变化。有了第一次作业的基础,尤其是建好了树形的结构,这次作业就简单了很多,同样采取多次遍历提取元素的方式即可。

在存储信息时,也是按照由需求来决定实现的原则,先大概分析指令需要什么样的存储方式,再去实现。

整体架构:

除了一些琐碎的工具类,或者用于存储信息的自定义类等等。在该次作业中,我新增了一下主要两个类:

我把顺序图、状态图的架构也加入到MyImplementation中,并且在MyImplementation中通过实例化直接调用这两个类的方法即可。用MyStateMachineMyInteraction分别实现状态图、顺序图。

    public MyImplementation(UmlElement... elements) {
        myinteraction = new MyInteraction(elements);	// 顺序图
        myclass = new MyClass(elements);	// 类图
        myState = new MyStateMachine(elements);		// 状态图
    }

    @Override
    public int getClassCount() {
        return myclass.getClassCount();	// 接收到指令的时候,直接调用方法即可。
    }

    @Override
    public int getClassSubClassCount(String className)
            throws ClassNotFoundException, ClassDuplicatedException {
        return myclass.getClassSubClassCount(className);	// 接收到指令的时候,直接调用方法即可。
    }

树型结构:

基本可以延续第一次作业的架构,仿照着来写。

但是需要注意。有些元素我们需要判断是类图的还是顺序图的。比如:UmlAttribute 。否则容易出现访问Null元素的bug。

难点实现:

判断关键状态:判断关键状态时,我采用的方法是使用两遍BFS:首先从起点开始BFS看是否可以到达任意一个终点;然后去掉查询点再dfs查看是否可以到达任意终点。

我在工具类中实现了 checkCriticalPoint 方法。

private boolean checkCriticalPoint(String stateMachineName, String stateName) {	// 删除了一些细节代码
        queue.add(initialId);
        visit.add(initialId);
    // 第一次BFS:
        while (!queue.isEmpty()) {
            String temp = queue.poll();
            visit.add(temp);
            if (stateEdge.containsKey(temp)) {
                for (String key : stateEdge.get(temp)) {
                    if (!visit.contains(key)) {
                        queue.add(key);
                        if (finalState.containsKey(machineId)
                                && finalState.get(machineId).contains(key)) {
                            flag = 1;
                            break;
                        }
                    }
                }
            }
        }
        if (flag == 1) {
            HashSet<String> vis = new HashSet<>();
            queue = new LinkedList<>();
            vis.add(pointId);
            vis.add(initialId);
            queue.add(initialId);
            // 第二次BFS
            while (!queue.isEmpty()) {
                String temp = queue.poll();
                vis.add(temp);
                if (stateEdge.containsKey(temp)) {
                    for (String key : stateEdge.get(temp)) {
                        if (!vis.contains(key)) {
                            queue.add(key);
                            if (finalState.get(machineId).contains(key)) {
                                return false;
                            }
                        }
                    }
                }
            }
        }
        return true;    // 一直没有return,说明是关键节点。
    }

第三次作业

第三次作业在前两次的基础上迭代增加了模型的错误及异常检查,如循环继承、重复继承等等,总的来说实现的难度并不高。

整体架构:

在第三次作业中,我增加了一个MyUmlStandardPreCheck.java的类,用于实现各种check指令,并对一些信息进行存储。

  • 判断循环继承:循环继承要求我们输出在出现循环的所有类或接口,也就是求强连通分量。但是这个方法对事件复杂度的要求并不高,因此我才用的是深度优先搜索,而没有使用其他算法比如tarjan。但是需要注意的是:自己继承自己的情况也需要特殊考虑。
  • 判断重复继承:使用深度优先搜索。对每一个类或接口向上递归查找,如果发现有重复的继承,就加入ans。

架构设计思维及OO方法理解的演进:

第一单元:

第一单元的主题是层次化设计,难点在于对表达式进行解析。在第一单元的学习中,首先面向过程式变编程的习惯总是在不断的作祟。然后老师上课的时候也反复和我们强调面向对象的重要性及其与过程式编程的区别。尤其是在第二次和第三次作业中,老师反复强调了好的架构的重要性。在hw2经历了重构之后,我对好的架构也有了更深的理解。

第一单元采用的是递归下降的办法,主要将表达式分解为表达式、项和因子三层。因为表达式中的项、表达式、因子三种元素又是一种天然的递归结构,所以在架构方面用递归结构来写可以为我们的实现提供极大的遍历。这也就是面向对象的思维带来的好处,在一个好的架构往往可以令递归下降的代码编写更为简单。我采用的思路是递归下降的层次化解析,

该单元是我第一次接触面向对象的思想,让我充分体会到了面向对象的设计和封装给程序设计带来的方便性,收获颇丰。

第二单元:

第二单元算是难度最大的一个单元了。主要原因在于需要自己在课下去了解很多多线程的知识,并学会对其加以运用。比如各种模型,还有各种设计模式,比如工厂模式,观察者模式等等。

通过本单元的学习,我对生产者消费者模型,流水线模型等有了更深的理解,其实质是多个对象之间在进行交互与协作,但是往往这种交互与写作会带来线程安全的问题。我们需要在尽可能提高程序效率的同时确保共享资源的绝对安全。如何确保线程安全呢?大概有两种方法:一是synchronized 锁来控制临界区及临界资源。二是使用ReentrantLock保持线程间的同步互斥。我感觉感觉ReentrantLock的lock.lock()、lock.unlock()的方法能更好体现临界区的范围,有时候能为代码实现提供很多便利。但是该次作业并没有一些特别难以实现的需求,因此在设计上我还是采用了synchronized 锁。

最后,放上自己对线程安全的体会

  1. 对wait() 理解:首先只有不需要运行的时候才会wait()。在消费者生产者模型里面,当线程在有任务或需求队列有资源的时候是不需要wait()的 。所以每次程序wait() 的前面都需要加上一堆代码来判断一下是否需要wait()。

  2. 关于为什么需要同步保护:
    当我们的进程有很多线程的时候,并且有共享变量,就注定很多个线程可能同时对一个共享变量进行读写,那么就会发生读到的是旧的值,或者写覆盖等问题。此时,就需要java提供的锁机制来进行保护。

  3. 那么进入wait() 前判断什么呢?
    (1):当前进程是否需要继续处理任务
    (2):是否已经end了。如果我们使用 setend()的方法的话。那么进入wait() 可能就没法唤醒。这一点至关重要。也就是说需要保证,如果程序已经setend()了。那么这个电梯将永远不会进入wait(),具体的做法是可以在前面加上判断:

    if (this.isEnd()) {	//进入下一次wait().
                    this.setElevetorEnd();
                    break;
                }
                try {
                    wait();
    }
    

    而且setend : 往往还有作用,那就是在任务执行完之后切断电梯的任务。这样处理便不会出现电梯一致wait()的情况了。

  4. 关于避免轮询。一个线程里面至少需要有一个 wait() ,否则就会 由于run方法里面 while(true)的存在,那个线程就会一直执行,一致占用cpu资源。

  5. 关于notify() 。我们需要确保最后的时候,程序一定能及时结束,而不会卡在 wait() 的状态。所以需要setend()。当遇到这个,就结束进程,setend() 之后不能再进入协同线程run方法的下一次的while()

  6. 关于死锁,当多个线程访问多个共享变量,并且出现嵌套的时候,最后按相同的顺序来访问这些共享变量,这样就可以有效的避免死锁的产生。比如:

    线程1访问顺序: A B C
    线程2访问顺序: A B C
    

第三单元:

第三单元的主要是学习规格化设计,具体就是JML语言。JML是一种行为接口规格语言,提供了对方法和类型的规格定义手段。我在刚开始阅读规格的时候感觉比较吃力,但后来阅读量上去了之后,再阅读JML规格就会越来越熟练。

同时,由于课程组已经帮我们规划好了架构,只需要根据契约式编程的方式根据JML语言进行填空即可。本单元最大的难点就是完全按照JML规格实现虽然可以确保程序的功能正确,但是很容易写出复杂度不复合要求的算法。在本单元我也复习了图论一些算法,对并查集和堆优化相关算法也掌握的更为熟练。

第四单元

第四单元主要介绍了UML解析相关知识。总的来看,三次作业是迭代式的设计,逐步完善这个UML解析器的功能。难点在于对模型元素的层次划分及解析,以及一些复杂方法的实现,还有存储信息方式的选择等等。在该单元,我通过对UML类图的解析来进一步理解了层次化设计的思想

测试理解与实践的演进:

首先,四次测试的主要方式都是利用评测机或者手动构造一些边界数据进行测试。

刚开始测试时使用随机构造的数据检验基本功能是否正常,然后根据边界条件来构造特殊的数据来进行边界测试,后来开始建立随机数据生成器,通过和同伴的对拍来进行大数据测试。

在第三单元中,我又接触到了新的Junit测试,并且尝试了相关的实践。JUnit 是一个 Java 编程语言的单元测试框架。虽然我感觉在目前作业中,这种测试方式效果不是很显著,但是老师说在以后的项目中,Junit测试的作用不可小视。

  1. 明确测试目标:
    • 每个方法是否都满足所要求的规格。这也是一种契约式的编程思想。
    • 是否能在任何使用场景下,类都能确保状态正确?
  2. 测试有效性问题:
    • 需要测试哪些数据?
    • 如何检验测试覆盖了多少代码成分?
  3. 模拟使用者对象与被测对象的交互:
    • 通过被测对象提供的方法。
    • 始终注意检查对象的状态。(repok()函数)
  4. 测试场景往往具有一定的实际意义
    • 往往对应着功能与场景。

课程收获:

  1. 学会了面向对象的思想,对架构设计有了更深刻的理解。无论是上课,还是研讨课,或是作业,老师和课程组每次都反复向我们强调二者的重要性,并想方设法的让我熟悉新的编程思想,跳出自己已有的舒适圈。
  2. 熟悉了一门新的语言,熟悉了一个新的工具IDEA。虽然学习的只是java的一小部分,掌握了java的基础语法熟悉了java提供的容器。但是以后用来写算法题还是够用的。
  3. 更加注重编程时的代码风格。课程组的checkstyle工具,让我在变量的命名方式,类和方法的复杂度,以及空格缩进方面取得了不小的进步。
  4. 接触到了多线程的编程,理解了为什么我们在编程中需要多线程,以及如何对共享资源进行保护等等。第二单元是最累的一个单元,但也是收获最丰富的一个单元。
  5. 体会到了与同学交流,在讨论区分享思路的重要性。在OO的道路上,没有团队协作真的是寸步难行,很多时候都需要通过讨论,或者是逛逛讨论区,来为自己拓展新的思路,提升自己的编程能力。

改进建议:

  1. 希望寒假的pre能够早一点开放(因为开放的时间正好和美赛的时间重合了,既需要准备美赛,又需要预习OO,同时还有OS下发的预习,可能寒假末期压力会有一点点大。
  2. 每个单元的第一次作业跨度较大,希望可以调宽一下时间。在这么多次作业中,我觉得每个单元都是第一次作业最难,因为需要自学很多新的知识,并学会应用,这些都需要很多时间。反而第二次,第三次作业相对感觉轻松一点。
  3. 希望能够在课程中讲解测试,或者在pre练习中开放有关评测机的训练。通过本学期的学习,我深刻体会到了测试的重要性。但是测试除了手搓数据以外,最重要的就是搭建评测机了。但是搭建评测机只能通过自己摸索学习,没有一个较为全面的指导。
posted @ 2022-06-29 11:05  乌拉圭的袋鼠  阅读(7)  评论(0编辑  收藏  举报