OO第二单元总结

第二单元总结

一、第五次作业

1.题目概况:

    本单元的第一次作业主要为完成单部多线程可捎带电梯的模拟。这也是学习OO一个多月以来第一次使用多线程编程。

2.思路:

这次作业的构架主要采用了生产者-消费者模式:

  • 构建一个请求队列ReqQueue类作为公共缓冲区,与消费者、生产者进行交互
  • 输入类Input作为生产者线程,负责读入标准输入,并将转化好的请求放入请求队列中
  • 电梯类Elevator作为消费者线程,从请求队列中获得请求,并进行相应的移动、上下人操作

对于电梯调度策略,主要是根据ALS算法,参考LOOK算法等进行改造:

  • 电梯设置一个主请求,和若干个捎带请求
  • 电梯内没有人且主请求为空时,从请求队列队首获取主请求,并开始运行
  • 在去接送主请求的过程中,每到一个楼层就将电梯与请求队列交互一次,更新请求状态:如有可捎带请求就加入电梯的捎带请求队列中,并顺便把到达目的地的捎带请求送出
  • 将主请求送达目的地,若此时还有捎带请求则把一个捎带请求转化成主请求,继续运行

类图:

  • 优点:整体结构较为简单,所使用类数目少,类之间的交互情况比较清晰,主要就是通过请求队列这个类来与输入类、电梯类进行两边的交互
  • 缺点:由于类数目较少,虽然一定程度上使整体结构简化,但一些类承担的责任也变多了,就比如下面度量分析中所展示的,电梯类的一些方法复杂度略高

3.度量分析:



可以看出除了电梯类的一些方法外整体复杂度较低。而复杂度高的方法主要集中在电梯类中。原因有几点:1.电梯与请求队列交互较复杂,这一块涉及电梯调度算法,而本人选用的调度算法涉及主请求、捎带请求等,不是那么简洁明了,因此不可避免地有高复杂度。2.if-else结构用的太多,其中几个复杂度较高的方法中都有一个共同特点,那就是if-else使用过多,比如判断两个请求是否是同一方向的方法,这里面有一些是条件过于复杂,但有一些是可以简化的,以后应当注意。

协作图:

二、第六次作业

1.题目概况:

    在第一次的基础上,增加了电梯数量乘客数限制、增加了负楼层

2.思路:

此次作业由于有了前一次的基础,不改变调度策略的话拓展起来算是相当轻松了,只需处理一下读入电梯数量、设置最大进入人数和负楼层即可

  • 读入电梯数量:
    • 这里也借鉴了生产者-消费者的思想,将Input类同时视为生产者和消费者,生产的只有一个:电梯数,同时获取电梯数传给主类,并据此生成相应数量的电梯让它们跑起来
    • 在输入类Input中解析出电梯数num,并通过this.queue.setNumOfElevator(num)传给调度器(即请求队列)
    • 同时Input类设置getNumOfElevator()方法,如果调度器中尚未设置好num,就wait,总体来看跟放入/获得请求差不多
  • 乘客数限制:
    • 在每一层上人的时候检查是否有余量
  • 负楼层处理:
    • 上下楼照常,因为没有第0层,所以遇到currentFloor == 0currentFloor++currentFloor--

类图:

本次结构和上次大致相同,类的个数也没有变化,依然是输入、电梯两者和公共资源请求队列进行交互。

3.度量分析:



本次复杂度较高的地方是上次作业遗留下来的,因为不打算改变调度方法,所以电梯的这些方法就不太方便改动。关于降低复杂度的方法,主要可以通过将电梯与调度器的交互方法交给调度器来实现,让电梯只负责移动和运人。

协作图:

三、第七次作业

1.题目概况:

    电梯逐渐魔鬼起来,增加了可达楼层限制,因此也就导致有些请求不得不需要换乘才能完成;增加了加入新电梯的请求;不同类型电梯载客量、运行速度和可达楼层不同。

2.思路:

这次作业的主要难点在于处理换乘请求,对于这一点,我采用了封装一个新的SubPersonRequest类的方法,以达到分割请求的目的

  • SubPersonRequest:
    • 使之继承PersonRequest,在此基础上增加type属性,表示完成这一请求需动用的电梯类型
    • 在构造函数中,判断出发和目的楼层是否同在一种电梯的可到达楼层中,如果是,则不需要换乘,即类型为“A/B/C”,若不是,则表明需要换乘,进一步判断两个楼层分别哪两个电梯可到达,据此将类型划分为“A-B/A-C/B-A/B-C/C-A/C-B”
    • 设置split方法,主要是针对于需换乘请求,返回一个ArrayList,其中的两个元素是将换乘请求分割成的两个子请求。至于换乘楼层,首先是在请求的出发和目的楼层之间寻找一个离出发楼层最近的楼层使得这一楼层是至少两部电梯的公共楼层,然后创建两个新的personId相同的SubPersonRequestfromFloor-tempFloortempFloor-toFloor,可以交由两部电梯分别完成。如果出发和目的楼层之间不存在换乘楼层,那可能就需要在请求反方向或者更远的地方找换乘楼层了
  • 调度器(请求队列)的变化:
    • 由于电梯分三种类型,用HashMap存储三个请求队列,分别对应分配给不同类型电梯的请求队列queue
    • 除了请求队列,新增了换乘请求队列transferQue,也用HashMap储存三个,在里面存储换乘请求分成的后半个请求。由于换乘请求有时间上的先后性,分割后的两个子请求必须先完成前一个,才能继续完成后一个。因此调度器分配请求给电梯是从queue中获得,处于transferQue的请求不能被立即调度给电梯。
    • 输入类放入请求时,放入的是SubPersonRequest类,判断其类型,如果是不需换乘的,直接加入对应类型的queue;如果需换乘,用split函数将其分成子请求,前半程加入queue,后半程加入到transferQue中待命。
    • 增加updateQue方法,每次电梯下人时,调用这一方法,遍历transferQue,找到其中与刚刚完成的请求的乘客id相同的请求,如果有,将其从transferQue转到queue中。

类图:

本次新增了一个人员请求的子类来代替其父类,其余的结构与之前基本相同。

3.度量分析:



本次作业除了之前电梯的一些高复杂度方法外,由于增加了SubPersonRequest类,带来了其他高复杂度的方法,如将需要换乘的原始请求划分为两个子请求的方法、SubPersonRequest类的构造方法(判断请求类型用了过多的条件语句)。

协作图:

4.SOLID原则:

  • SRP:本次作业输入类专门负责处理输入、电梯类负责上下和接送,都能各司其职,唯一不足的是调度器(请求队列)承担的任务较多
  • OCP:本人为电梯类设置了一系列的属性,拓展或改写相关的功能也比较方便,这点符合OCP原则。调度器的拓展也不会对其他产生影响,也只需要在原有基础上增加新的方法即可(但可能会越来越臃肿,如果规模非常大重构是有必要的)
  • LSP:本次作业中使用继承的只有一处,即作为PersonRequest子类的SubPersonRequest类,也比较符合ISP原则,除了在生成SubPersonRequest处用到了作业给出的PersonRequest类,其他地方,包括电梯类、调度器等使用的都是子类
  • ISP、DIP:本单元作业用到这两个原则的地方不多

四、BUG分析

    这三次作业中出现bug较少,很幸运地在三次强测里没被发现bug,也没有碰到过奇奇怪怪的RTLE、CTLE等错误。第1、3次互测中侥幸躲过一劫,但第2次互测中被找到了一个bug,原因是没有处理好乘客容量与进人:在前往主请求出发地的过程中,如果搭载了过多的乘客(此时主请求的那位还没进入电梯),等到了主请求的出发楼层,由于客满,主请求进不去电梯,但是主请求还是在的,于是电梯又傻乎乎地前往主请求目的地了。也就是说,虽然被分配了请求,也照着请求做了,但人没进去,造成了一个WA。解决方案很简单,在接收捎带请求时,将最大捎带数由电梯的capacity改为capacity-1,即为主请求留一个位置,保证能接到人。

五、互测策略

    本单元的互测及本地自测依然是采取了自动化测试的方法,用python搭了一个简单的评测机(不得不说python真好用)。评测机主要由三部分组成:数据生成、电梯行为检查、运行程序

  • 数据生成:主要用random库来随机生成时间间隔、id、楼层等,其中,时间间隔的生成考虑到了间隔为0、间隔较长(10s+)的情况
  • 电梯行为检查:
    • 电梯逻辑检查:采用有限状态机,根据电梯arrive、open、close、in、out等行为转移状态,如果状态转移不当,如未开门就进出人员,则会跳转至ERROR状态;除此之外,还会判断到达楼层是否正确、是否到达的是相邻楼层、开关门的是否是可达楼层等等。如果运行时间过长(超过210s)也会判为错误。基本上如果电梯运行逻辑不对以及RTLE都会检查出来。
    • 人员输运情况检查:首先根据输入的请求建立一个字典,存放人员id和对应的to、from楼层关系。然后对于字典中的映射关系一个个在输出结果中查找,是否把每个人都运送到位。如果有的请求没有被完成则会被检查出。
  • 运行程序:这一部分也是最难写的一部分,在看了评论区以后知道了python的subprocess,然后查阅了一些资料,学会了怎么用它来运行java程序,步骤大致如下:
    • 将程序打包成jar包,用java -jar xxx.jar命令运行
    • process = subprocess.Popen(cmd, shell = True, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)语句来创建进程
    • 逐条输入数据,用process.stdin.write(instr),并在每输入一条数据后flush一下。输入之间用time.sleep(t)来暂停一段时间
    • process.stdoutprocess.stderr中读取标准输出、标准异常
    • 在运行程序的同时将相关评测信息输出到控制台上以便实时观看

    利用这个自动评测机,在第七次作业中发现了两个其他人的bug:停止输入后,程序仍然不断运行,推测可能是电梯停运条件设置错误或者某处出现了死锁。除了互测以外,自动评测机还有一个重要的用处是自测。在这一单元中,靠着自动评测机,我就成功的发现了自己的一个电梯停运条件上的错误,如果没发现又将是一场惨案。
    线程安全是本单元的一个重点,因此测试时也要着重看这一点。死锁是其中常见问题之一,关于这一点,我在测评机中主要是看程序运行时间,如果某个人的程序运行时间过长,迟迟不结束,那大概率就是死锁了。初次之外,在互测时虽然没时间详细浏览每个人的代码细节,但在一些容器的使用、资源共用的地方会着重去看。
    总而言之,这一单元测试与第一单元还是区别挺大的,主要有以下几点:1、正确性判断:第一单元作业输出仅一行,判断正误比较简单,python中有现成的库供使用。而本单元,答案正确性考虑要素过多,首先就是指导书上限定的几点,如电梯运行逻辑、时间等等,最重要的是多了线程安全这一因素,这就使得测试难度有所上升;2、测试时间问题:第一单元输入也仅仅是短短一行,运行一次程序大多一秒不到,数据是成百成百条测的。这次大大不同,运行时间短则十几秒,长则一两分钟,再加上一个屋子有八人,几个小时下来平均每人测试的样例仅仅几十次甚至十几次,等待过程痛苦;3、相比与第一单元,第二单元的一些错误不那么直观,比如CPU运行时这个就很难得知(本人能力有限只能肉眼看有没有暴力轮询)。写测评机花费的时间多了,测试时间花费也多了,但检查出错误也不那么容易了,要想做到样例全覆盖几乎不可能。可能这也是为啥这一单元明显感到大家互测热情都不如上一单元(有好多连空刀都不愿意放了)。不过,写测评机的过程中我也确实学到了很多东西(OO课教我学python),浏览他人代码,了解不同调度策略和保证线程安全的操作也很有好处。

六、心得体会

  • 线程安全:线程安全是本单元的核心问题之一。由于这一单元是第一次接受多线程的洗礼,在一开始编写代码时还是比较保守的,先保证线程安全。具体措施大概有:使用安全容器、给一些几个线程可能共用的方法上锁、灵活使用notifyAll()wait()等、避免一个线程调用另一个线程方法等等,比较幸运的是没被安全问题所困扰(不过感觉不被线程安全毒打几次还是少了点经验)。在讨论课上听了许多大佬介绍的课上没讲过或者很少提及的线程安全机制,如volatile、ReentrantReadWriteLock、Atoms等操作,感觉受益很大,希望以后也能在实践中把这些都亲自尝试一下。经过这一单元的学习,我认识到了线程安全在多线程编程的重要意义。
  • 设计原则:本单元还学到了一些新的设计原则,即SOLID原则。我认为其中的一些原则在本质上是与第一单元的课堂上教给我们的设计原则是相同的。例如SOLID中的S(RP),代表每个类、方法只有一个明确职责,其实就是告诉我们要降低代码耦合度;O(CP)则是对代码的可拓展性提出一定的要求,在写代码前要先思考,为日后的新增功能考虑,否则此次重构的痛苦是难以接受的;还有L(SP)、I(SP)也与多态思想有异曲同工之妙,虽然本次作业中继承、接口的使用很少,但以后的设计中遵循这些原则也是很有用的。而三次作业下来而看,这些设计原则确实对我的程序设计大有帮助。就比如第五次到第六次作业中,由于之前考虑到了一些可能的电梯新增功能,在设计时就保证了每个类和方法的可重用性,功能也更加明确,所以第六次作业中我的修改只有几十行就能满足新增要求,第七次作业也无需经历大面积修改就完成了换乘任务。可见遵循良好的设计原则对于程序设计的重要性。
posted @ 2020-04-17 22:13  xcw1010  阅读(156)  评论(0编辑  收藏  举报