BUAA面向对象设计与构造——第二单元总结

第一阶段:单部傻瓜电梯的调度

第二阶段:单部可捎带电梯的调度

  (由于我第一次写的作业就是可捎带模式,第二次只是增加了负数楼层,修改了一部分参数,因此一起总结。)

  1.设计策略

  这次作业我设计了两个线程:Elevator和DealWithInput,前者模拟电梯,而后者用来处理输入。队列PersonQueue作为一个单例模式下的对象,被两个线程所共有,充当调度器的角色。

  DealWithInput基本套用下发的模板,只是增加了每次读到新请求就通知调度器pq.add(requst),并且pq.notifyall(),以用来唤醒电梯。在读到EOF后,设置标志输入结束的flag,并再次唤醒电梯,防止电梯一直等待。

  PersonQueue调度器里包含了一个Floor类型的数组,将每个楼层独立处理。每个Floor类中包含一个记录等待乘客的数组waitingPersion,乘客按到来的先后顺序进入数组。描述乘客的Person类记录了每个乘客的ID、起始楼层、目的楼层和运行方向。电梯向调度器询问主请求时,调度器扫描所有楼层,查看是否为空,只要有一个楼层不为空,电梯就仍需运行。当到达某一层后,电梯需要捎带,这时又要向调度器发出询问。调度器将这个询问转交给对应的Floor对象。Floor遍历本楼层全部乘客,如果有相同方向的乘客,则把这些乘客打包成一个数组返回给调度器,并且从该楼层的等待队列删去。之后调度器再返回给电梯。

  Elevator采用扫描算法,即当电梯静止时,先向调度器询问主请求,根据主请求决定运行方向,之后到达每一层如果还有同方向的乘客则捎带。下乘客则是由电梯自己判断,电梯包含了一个Person类型的eleList数组,记录电梯里的人。每到一层,先下后上,电梯遍历数组查看是否有人的目的楼层是当前层,如果有,则让他下电梯并且从电梯数组中删除。上下乘客结束后,电梯要向调度器询问是否需要改变方向。改变方向后进行二次上乘客。这个操作是针对这种情况:例如电梯向上运行到达5层,所以第一次查找乘客时只会查找5层要向上的乘客。但是如果这层没有人向上,电梯内已经没人了,5层以上的楼层也没有需求,因此电梯可以改变方向下,并且接5层向下的乘客。并且二次查找乘客也可以尽量多的捎带。

  时序图如下:

  

  第五次和第六次作业的一个比较大的区别在于:

  第五次作业电梯采用轮询方式,比较占用CPU。

  第六次作业改成wait()和notifyall()的模式,如果楼里没有请求,那么电梯会wait,而当输入进程读到了新的请求时会唤醒电梯进程。当读到EOF时,也需要唤醒电梯防止一直等待。

  2.结构度量

  类图如下:

  

  类图展现了类之间的实例化和创建关系。主进程MainClass创建了Elevator和DealWithInput两个进程,DealWithInput直接向PersonQueue增加请求,PersonQueue管理多个Floor类,每个Floor管理等待的Person.Elevator进程和调度器互相传递信息,同时也管理Person来记录电梯内部的人。

  

  以上是关于程序本身的一些数据统计。可以看到每个类的平均循环复杂度还是比较低的,说明方法的设计比较符合“高内聚、低耦合”的原则,而不是大多数功能集中在某一个方法中。

  3.程序bug

  第五次作业在公测中没有什么bug,但是用相同的架构在第六次的弱测就发现了bug。

  首先最致命的bug是程序CPU运行时间超时。因为我第五次作业采用轮询模式,电梯一直在while循环,每次循环都询问调度器是否需要运行。这种模式在第五次比较简单的测试中侥幸存活了下来,但是第六次就不行了。因此我改为了等待与唤醒的模式。电梯如果询问调度器得到的回答是暂时不需要运行,那么将调用pq.wait()进行等待。而输入进程一旦获取到新的需求,就调用pq.notifyall()来唤醒电梯。

  其次是电梯的方向问题。第五次作业我的电梯是送完乘客且运行方向的楼层没有乘客了,才进行方向的改变。但是这样有可能发生在某一层第一次开门下乘客,然后电梯意识到要改变方向,再次开门上乘客。因此为了节省时间以及更加符合实际,我在每次开关门之间增加了对运行方向的判断,修改为二次搭乘模式。

第三阶段:多部可捎带电梯的调度

  1.设计策略

  这次我依然选择将调度器作为一个共享对象,而不是进程的模式。输入进程依旧存在,电梯进程变成了三个,电梯单独运行的框架和前两次类似,也都是先向调度器询问主请求(即启动方向),每到一层都询问可捎带乘客和该下电梯的乘客。

  第七次作业比较大的一个改进在于赋予了Person类更多的功能。就像老师上课讲的工厂和工人的例子一样,乘客可以自己判断上下电梯,并且在一开始就规划换乘的路线。每当实例化一个Person对象时,将调用内部方法,来判断该乘客是否需要换乘。如果不需换乘,那么就当作正常的乘客,记录其起始楼层和目的楼层。如果需要换乘,那么将调用方法来得到他的换乘楼层(middleFloor),并且将这位乘客的目的楼层存入换乘楼层。当该乘客在目的楼层(即换乘楼层)下电梯后,再修改自身的起始楼层为换乘楼层,目的楼层为真正的目的楼层,将更新后的这名乘客再次加入等待队列。这样相当于把要换乘的乘客分为两名乘客,先后加入等待队列。这样就把换乘这一难题交付给乘客来完成,调度器辅助更新,而电梯只需要正常运行,更加清晰有效地分配了功能。因此IN和OUT这两个方法都在Person类,而不是电梯进程来完成。

  按照题目中电梯楼层的分配,有一些乘客是可以乘坐多个电梯都能到达的。例如ABC三部电梯都可以从1层到15层。因此我先在Person类中加入了可乘坐的电梯这一属性,用三位二进制数表示是否能乘坐对应电梯,例如其值为111表示三部电梯都可乘坐。这一属性是在判断换乘时同时完成的,而对于要换乘的人,在他第一次OUT后更新楼层信息时,同时更新这个属性。

  为了实现三部电梯的配合,我又引入了按钮属性。每个Floor类增加了三个数组,每个数组有两个元素,这六个值描述了ABC三部电梯上下行共六个按钮。当乘客第一次加入某一楼层等待队列时,考虑以下三种情况:

  1.只能坐一部电梯,那么就按对应电梯对应方向的按钮;

  2.可以坐两部电梯AB,那么若A到达时间短/A排队人数没有满员/B的反方向按钮被按则按A电梯,否则按B电梯。关于B的反方向按钮被按这一点,主要是因为如果B电梯要先以反方向经过本楼层的话,那么同方向会等更久。因此如果A的理论到达时间更短就优先选A。

  3.可以坐三部电梯,那么类似于第2种情况,从三部中选一部按按钮。

  因此每个乘客在刚进入等待队列时,都只按了一个理论上最合适的电梯。当然这只是理论值,不能排除电梯因为停靠楼层开关门所导致的时间延误。因此当电梯A(BC同理)每运行一层时,还要通知调度器刷新按钮。对于楼层x,如果此时A刚从x层上行,那么我们可以推测对于A再次以下行方向返回x层需要比较久的时间,因此对于低于x层的楼层,如果等待A电梯的人势必要等很久。因此这时刷新所有<=x层的楼层,如果等待A电梯的乘客中,有可以乘坐BC电梯的人,那么也按BC的按钮,这样如果BC电梯先于A电梯到达,那么这些人就可以早一步上电梯。

  这样一来,每个人虽然最开始只按了一个按钮,但可能因为刷新操作按多个。因此当电梯停靠一层并进行了上下乘客操作后,要遍历该楼层的所有乘客再次进行按电梯操作。这里的按电梯原则与最开始是一样的,都只按一个。按多个的刷新是在电梯运行到其他楼层时才进行。

  通过按钮这个属性,使得电梯之间的调度更加充分,而不是只集中在一部电梯上。每部电梯在向调度器询问时,调度器遍历每一楼层的按钮即可安排电梯接下来的运行方向了。

  2.结构度量

  类图如下:

  

  类图展现了类之间的实例化和创建关系。主进程MainClass创建了Elevator和DealWithInput两个进程,DealWithInput直接向PersonQueue增加请求,PersonQueue管理多个Floor类,每个Floor管理等待的Person.Elevator进程和调度器互相传递信息,同时也管理Person来记录电梯内部的人。Person类拥有了更多的功能,诸如向调度器报告按按钮,在电梯进程中进行IN和OUT的操作等。

  

  这一次作业Floor类和Person类的复杂度增长幅度很大,主要是因为赋予了Person更多的功能,以及Floor添加了按钮属性所导致的。但总体来看复杂度还算比较平均,没有出现某一类包揽大多数功能的情况。

  3.程序bug

  公测中全部通过,但性能分不太高。我认为是因为我的电梯调度还是针对于一些细节部分的优化,例如尽量多按按钮来使得乘客尽早上电梯。但是这也可能导致电梯空跑。我觉得一个绝对的优化策略还是很难找(几乎不存在)的,因为电梯对即将到来的乘客是未知的。因此优化这一点只能尽力从架构上设计地更完备,考虑更多的情况。

  在弱测和本地测试中,我发现我还是遇到了CPU超时的情况。虽然单部不会忙等,但是由于多部电梯同时工作,才会导致这一错误。当AB电梯都没有需求后,他们发现输入也结束了,但是楼里还有人,因此会进入等待状态,此时C还在工作,当C结束工作后,他检测到输入也早早结束了,因此结束了进程。但是此时AB没有被唤醒,也就无法检测楼里没人这个真正的结束条件。另一种情况是最后一个乘客需要换乘,他从C电梯出来后,AB电梯还在等待,也就没有电梯来接他了。因此我对于电梯在每一层的运行结束后都加入了notifyall,这可以解决以上两个问题,并且对于不需要工作的电梯,即使被唤醒后也能马上再次进入等待。

总结 

  根据SOLID原则分析:

  SRP(单一责任原则):我一共设计了MainClass, DealWithInput, Elevator, PersonQueue, Floor, Person六个类,前三个类分别是三个进程,第四个作为调度器,后两个分别针对楼层和乘客两个角度设计功能。实现了功能的划分,使得程序架构条理清晰。

  OCP(开放封闭原则):指架构应该是可扩展的。这个单元我做的比较好的一点就在于减少了代码的重构,第六次和第五次代码几乎一样,只是针对bug和指导书不同的要求进行一小部分的修改。第七次作业里,每部电梯的运行架构也和第六次一样,只是增加了电梯间配合调度的功能。

  由于本次作业没有使用继承、接口和抽象类,因此其他三个原则不做讨论。

  对于线程安全方面,我采用synchronized加锁的模式。所有针对调度器即等待队列的操作都需要加锁,包括添加乘客,捎带乘客(即取走乘客)和判断是否为空等。这样才能保证对象的安全,防止出现一个乘客同时上两部电梯,或者是从队列中删除但是却没有上电梯的错误。

  总之经过这一单元的练习,增强了我对多线程的认识,对于线程安全有了更深刻的体会。不再像第一单元那样每次重构代码,是本单元最大的进步。

BUAA面向对象设计与构造——第二单元总结的更多相关文章

  1. BUAA面向对象设计与构造——第一单元总结

    BUAA面向对象设计与构造——第一单元总结 第一阶段:只支持一元多项式的表达式求导 1. 程序结构 由于是第一次接触面向对象的编程,加之题目要求不算复杂,我在第一次作业中并没有很好利用面向对象的特点, ...

  2. 面向对象设计与构造:JML规格单元作业总结

    面向对象设计与构造:JML规格单元作业总结 第一部分:JML语言理论基础 JML语言是什么:对Java程序进行规格化设计的一种表示语言 使用JML语言有什么好处: 用逻辑严格的规格取代自然语言,照顾马 ...

  3. 面向对象设计与构造:oo课程总结

    面向对象设计与构造:OO课程总结 第一部分:UML单元架构设计 第一次作业 UML图 MyUmlInteraction类实现接口方法,ClassUnit和InterfaceUnit管理UML图中的类和 ...

  4. 从结构和数字看OO——面向对象设计与构造第一章总结

    不知不觉中,我已经接触OO五周了,顺利地完成了第一章节的学习,回顾三次编程作业,惊喜于自身在设计思路和编程习惯已有了一定的改变,下面我将从度量分析.自身Bug.互测和设计模式四个方向对自己第一章的学习 ...

  5. BUAA OO 2019 第二单元作业总结

    目录 总 架构 controller model view 优化算法 Look 算法 多种算法取优 预测未来 多线程 第五次作业 第六次作业 第七次作业 代码静态分析 UML 类图 类复杂度 类总代码 ...

  6. 2020-BUAA-OO-面向对象设计与构造-第四单元总结&课程总结

    咱的OO结束辣! Part1: Unit4 Summary 本单元作业,我主要使用了适配器模式和访问者模式.总体上看,代码量和文件数量有所上升,但配合分包等措施后,文件结构清晰,各部分耦合度均较低.缺 ...

  7. 「BUAA OO Unit 2 HW8」第二单元总结

    「BUAA OO Unit 2 HW8」第二单元总结 目录 「BUAA OO Unit 2 HW8」第二单元总结 Part 0 前言 Part 1 第五次作业 1.1 作业要求 1.2 架构设计 1. ...

  8. 第二单元总结:基于synchronize锁的简单多线程设计

    单元统一的多线程设计策略 类的设计 电梯 每部电梯为一个线程. 电梯从调度器接收原子指令,知晓自己的状态(内部的人/服务的人.运行方向.所在楼层) 原子指令包括且仅包括: 向上走一层 / 向下走一层 ...

  9. 面向对象设计(Object-Oriented Design,OOD)

    前言 OOD简介 Shubho:亲爱的,让我们开始学习OOD吧.你了解面向对象原则吗? Farhana:你是说封装,继承,多态对吗?我知道的. Shubho:好,我希望你已了解如何使用类和对象.今天我 ...

随机推荐

  1. JDK常用命令行工具(基于JDK10)

    虽然我是在jdk10环境下, 但是大体上和jdk8是差不多的. 总共有这么多 本来想着一口气把所有命令都边学边总结一下的, 结果发现....有些还真的不是很常用....或者说我这个水平还接触不到那么多 ...

  2. C#代码使用Process类调用SWFTools工具

    一.Process类调用SWFTools工具将PDF文档转为swf文档 1 string cmdStr = "D:\\SWFTools\\pdf2swf.exe"; string ...

  3. python excel写入及追加写入

    # -*- coding:utf-8 _*- """ @author:Administrator @file: excel.py Description :如果行数是10 ...

  4. DUMP 5 企业级电商项目

    [订单模块] 创建订单 商品信息  订单列表  订单详情 取消订单 订单列表  订单搜素  订单详情  订单发货 [创建订单]  购物车勾选商品 涉及 Cart Product  => 一个商品 ...

  5. 高阶函数(Higher-order function)

    变量可以指向函数 以Python内置的求绝对值的函数abs()为例,调用该函数用以下代码: >>> abs(-15) 15 但是,如果只写abs呢? >>> abs ...

  6. 从XML文件和properties文件提取数据

    XML文档格式内容如下 <?xml version="1.0" encoding="UTF-8"?> <root>     <fi ...

  7. git怎样删除未监视的文件untracked files

    # 删除 untracked files git clean -f # 连 untracked 的目录也一起删掉 git clean -fd # 连 gitignore 的untrack 文件/目录也 ...

  8. 红警大战JAVA简单版

    代码结构: 相关源码: 武器类: 属性:武器,攻击力,子弹数量. 方法:给属性赋值(set属性()方法) 获取属性值(get属性()方法) package 红警大战简单版; public class ...

  9. Fatal error encountered during command execution

    MySQL + .net + EF 开发环境,调用一处sql语句报错: Fatal error encountered during command execution[sql] view plain ...

  10. 华南理工大学“三七互娱杯”程序设计竞赛(重现赛)( HRY and array 高精度除法模板)

    题目链接:https://ac.nowcoder.com/acm/contest/874/D 题目大意:给你两个数列a和b然后对a可以进行排列,对b可以任意排列,问你sigma(a(i)*b(i))的 ...