面向对象编程课程(OOP)第二单元总结

一、设计策略

第一次作业(傻瓜式电梯):

  由于是第一次写多线程作业,许多的知识还处在理论阶段,所以第一次作业写得非常的朴实无华。整个程序总共有四个类,Main类负责通过电梯类实例化一个电梯,然后通过while循环不断地通过阻塞式输入接口得到正确的乘客请求;Dispatch类是调度器的存放位置,由于是傻瓜式电梯,所以其实调度器就仅仅是一个先进后出的队列而已;Elevator类是电梯类,里面封装了电梯的各种行为;Request类是乘客请求类,为了方便我程序的处理,所以我将从输入接口中得到的乘客请求通过Request类进行再加工,增加了一些例如Direction这种属性,以供调度器进行使用。

  整个程序总共就只有两个线程,分别是主线程和电梯线程。之所以我没有将调度器也设置为一个线程,一方面是因为刚开始写多线程,对于多线程的处理没有那么自信拿手,另一方面也是因为傻瓜式电梯根本不需要进行调度,完全不需要一个线程来进行异步操作。在调度器中有一个flag属性,一旦Main类中读入到null,则意味着之后没有输入了,此时就将flag置为0。在电梯线程中,我是采用了sleep加上轮询的方式来运行,当调度器的队列为空且flag置为了0,那么就跳出循环,进而结束电梯线程。而调度器的读写互斥我是通过使用线程安全的队列ConcurrentLinkedQueue完成的。下面是部分代码:

 1 public void run() { //电梯线程的运行代码
 2         while (true) {
 3             try {
 4                 Thread.sleep(100); //使用sleep加轮询策略
 5             } catch (InterruptedException e) {
 6                 e.printStackTrace();
 7             }
 8             if (!Dispatch.getInstance().getQueue().isEmpty()) {
 9                 temprequest = Dispatch.getInstance().poll();
10                 if (temprequest != null) {
11                     runElevator();
12                 }
13             } else {
14                 if (Dispatch.getInstance().getFlag() == 0) {
15                     break;
16                 }
17             }
18         }
19     }
while (true) { //主线程读入乘客请求
            PersonRequest request = elevatorInput.nextPersonRequest();
            
            if (request == null) {
                Dispatch.getInstance().setFlag(0);
                break;
            } else {
                //queue.add(request.toString());
                Dispatch.getInstance().add(request);
            }
            
        }

 

第二次作业(ALS捎带电梯):

  第二次作业的类与第一次作业的完全相同,同时线程也仍旧只有主线程和电梯线程。但是由于这次作业需要实现一个可以捎带的电梯,所以我在Dispatch类中维护了两个Vector,一个用来存储方向向上的乘客请求,一个用来存储方向向下的乘客请求。而在电梯类中,我也开设了一个队列,这个队列中的乘客请求是电梯已经确定要处理的乘客请求,且同一时刻,我会保证这些请求行进方向都是相同的,并且不需要电梯调转方向就能够完成。这样一来,电梯线程只需要在每次到达一个新楼层的时候与调度器进行一次交互,而调度器根据现在电梯运行的方向将能够被捎带的乘客请求分配给电梯,电梯将其存储至其自身的队列中即可。所以,我的设计策略中没有主请求与副请求之分,电梯会将同一方向且该电梯能够一次完成运输的请求都取出来进行处理。另外,这次作业我舍弃了原来的sleep加轮询的方式,而是改用wait和notify组合,大大减少了CPU的占用率。相关代码如下:

public void run() { //电梯线程运行代码
        while (true) {
            eleSleep(0.001);
            if (Dispatch.getInstance().getFlag() == 0 &&
                Dispatch.getInstance().getNup() == 0 &&
                Dispatch.getInstance().getNdown() == 0 &&
                getNpassenger() == 0) {
                break;
            }
            Dispatch.getInstance().get(this.floor, this.direction, this);
            if (getNpassenger() == 0) {
                synchronized (Dispatch.class) {
                    if (Dispatch.getInstance().isempty()) {
                        Dispatch.getInstance().elewait();
                    }
                }
                changeDirection(); //如果该方向没有请求了,那么将调转方向
                if (getNpassenger() == 0) {
                    changeDirection();
                }
            }
            runelevator();
        }
    }

其中,elewait函数的代码在Dispatch类中,如下所示:

public synchronized void elewait() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

而notifyAll函数的调用分别在Dispatch函数收到一个新请求或者是Dispatch受到null进而将flag置为0时进行,由于代码过长,故不贴出来了。

第三次作业(多电梯):

  第三次作业的类同样也是四个,且与前两次作业的功能基本一致。Main类还是主要负责乘客请求的输入,同时开启不同的电梯线程;Dispatch是一个单例模式的调度器;Elevator是电梯类,里面有各个属性供电梯实现个性化定制;Request类主要是将从输入接口中得到的乘客请求进一步处理,增加一些必要的属性。尽管大体上这次作业的架构与前两次作业没什么不同,但是还是有几个关键的改动。一、电梯类中增加了一个停靠楼层数组,用于记录该电梯可以停靠的楼层;二、调度器的调度方法发生调整,虽然对于每台电梯而言,调度策略仍旧与第二次作业相同,但是调度器在接收到新的乘客请求时,会根据电梯能够停靠楼层的情况给请求打上标签,标志着该请求可以被哪几台电梯执行。如果没有任何一台电梯能够直达,那么就将根据运行楼层最短的原则,将原请求拆分成两个不同的请求,打上标签后再放入队列中。拆分请求的流程如下:

private synchronized int checkRequest(Request request) {
        int start = request.getStart();
        int end = request.getEnd();
        int issingle = 0;
        
        if (aqualified[start + 3] == 1 && aqualified[end + 3] == 1) {
            request.setQualified(1, 1);
            issingle = 1;
        }
        if (bqualified[start + 3] == 1 && bqualified[end + 3] == 1) {
            request.setQualified(2, 1);
            issingle = 1;
        }
        if (cqualified[start + 3] == 1 && cqualified[end + 3] == 1) {
            request.setQualified(3, 1);
            issingle = 1;
        }
        if (issingle == 1) {
            return 0;
        }
        //检验是否能够直达
        int returnvalue = 0;
        int mingap = 48;
        int boardfloor = 0;
        int[] tempar;
        //检查是否能借助A电梯进行换乘,后面的以此类推。
        tempar = checkATransform(mingap, returnvalue, boardfloor, start, end);
        mingap = tempar[0];
        returnvalue = tempar[1];
        boardfloor = tempar[2];
        tempar = checkBTransform(mingap, returnvalue, boardfloor, start, end);
        mingap = tempar[0];
        returnvalue = tempar[1];
        boardfloor = tempar[2];
        tempar = checkCTransform(mingap, returnvalue, boardfloor, start, end);
        returnvalue = tempar[1];
        boardfloor = tempar[2];
        
        split(returnvalue, boardfloor, request); //将请求分成两个可以直达的请求
        return 1;
    }
    
    private synchronized int[] checkATransform(int mingap, int value,
                                               int board, int start, int end) {
        int[] returnar = new int[3];
        int tempmingap = mingap;
        int returnvalue = value;
        int tempboard = board;
        returnar[0] = tempmingap;
        returnar[1] = returnvalue;
        returnar[2] = tempboard;
        if (aqualified[end + 3] == 1) {
            // Check Elevator B -> Elevator A
            if (bqualified[start + 3] == 1) {
                for (int i = -3; i <= 20; i++) {
                    if (aqualified[i + 3] == 1 &&
                            bqualified[i + 3] == 1) {
                        int gap = Math.abs(i - start) + Math.abs(end - i);
                        if (gap < tempmingap) {
                            tempmingap = gap;
                            returnvalue = 21;
                            tempboard = i;
                            returnar[0] = tempmingap;
                            returnar[1] = returnvalue;
                            returnar[2] = tempboard;
                            if (tempmingap == Math.abs(end - start)) {
                                return returnar;
                            }
                        }
                    }
                }
            }
            // Check Elevator C -> Elevator A
            if (cqualified[start + 3] == 1) {
                for (int i = -3; i <= 20; i++) {
                    if (aqualified[i + 3] == 1 &&
                            cqualified[i + 3] == 1) {
                        int gap = Math.abs(i - start) + Math.abs(end - i);
                        if (gap < tempmingap) {
                            tempmingap = gap;
                            returnvalue = 31;
                            tempboard = i;
                            returnar[0] = tempmingap;
                            returnar[1] = returnvalue;
                            returnar[2] = tempboard;
                            if (tempmingap == Math.abs(end - start)) {
                                return returnar;
                            }
                        }
                    }
                }
            }
        }
        return returnar;
    }

private synchronized void split(int value, int boardfloor,
                                    Request request) {
        Request add1 = new Request(request);
        Request add2 = new Request(request);
        add1.setEnd(boardfloor);
        add2.setStart(boardfloor);
        if (value == 21) {
            // Elevator B -> Elevator A
            add1.setQualified(2, 1);
            add2.setQualified(1, 1);
        } else if (value == 31) {
            // Elevator C -> Elevator A
            add1.setQualified(3, 1);
            add2.setQualified(1, 1);
        } else if (value == 12) {
            // Elevator A -> Elevator B
            add1.setQualified(1, 1);
            add2.setQualified(2, 1);
        } else if (value == 32) {
            // Elevator C -> Elevator B
            add1.setQualified(3, 1);
            add2.setQualified(2, 1);
        } else if (value == 13) {
            // Elevator A -> Elevator C
            add1.setQualified(1, 1);
            add2.setQualified(3, 1);
        } else if (value == 23) {
            // Elevator B -> Elevator C
            add1.setQualified(2, 1);
            add2.setQualified(3, 1);
        }
        
        add1.setHasnext(1);
        add1.setNext(add2);
        add2.setFlag(1);
        
        addToArraylist(add1);
        addToArraylist(add2);
    }

二、程序结构度量

第一次作业(傻瓜式电梯):

UML类图如下:

度量图如下:

注:

  ev(G)为Essentail Complexity,表示一个方法的结构化程度

​  iv(G)为Design Complexity,表示一个方法和他所调用的其他方法的紧密程度

  ​v(G)为循环复杂度

​  OCavg为平均循环复杂度

  ​WMC为总循环复杂度

第二次作业(ALS捎带电梯):

UML类图:

度量图:

注:

  ev(G)为Essentail Complexity,表示一个方法的结构化程度

​  iv(G)为Design Complexity,表示一个方法和他所调用的其他方法的紧密程度

  ​v(G)为循环复杂度

​  OCavg为平均循环复杂度

  ​WMC为总循环复杂度

第三次作业(多电梯):

UML类图:

度量图:

注:

  ev(G)为Essentail Complexity,表示一个方法的结构化程度

​  iv(G)为Design Complexity,表示一个方法和他所调用的其他方法的紧密程度

  ​v(G)为循环复杂度

​  OCavg为平均循环复杂度

  ​WMC为总循环复杂度

三、基于Solid原则的评价

SRP(Single Responsibility Principle):

  在三次作业中,我所设计的类功能分配都比较清晰:Main类就是用来接收输入的乘客请求,并开启电梯线程;Dispatch类就是模拟实际的调度器所做的工作,接收乘客请求,然后进行一定的处理,同时在电梯每一次寻求与调度器交互的时候分配给各个电梯其所能够完成的请求;Elevator类就是模拟真正的电梯运作,包括开关门,上下课,每一层之间的运行等等。所以在这一点上,做得还算不错,逐渐抛弃了以前面向过程的思维方式。

OCP(Open Close Priciple):

  前两次作业我觉得我的设计可拓展性不够,许多的参数都是类设定好的,不太符合OCP原则。第三次作业由于有多电梯的存在,且每一台电梯的具体参数都不太一样,所以第三次作业的可拓展性是比较强的,比如我可以随时修改我的调度算法,比如我可以新增许多的电梯,并设定电梯可以停靠的楼层等等。另外由于我的SRP做得还不错,所以要修改功能时基本不用涉及其他类,只需要修改单独的类或者单独的方法即可。

LSP(Liskov Substitution Principle), ISP(Interface Segregation Principle)and DIP(Dependency Inversion Principle):

  这些原则我觉得在这个单元的作业中我做得还很不够。首先我没有使用过继承或者接口,现在回想起来,对于电梯类来说,我完完全全可以使用一个接口,里面存放着电梯运行的一些基本方法,或者继承与一个最基础的电梯母类,使得整个程序结构层次更加完整。

四、bug分析

  非常幸运地是,在本单元的三次作业中,我都侥幸通过了强测和互测阶段。但是,在我自己测试程序的过程中,还是能够发现一些bug。

  一、多电梯时有两个电梯wait却始终醒不来。有关代码如下:

public void run() {
        while (true) {
            eleSleep(0.001);
            if (Dispatch.getInstance().getFlag() == 0 &&
                Dispatch.getInstance().getNup() == 0 &&
                Dispatch.getInstance().getNdown() == 0 &&
                this.npassenger == 0) {
                break;
            }
            //System.out.println(this.id + "-step1");
            getNewRequest();
            if (this.npassenger == 0) {
                if (Dispatch.getInstance().qualifiedempty(this)) {
                    //Dispatch.getInstance().elewait();
                    eleWait();
                }
                //System.out.println(this.id + "-step2");
                changeDirection();
                if (this.npassenger == 0) {
                    changeDirection();
                }
            }
            runelevator();
            Dispatch.getInstance().eleWake();
        }
    }
View Code

  在最开始的版本中,runelevator被调用完后,后面没有跟eleWake函数,这样就会导致问题。因为有三个电梯,那么对于每个电梯,如果它与调度器交互没有拿到它能够运输的乘客,并且调度器的flag也并没有置为0时,它就会进入wait状态。虽然当调度器得到null请求时会调用notifyAll,但是由于有三个线程,所以意味着只有一个线程会结束,其他的两个线程有很大的概率一直在等待池中不会被唤醒,自然也就不会结束。所以我在每个线程运行完runelevator之后,都会notifyAll一次,以保证其他的线程不会一直在那死等,保证了程序能够正确结束。

  二、电梯不会转向,导致飞天。这主要是程序逻辑的问题,在电梯以当前方向无法从调度器中得到合适的请求时,我只让它转换了一次方向,但很有可能其实剩下的请求也是同方向的,只是出发楼层不在电梯前进方向上,所以要解决这种问题,我需要让它连续转换两次方向,并且每次都将电梯所在楼层设为最高或者最底层,这样能够保证电梯不至于出现飞天的状况。另外,在第三次作业中拆分请求时,可能一个向上的请求会被拆分成一个向下的和一个向上的请求,所以在Request类中,还需要使用updateDirection函数来重置Direction值,不然就可能让电梯拿到方向错误的请求。

五、测试策略

  首先说说我自测的策略。由于我没有采用自动化评测方法,所以所有的数据都是我自己撰写的。我会根据一些容易出错的边界情况设计样例,比如电梯的换向,电梯的换乘,电梯的超载,从一楼到负一楼的电梯运行状态。然后再测试电梯是否能够自动结束,比如在0.0秒投入所有数据然后ctrl+D强制停止,又或者在所有的请求都跑完了之后再输入ctrl+D停止。

  接着说一下我的Hack策略。由于这一单元作业盲狙的难度相对上一单元来说难了许多,所以我主要是以阅读代码为主。主要关注的点是调度器是否出现了读写互斥的情况,或者本身一个线程安全的数据容器是否被嵌套在另一个容器中进而有可能产生错误,亦或者wait和notify的逻辑是不是完全正确,会不会出现有线程wait了但是一直等不到唤醒信号出现的情况。所以我关注的主要都是线程安全方面的问题,而对于功能实现方面关注得不太多。在阅读他人代码的同时,自己对于线程安全和如何合理加锁方面有了新的理解。

六、心得体会

  本单元实验中让我印象最深的一点就是优秀的优化方法一定是建立在良好的程序架构基础之上的。一个好的设计架构能够将每一块的功能都分开,减少耦合度,每个类都只需要完成分配给它的工作,不同类之间通过数据信息来进行交互。在这样的架构的基础上,我们做优化就变得非常简单,只要我们能够提出比较好的优化算法,那么就可以比较轻松地在我们的程序中实现,而不用担心优化算法可能干扰了我们整个程序的逻辑正确性。其次就是在优化的过程中,我发现越贴近生活的优化方式效果往往都是不错的,因为一般来说,生活中的实例是在各种条件制约下的一个比较不错的选择,所以我们在设计程序的时候也应该多参考生活中真正的电梯运行方式,或者借鉴类似事物的一些优化算法。

  在线程安全方面,我一开始是所有的方法都加上了synchronized的锁,虽然在这个单元的作业中无伤大雅,但其实浪费了许许多多其实能够并行执行的代码块。如何能够缩短临界区的大小,同时保证程序的正确性是一门学问,还需要许许多多的实践来锻炼自己。总的来说,要想真正做到线程安全,就需要全面地了解每一种机制的运作方式以及其背后的原理,比如synchronized在什么情况下锁的是当前对象,什么情况下锁的又是整个类,在其他类中又怎么调用这个类的锁等等,这些东西错综复杂,但是我们一旦了解了其真正的运行机制,那么就可以举一反三,在我们希望使用的场合用正确的方式完成我们想完成的任务。

  最后祝北航的OO课程能够越办越好,让学生们的收获越来越多。

posted @ 2019-04-24 17:54  Cauchy_Mars  阅读(275)  评论(0编辑  收藏  举报