面向对象程序设计——电梯问题总结

面向对象程序设计——电梯问题总结

本次电梯有三次问题,从一台电梯到多台电梯,从傻瓜单次调度到可稍带等优化调度。难点主要在于多线程的设计,以及调度策略等。

设计思路

本次实验较好的一点是设计风格基本保持了较好的可扩展性,三次作业中没有产生明显的重构,所以这次就主要针对与第三次作做一个总结。

本次电梯我主要将人乘坐电梯这件事分为三个主体,即请求、电梯和调度器。

其中请求是最简单的一个部分,在第一和第二次作业中我直接使用了提供的PersonRequest接口,并没有给请求赋予更多的特性和功能,其控制是由电梯和调度器来进行的,其状态取决于其当前所处电梯和调度器中的数据结构的具体位置。

每个电梯都是一个线程,其从整体上来看只存在三个状态,向上,向下和暂停(即线程wait)。另外,其内部储存着其每层运行时间、开关门时间、容量、电梯名称、当前楼层、门的状态(开/关)、可到达楼层和已上电梯的请求。电梯不关心请求如何分配,不关心目标的楼层是什么(只关心是要向上还是向下),也不关心什么人需要上电梯。电梯关心的是当前楼层里有没有请求需要离开电梯,还有接下来是向下一层、向上一层还是暂停。

调度器是一个托盘,采用单例对象保证只有一个调度器且方便电梯调用其方法。调度器负责输入的处理和分解,负责处理哪些请求需要上电梯,以及接下来是什么状态。

调度方法

调度由调度器进行。其中分为请求的分析以及具体的调度

请求的分析

在调度器中,利用一个ElevatorMap来记录各个电梯可以到达得楼层的情况,使用的是类似于邻接矩阵的形式,每一层楼是一个节点,不过这里矩阵中的元素不是一个数字,而是一个储存着可达情况,以及可使用电梯和需要时间的对象。对于每个输入的请求,会在这个图上利用Dijkstra算法求出最短路径,这样就得到了我们对此请求的分解,将这个最短路径信息(包含起始楼层,到达楼层以及可以乘坐的电梯编号,以ArrayList储存)封装进继承于PersonRequest的新的请求类,并加入调度器的等待队列中。其中这个新的请求类会将第一个路径的起始和结束作为getFromFloor和getToFloor的返回值,将剩下的路径封装起来,待该请求下电梯完成当前路径后,接下来的路径就会取代之前的路径。这样就在储存请求分解方式的同时,解决了对分解后的请求顺序有要求的问题。

具体的调度

在每个电梯对象中有两个请求队列,一个队列是电梯已经接受的请求队列,另一个是电梯假装接受的请求队列。

当电梯每到一层时,先判断此层是否是自己的可停靠层,若不是则输出ARRIVE后前往下一层,若是,则首先判断有没有请求要下电梯,之后将自己传入调度器中,由调度器决定是否有人需要上电梯。首先调度器先判断当前电梯的门是否已经打开了,如果没有调度器则根据等待队列中的请求信息先判断是否有请求要在这一层出发并且方向相同且可以乘坐当前电梯,如果没有则返回。如有,则先开门,在将当前队列中所有符合出发楼层要求以及电梯要求的同方向请求存入已接受的队列中,并从调度器的等待队列中除去,对于符合出发楼层要求以及电梯要求的不同方向电梯,则放入假装接受的请求队列中,并且不从调度器的等待队列中除去。之后电梯判断门的状态,若开着则将门关闭。

这里的所谓假装接受的请求,是我为了减小算法难度,同时又想尽量多捎带一些人少开门的结果。其实等同于对这些请求做了一个标记,指示说这个请求已经进到某个电梯了,下次“不用开门就可以直接进”。我最后发现这个也完全可以给请求做标记来实现,不用给电梯单独多加一个队列来储存。当然这个操作非常的不自然,最终也的确给我带来了好多个bug,值得我的反思。

最后电梯判断当前自己已经接受符合当前方向的请求是否都已经处理完毕,若没有,则方向不变地前往下一个楼层,若已经为空则将自己传给调度器,由调度器决定下一步的方向。调度器则从等待队列中查找第一个该电梯可以接受的请求,将前往其出发楼层作为电梯的方向。若当前等待队列中不存在符合要求的请求,则让电梯线程等待(wait),直到被notify。

最终,如何判断电梯完全运行结束呢?当输入结束且三个线程都wait的时候就已经结束了,我的具体实现方法是在调度器中设置两个标志inputEnd和everythingEnd,当输入结束后就给inputEnd置True,每当电梯要wait之前,都判断一下当前everthingEnd了吗?或者inputEnd且其他两个电梯都已经wait了吗?如果是,则将everythingEnd置为True,并notify其他的线程,之后结束运行。

多线程控制

第三次作业中我作为托盘的调度器是需要做线程安全处理的对象,而其中其需要线程安全的对象就是等待队列waitingRequestList,为了使开关锁更加灵活,我使用了ReentrantLock来实现对等待队列的访问互斥,我没有将读写分来,因为调度器中的读写常常是伴随的,所以所以需要访问等待队列的操作都需要上锁,其中尤其注意在一整个对循环队列的循环中,锁不可以释放,否则会出现危险,详见BUG部分。在线程同步上,我使用了配套于Lock类的Condition类,使用其condition.await,以及condition.signalAll即相当于wait和notifyAll方法,当等待队列中没有电梯可以处理的请求时电梯就wait,当有新的请求加入时就notifyAll,其中需要注意的是不仅新的请求可以从输入中获得,需要换乘电梯的请求在中途下电梯后也会产生新的请求,同样需要notifyAll。

 

程序分析

下图是第一次单独电梯程序的类图,由于要求比较低,我只设置了两个类,Main和Elevator,分别作为两个线程,保证了最基本的功能。

Methodev(G)iv(G)v(G)
Elevator.Elevator(LinkedList<PersonRequest>)111
Elevator.goToFloor(int)212
Elevator.letSomeoneIn(int)111
Elevator.letSomeoneOut(int)111
Elevator.run()435
Elevator.waitFor(int)112
Main.main(String[])333
ClassOCavgWMC
Elevator1.6710
Main33
Packagev(G)avgv(G)tot
 2.1415

第一次作业的程序复杂度也比较低(写的这么简易肯定低啊),只有ELevator.run由于集成度较高所以复杂度偏高一些。

在第二次作业中,我多设置了一个调度器类作为托盘使用,还是两个线程Main和Elevator,第二次作业中类之间的关系还算比较明晰,尤其是和之前求导的作业相比,没有过多的耦合。

Methodev(G)iv(G)v(G)
Dispatcher.Dispatcher()111
Dispatcher.addRequest(PersonRequest)122
Dispatcher.anyoneGetOn(Elevator)41313
Dispatcher.askDirection(Elevator)336
Dispatcher.checkDirection(PersonRequest)212
Dispatcher.getInstance()111
Dispatcher.getNewDirecion(PersonRequest,int)212
Elevator.Elevator()111
Elevator.addRequest(PersonRequest)111
Elevator.addTempInRequest(PersonRequest)111
Elevator.anyoneGetOff()133
Elevator.changeDirection(Direction)111
Elevator.changeDoorState()112
Elevator.checkRemoveTempIn()333
Elevator.closeDoor()122
Elevator.getDirection()111
Elevator.getDoorState()111
Elevator.getFloor()111
Elevator.goNextFloor()323
Elevator.goToFloor()114
Elevator.isEmpty()111
Elevator.openDoor()122
Elevator.run()334
Elevator.waitFor(int)123
Main.main(String[])333
ClassOCavgWMC
Dispatcher3.1422
Elevator1.8832
Elevator.Directionn/a0
Elevator.DoorStaten/a0
Main33
Packagev(G)avgv(G)tot
 2.5664

但由于我对调度器调度请求上电梯的方法的设计有一些不太合理,所以可以看到anyoneGetOn方法复杂度过大,明显高于其他方法的复杂度以及平均复杂度,最后在强测中被爆的bug也证明了这里的设计有一些问题。

第三次作业中,主要的三个类仍然是Dispatcher,Elevator和Main,剩下的三个类分别是用于拓展PersonRequest的PersonRequestNew,用于求得请求分解方法的ELevatorMap以及Map的基本元素MapCell。

Methodev(G)iv(G)v(G)
Dispatcher.Dispatcher()111
Dispatcher.addProcessedRequset(PersonRequestNew)212
Dispatcher.addRequest(PersonRequest)122
Dispatcher.anyoneGetOn(Elevator)41818
Dispatcher.askDirection(Elevator)357
Dispatcher.checkDirection(PersonRequestNew)212
Dispatcher.findFirst(int)323
Dispatcher.getInstance()111
Dispatcher.getNewDirecion(PersonRequestNew,int)212
Dispatcher.initialFloorStops()122
Dispatcher.printOutput(String)111
Dispatcher.setFloorStopsAndMap(Integer[],Integer[],Integer[])144
Elevator.Elevator(int,int,int,Integer[])111
Elevator.addRequest(PersonRequestNew)111
Elevator.addTempInRequest(PersonRequestNew)111
Elevator.anyoneGetOff()133
Elevator.changeDirection(Direction)111
Elevator.changeDoorState()112
Elevator.checkRemoveTempIn()333
Elevator.closeDoor()122
Elevator.getDirection()111
Elevator.getDoorState()111
Elevator.getElevatorId()111
Elevator.getFloor()111
Elevator.getIdChar()111
Elevator.goNextFloor()323
Elevator.goToFloor()114
Elevator.isEmpty()111
Elevator.isFull()111
Elevator.openDoor()122
Elevator.printOutput(String)111
Elevator.run()345
Elevator.waitFor(int)123
ElevatorMap.ElevatorMap(Integer[],Integer[],Integer[])199
ElevatorMap.dis(int,int)222
ElevatorMap.dr(int)111
ElevatorMap.r(int)111
ElevatorMap.shortestWay(int,int)4610
Main.main(String[])333
MapCell.MapCell(int,int,int)111
MapCell.checkAndModify(int,int)112
MapCell.getDis()111
MapCell.getElevators()111
MapCell.getEnd()111
MapCell.getStart()111
OutputProcess.main()111
PersonRequestNew.PersonRequestNew(ArrayList<MapCell>,PersonRequest)112
PersonRequestNew.getNextRequest()111
PersonRequestNew.isIntheList(Integer)111
PersonRequestNew.resetList(Integer)111
ClassOCavgWMC
Dispatcher2.8334
Elevator1.7637
Elevator.Directionn/a0
Elevator.DoorStaten/a0
ElevatorMap4.422
Main33
MapCell1.177
OutputProcess11
PersonRequestNew1.255
Packagev(G)avgv(G)tot
 2.46123

第三次作业中复杂度最高的仍然是anyoneGetOn方法,但万幸的是这次bug不是出现在这里...ELevatorMap中的求最短路的方法复杂度也较高,但实际上此方法的思想并不复杂,没有太多的细节。

对于SOLID原则,我认为我这次并没有很好地贯彻,在编码的时候对其认识还不够,只有单一责任原则做的稍微较好,开放封闭原则有一些的关注,但我这次程序设计中并没有使用继承或者工厂模式,所以在里氏替换和依赖倒置原则上也较难评判,在接口隔离上我觉得我做的不够好,在设计中我也注意到相关问题但在已经形成的框架下没有找到好的解决方法,在以后的设计中我会从更加整体的角度来考虑设计。

 

BUG

第一次作业题目比较简单,所以在强测和互测中均没有出现问题,但在线下自己的调试中的确发现了很多之前没有注意到的事情,大多是关于多线程的互斥等方面。

第二次作业中随着复杂度的上升,对线程控制的难度也有所上升。不过这次的bug虽然与线程控制有关,但却是因为我对迭代器认识不足造成的。之所以使用迭代器来遍历List是因为迭代器可以实现在循环中的remove,我想当然地就认为使用迭代器时任何对队列的操作都不会产生什么影响,但事实上由于我在迭代中可能会由于需要开门而进入睡眠,导致睡眠中如果有新的请求插入队列,再返回后迭代器就会报出ConcurrentModificationException。导致我的强测炸掉了一半的点,但为什么线下没有发现呢?因为我没有来得及写定时输入,所有的测试都是手动输入,所以就不太会出现这种问题,但是在定时输入下这种问题出现的几率就非常非常大了,仔细想想炸掉一半的点其实已经算很少了。解决方法就是在这之前专门寻找一次有没有需要开门的请求,如果需要开门并睡眠,则在返回后直接break,避免调用迭代器,就不会有这种问题了,之后再在另外一个不会sleep的循环里正常使用迭代器就可以了。

有了第二次惨痛的教训,在第三次的作业中我当然就更加小心,使用了自动化测试对电梯进行测试,但还是在测试的时候遗漏掉了一个点而被强测找了出来,这个bug是由于我有所谓假装接受请求的策略,然而我在查看当前电梯是否为空时没有考虑假装接受的请求的队列,再加上我在调度请求的一些其他操作,导致了我可能会在电梯满员的时候还假装接受其他的请求。但是虽然我是假装接受,但实际上白纸黑字的输出IN,评测机可不管你假装不假装啊唉!

在查别人的bug时我主要使用自动化评测,查到了三个人的bug但在评测系统上只刀中了一个bug,剩下两个人的bug难以复现,但的确实实在在存在,这就提醒我们即使我们顺利通过了强侧和互测,也不代表我们的代码是百分之百正确的。除了自动化评测,在第三次互测中我发现了有一位同学有和我第二次互测中相同的问题,不过他的问题比较隐蔽难以发现,需要非常巧合的数据才可以造成bug,我构造了很久的数据也难以实现。、

 

心得体会

这三次电梯作业和上一单元的求导作业相比少了输入处理的难度,给设计减少了很多负担,增添了不少乐趣。本次作业主要关注于多线程的设计,多线程程序的设计与以往的程序有很大的不同,需要考虑多个线程之间并行运行,需要考虑他们之间的资源互斥与信息传递,而多线程调度的不确定性也给设计带来了很多困难,这些需要去学习去适应的。在编码调试的过程中也发现,以往的调试方法可能已经失效了(有时候还是可以用的),并且多线程的不确定性使得同样的输入数据有时候可以正确运行,有时候却不能,我们需要做到的不仅是让程序可以在一次运行中得到正确的结果,而是使得程序在每次运行中都得到正确解,这就需要我们对多线程程序的设计有比较深的体会对多线程中容易出现的问题有足够的敏锐性,也要遵循多线程程序设计中的准则来避免问题的发生。

 

转载于:https://www.cnblogs.com/yhhhz/p/10763864.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 面向对象程序设计分为两种,一种是基于类的,另一种是基于原型的。其中基于类的程序设计是最常见的一种,它主要涉及到两个概念,即类和对象。类是一个抽象的概念,它描述了一类具有相同属性和方法的对象,而对象则是具体的实例。在基于类的程序设计中,类用于描述对象的属性和方法,而对象则是类的具体实现。因此,基于类的程序设计更注重对象的封装、继承和多态等概念。 ### 回答2: 面向对象编程是一种程序设计范式,它把程序看作是一组对象的集合,每个对象都有一个特定的属性和方法。在Python中,每个对象都是一个类的实例,类是一个抽象的概念,用于表示一组相似的对象。对象是类的实例,它继承了类的属性和方法,同时可以拥有自己的属性和方法。 在Python中,类定义的语法为: class ClassName: # 属性和方法 在类定义中,可以定义属性和方法,类的属性是一些数据,用来描述类的特征;类的方法是一些函数,用来描述类的行为。同时,类还可以继承其他类的属性和方法,通过继承,子类可以重用父类的代码,同时也可以增加自己的属性和方法。 在Python中,对象的创建和使用的语法为: object_name = ClassName() 在对象的创建过程中,会调用类的构造函数(__init__()方法),用来初始化对象的属性和方法。然后,可以使用对象的属性和方法来完成具体的任务。 在面向对象编程中,有三个重要的概念,分别是封装、继承和多态。封装是指把数据和行为包装在一个类中,对外部隐藏具体的实现细节;继承是指子类可以继承父类的属性和方法,在子类中可以重写父类的方法和添加自己的方法;多态是指同一个方法可以在不同的对象上产生不同的结果,通过实现多态,程序可以更加灵活和可扩展。 最后,在Python中,还可以通过__str__()方法来自定义对象的字符串表示方式,方便输出和调试。同时,还可以通过@property和@setter装饰器来实现属性的封装和保护。 总之,类和对象是面向对象编程的基础,它提供了一种更加灵活和可扩展的编程方式,同时也对程序的设计和架构提出了更高的要求。因此,熟练掌握类和对象的相关知识,是Python编程的基础和必备技能。 ### 回答3: Python中的面向对象编程(Object-Oriented Programming,OOP)是一种将数据和行为封装在一起,以创建对象为中心的思想。在Python中,一切皆为对象,从简单的数字、字符串到复杂的列表、元组都是对象。面向对象(Object-Oriented)编程中最基本的两个概念是类和对象。 类是一种数据类型,是一组相关数据和方法的描述。其实就是一个对象的模板,包含了它所描述的对象共有的属性和方法。Python中定义类的方法比较简单,使用关键字`class`即可,例如: ```python class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): print("汪汪!") ``` 上述代码定义了一个Dog类,包含了Dog对象共有的属性(name和age)和方法(bark)。通过关键字`def`定义Dog类的方法,其中`__init__`方法是一个特殊的方法,用于初始化类的对象属性。 当我们需要使用Dog类时,需要将它实例化为一个对象。例如: ```python dog1 = Dog("小黑", 3) dog1.bark() # 输出:汪汪! ``` 上述代码创建了一个Dog对象,设置了它的name为“小黑”,age为3,然后调用了Dog类的方法bark。 另外,Python中还有一些内置函数,可以用于操作类和对象。例如: - `type`函数:用于返回对象的类型。使用方法为`type(object)` - `isinstance`函数:用于检查对象是否为指定类的实例。使用方法为`isinstance(object, classinfo)` - `hasattr`函数:用于检查对象是否具有指定的属性。使用方法为`hasattr(object, name)` - `getattr`函数:用于获取对象的属性值。使用方法为`getattr(object, name[, default])` - `setattr`函数:用于设置对象的属性值。使用方法为`setattr(object, name, value)` 以上是Python中定义类和使用类的基本语法和函数,掌握了这些知识后,可以尝试设计自己的类并应用到实际的编程项目中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值