面向对象第二单元(电梯)总结

面向对象系列:

(1)多项式求导:https://www.cnblogs.com/ZhaoLX/p/OO_Derivative.html

(2)电梯(多线程):https://www.cnblogs.com/ZhaoLX/p/OO_Elevator.html

(3)地铁(JML):https://www.cnblogs.com/ZhaoLX/p/OO_Subway.html

(4)UML图:https://www.cnblogs.com/ZhaoLX/p/OO_UML.html

OO-Unit2

一、本单元作业的设计策略及基于度量的分析

(零)SOLID原则

这次博客作业要求基于SOLID原则分析自己的程序,所以在这里附上自己的学习笔记。

SOLID是面向对象设计和编程中几个重要编码原则的首字母缩写。

(1)SRP(The Single Reponsibility Principle):单一责任原则

  一个类或者一个方法都只有一个明确的职责。如方法的职责为:从某个特定的方面对对象的状态进行更新和查询;类的职责为:使用多个方法,从多个方面来综合维护对象所管理的数据。当不满足这一原则时,逻辑难以封闭,容易受到外部因素变化的干扰,导致类/方法不稳定。当这个类/方法需要承担其他类/方法的职责的时候,就需要分解这个类/方法。 

(2)OCP(The Open Closed Principle):开放封闭原则

  软件实体应该是可扩展,但是不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。如新功能出现时,无需修改已有的实现(close),而是通过扩展来增加(open)。

(3)LSP(The Liskov Substitution Principle):里氏替换原则

  这是一个对于父类和子类之间“IS-A”关系的约束:任何父类出现的地方都可以用子类来代替,并不会导致使用相应类的程序出现错误。

(4)DIP(The Dependency Inversion Principle):依赖倒置原则

  1) 高层模块不应该依赖于低层模块,二者都应该依赖于抽象 (防止低层模块的改变对高层模块产生影响)
  2) 抽象不应该依赖于细节,细节应该依赖于抽象 

(5)ISP(The Interface Segregation Principle):端口分离原则

  不要强迫用户去依赖那些他们不使用的接口,避免用户在使用一个接口的时候必须要实现很多他并不想要的方法。即最好使用多个专门的接口,而不是使用单一的总接口。

博客链接(每个原则都有示例,讲解非常清晰):https://www.cnblogs.com/OceanEyes/p/overview-of-solid-principles.html#_label4

(一)第一次作业

(1)设计策略及协作图

  由于第一次作业的需求就比较简单,也没有性能分,所以一main到底甚至都是可以的。我是完全仿照理论课和实验课给出的“生产者-消费者”的样例代码写的,线程有两个类,一个是“生产者”RequestInput(输入),另一个是“消费者”Elevator(电梯);共享对象,也就是托盘,为RequestQueue(请求队列)。生产者和消费者对共享对象进行操作的代码都在共享对象中实现。RequestInput从标准输入中读入PersonRequest,然后调用requestsAdd方法将其存入RequestQueue中;Elevator调用requestsProcess方法从RequestQueue中读取请求,然后运送该请求。(elevatorMove, elevatorDoorOpen, elevatorDoorClose三个方法均为requestsProcess调用的私有方法,只是为了代码复用。)类图如下:

  

(2)基于度量分析

  第一次作业确实比较简单,所以方法和类的度量表现都很良好(假装是自己架构很好)。同样也由于太简单了,就不画线程协作图,也不从SOLID原则分析了。

   

(二)第二次作业

(1)设计策略及协作图

  和第一次作业相比,第二次作业要求要用ALS(可捎带)调度,由于增加了主请求和可捎带请求的概念,要实现执行主请求时判断其它请求是否可捎带,如果可捎带的话就要加入电梯的工作队列,所以在Elevator类中加入了一个队列存储电梯的工作队列。此外,这次为了解耦,改了一下类中的方法,让共享队列(同时也是调度器)只负责请求的接收(从输入接收)和分发(分发给电梯),让电梯负责处理被分配到的所有请求。也就是说,一个请求输入且存储到RequestQueue中后的处理流程是:先由Elevator调用RequestQueue的requestsAlloc方法,从全部的请求队列中寻求请求分配,维护完Elevator类内部的工作队列后,电梯逐层运行,按照ALS策略,处理这些请求。如果采用这种设计策略,在Elevator类中就需要对很多方法加锁。此外,由于第二次作业对CPU时间有了限制,导致暴力轮询会超时,所以必须要引入wait/notify来进行线程的控制。简单来说类图及时序图如下:

         

  ALS策略本身没有什么问题,可能性能上稍差一点,但是和其他同学也在用的SCAN, LOOK不会差太多,但是由于我在设计时犯了一些错误,强测中有一个点因为请求的分配和处理导致最终超出了Tmax。这个在第二部分会详细说明。

(2)基于度量分析

  度量结果如下图所示,可见主要的问题出现在Elevator类当中。分析代码发现run和doorOpenAndClose两个方法中调用了过多的其它方法,耦合过于紧密。其中run方法由其本身特性决定,这个不太好解决;而doorOpenAndClose方法主要是对电梯自己的工作队列进行操作,对自身状态(尤其是内部包含的人员)进行维护,也是在不停调用方法。

    

  由于第二次作业在SOLID原则下暴露的问题在第三次多电梯中几乎全有,所以在这里就不着重分析了。

(三)第三次作业

  终于迎来了第三次略显魔鬼的多电梯,其魔鬼之处不只在于电梯数量的增多,更在于电梯可达楼层的限制,以及因此导致的必须由调度器来对乘梯人的部分无法直达的请求进行分割(调度器:终于落我手里了)。那么这样一来,我们第二次作业中的架构还能不能复用呢?答案是可以的!令人感动!这次我不用像第一单元一样去重构了!

(1)设计策略及协作图

  和第二次作业相比,这次我丰富了Elevator类中的成员变量,除了accessFloor(可打楼层)、inPersonMax(可容纳人数)等这些本次作业要求的变量,还添加了如state(上升还是下降)、targetFloor(目标楼层)等属性。其实在第二次作业中就应该加入的,第二次作业也因为没有这些属性而使得请求分配的逻辑变得比较复杂。不过这次最大的变化还应当是增加了一个RequestSplit类来对不能直达的请求进行分割,类中各方法的逻辑并不复杂,单独拿出来只是觉得不应该让共享队列/调度器RequestQueue类做太多的事情,尤其是这种好像应该是乘梯人应该自己完成的任务...对一个请求进行拆分的一个重要问题就是:如果一个请求被拆分为前序请求和后序请求,那么后序请求只有在前序请求被执行完毕后才可以插入到请求队列中去。为了满足这个时序上的需求,我在RequestQueue类中增加了一个HashMap用来存储<前序指令,后序指令>,在前序指令执行完成后到HashMap中取出这个key-value对,然后把后序指令再加入到请求队列中。类图及时序图如下所示:

    

(2)基于度量分析

  度量结果如下图所示,Elevator类的run方法和doorOpenAndClose方法是祖传的老毛病,RequestQueue的requestsAlloc和determinMajorRequest两个方法也存在上述问题,但是不是太严重。类的方法平均循环复杂度上,SplitRequest类独树一帜,根据代码发现这个类确实循环层数比较多,判断的分支也很多,是比较面向过程的一个部分。总体来说,和上一单元的作业相比,这个单元的三次作业的度量结果都有了比较明显的提升,还是可以看到自己的进步的。

     

(3)基于SOLID原则分析

  由于这三次的作业都不涉及继承与接口(继承Thread类与实现Runnable接口就不算在内了),所以只涉及三个原则的分析:单一责任原则、开放封闭原则和依赖倒置原则。

  ① 单一责任原则:我在设计上尽量避免类和方法的职责越界,比如在第三次作业中把分割请求这个功能单独交给一个类去做,其主要原因在于这个工作需要得知三部电梯的可达楼层,而且在逻辑上非常面向过程,写的也是很暴力的算法,最为重要的是,我出于简化考虑,只采取了静态分割的策略,即一个请求如何分割是固定的,与当前时刻三部电梯的状态没有关系,这也使得它可以在一个单独的类中进行处理。

  ② 开放封闭原则。这个原则我做的并不好,除去第一次作业不提,在二三次作业中,由于我第二次作业考虑不全面所以写的并不是正确的ALS,它在一些情况下会把本不该分配的任务作为可捎带的请求分配给电梯,虽然在性能上表现并不一定比ALS差,但是不适用于第三次作业(因为会导致性能变差)。所以在第三次作业中对于如何分配请求的方法做了重构。

  ③ 依赖倒置原则。个人感觉这次作业的模块没有高低层之分,虽然Input类和Elevator类中都有RequestQueue类的成员变量,但其实只是为了将其作为共享变量,并不违背依赖倒置原则。RequestSplit类可以看做是协助RequestQueue完成工作的类,Input和Elevator两个类是相对平等的关系,也不违背依赖倒置原则。如果个人理解有误,欢迎指正。

二、自己程序的Bug

  这三次作业整体来讲都没有出现由线程安全带来的问题,这也是比较欣慰的一点。其中除了第二次作业的强测由于性能问题挂了一个点,其他的强测与互测中都没有被找出bug。下面就来分析一下第二次作业的bug。

  这个bug主要来自于我在设计电梯类的时候没有考虑清楚,一些比较基本的,如现在是上行还是下行、目标楼层这两个指标都没有添加。这样做会带来两个问题:一是当电梯没有接到主请求的乘客时,判断捎带请求会出问题;二就是强测中出现的bug,会把一些并不应该被捎带的请求当成可捎带请求并加入队列,导致请求执行顺序上出问题,影响了性能。下面逐个来分析:

  (1)例如:当前电梯在1层且工作请求队列为空,此时有请求9-2,3-9及8-2。我的程序会把9-2作为主请求,然后根据主请求的方向(9-2,向下)来判断捎带请求,将8-3作为捎带请求加入工作请求队列。将8-3作为捎带请求固然没有错,但是我忽略了本可以捎带的请求3-9,造成性能的下降。

  (2)我在判断捎带请求的时候,所用布尔表达式为:

boolean sameSide = ((targetFloor - presentFloor) *
                    (newTargetFloor - presentFloor)) > 0;
boolean halfway = (((targetFloor - newStartFloor) *
                    (newStartFloor - presentFloor)) >= 0);
boolean canBeTaken = sameSide && halfway;

其中targetFloor为电梯主请求的toFloor,presentFloor为电梯当前所在楼层;newTargetFloor为当前判断请求的toFloor,newStartFloor为当前判断请求的fromFloor。在如下情景时,这种判断方法就会出现很大问题。当前电梯在15层且主请求为-3- -1,此时还有请求13-14,那么13-14这个请求会满足canBeTaken表达式并成为队列中第二个请求,这样的话执行完主请求后电梯就会跑到接近顶层去执行13-14,造成性能的下降。

三、发现别人程序的Bug所用的策略

  第二单元由于没有输入格式判断、测一组数据时间太长、并行导致结果具有不可复现性等等因素,让互测变得困难而且神秘了起来。一开始我是有点拒绝的,因为不知道这次怎么写评测机,之后在室友大佬的帮助下,进行了一些简单的互测,并且在第二次作业中挽回了一些分数。

  测试的三个部分(构造输入、将构造的数据输入到程序并获取结果、评测结果)中,我认为在这一单元中比较难的是第二和第三步,特别是第三步。第二步定时输入开始不知道要怎么弄,之后才发现直接sleep就可以了hh。第三步就是抱室友大腿,利用了他写的评测代码。而在构造输入部分,我在随机生成的基础之上,尝试在第三次作业中提高容易出错的楼层(比如-3、3、15之类)的比例。老师在课上讲过,要针对这次的需求和被测试代码的设计结构来构建测试用例,不然盲目构造的话可能构造了100个数据点,也只是在测试一种情况。我深深地认同老师的观点,但是水平所限,确实不知道该怎么出数据...如果有机会的话,也希望老师在课上可以讲一讲

四、心得体会

  由于这三次作业我都没有重构,所以写起来不像第一单元面对第三次作业那样抓狂。在这个过程当中,我也体会到了程序如何根据需求的变化而变化。这其中,一方面是SOLID原则中的开放封闭原则,但不可避免的是,我们需要对一部分代码进行修改。在这个过程中,往往就能看到增量的需求背后,增量的程序设计。

  这三次作业都设计到一个问题,那就是程序当且仅当在什么时候,可以结束。有趣的是,由于需求及设计上的特性,这三次作业在这一问题上的考量的难度是增量的。第一次作业是一部傻瓜电梯,所以在读到NULL且请求队列为空时就可以结束了。第二次作业由于在电梯内部加入了电梯的被分配的请求队列,所以还需要等这个请求队列为空时才可以结束。第三次作业由于有拆分请求的情况,所以必须等拆分的请求也被处理完,也就是HashMap也为空时,才可以结束。这些只是电梯线程结束的条件,那么电梯线程还有可能处于等待,也就是wait之后还没有被notify的状态,那么电梯什么时候应该进入这个状态,又应该在什么时候从这个状态转移到工作状态呢?其实是一个需要严谨的逻辑才能理清楚的问题。由于这个问题比较细碎,而且我的方法也算不上优雅,就不在这里展开说了。但我从这一个问题当中感受到了应对增量的需求,我们应当如何进行增量的开发

原文地址:https://www.cnblogs.com/ZhaoLX/p/OO_Elevator.html