面向对象课程第二单元作业总结
面向对象课程第二单元作业总结
前言
电梯系列作业分为三个阶段,逐步深入实现越来越复杂的电梯运行状态模拟。
第一阶段实现单部多线程傻瓜调度(FAFS)电梯的模拟;
第二阶段实现单部多线程可捎带调度(ALS)电梯的模拟,在第一次作业的基础上添加了捎带需求;
第三阶段实现多部多线程智能(SS)调度电梯的模拟,在第二次作业的基础上增加了换乘的需求。
目录
一、设计策略
- 整体架构
- 电梯调度算法
二、实现评估
- 经典OO度量和类图
- 线程间协作
- SOLID分析
三、漏洞分析
- 线程间状态通信
- 线程间数据共享
四、心得体会
一、设计策略
电梯系列作业的两个重点在于代码架构和调度算法设计。下面就代码架构和三次调度算法的设计分别阐述。
整体架构
在第一次作业的架构设计过程中,受到上一次作业的启发,考虑了架构对于后续扩展的适应性,所以本单元的三次作业的架构基本上没有太大改变。
电梯调度的情境中,很显然存在三个对象——模拟用户输入、调度器和电梯。考虑到后续可能存在的多电梯的扩展,笔者将用户输入,调度器和电梯分别设计成三个线程,用户输入和调度器、调度器和电梯之间分别设置一个共享对象由于数据的交互。
本着“功能分离”的原则,笔者将整个模拟运行的过程分为几个小模块分给不同的对象执行。用户输入只管接收请求,并将请求放入与调度器的共享队列中;调度器只管从用户请求队列取指令,根据此时电梯的运行状态(剩余容量、运行速度等)信息对请求进行一定的分解,将分解出的指令转发给特定的电梯执行;电梯只管运行调度器分配的指令。关于详细的线程间数据共享问题在后面会详细论述。
结构图如下:
电梯调度算法
对于不同的电梯调度需求需要应用不同的电梯调度算法。
第一阶段电梯需求非常简单。因为是第一次编写多线程程序,所以笔者没有在调度算法上花费太多时间,使用了先来先服务的策略,更多地关注了Java的多线程机制。
第二阶段电梯需要实现捎带请求。参照现实中电梯的运行规律,确定了“一条道走到黑”的调度算法。电梯初始运行时先根据各楼层上下人数的多少确定一个运行方向,向这个方向运行直到该方向的楼层没有乘客并且电梯内没有乘客为止,然后再重新开始这个过程。在电梯向某个方向运行的过程中,电梯会检查并捎带当前停靠楼层同方向的乘客。这样虽然很多情况下不是最优的算法,但是保证了电梯在两趟(一上一下)内能够将现有的所有乘客送达目的地。
第三阶段电梯电梯调度算法的重点在于实现换乘需求。说起换乘,就想到了寻路;说起寻路,就想起了图算法。那么怎样建立这个图呢?再次参考现实情况,往往人在乘坐公共交通工具的时候会选择少换乘的路线,一来这样比较方便,二来可以减少影响到达时间的因素,进而更快到达目的地。
根据少换乘的原则确定调度算法。将楼层抽象成结点,根据某一部电梯可到达的楼层在这些楼层之间建立一个无向完全图,以边的类型来标记根据不同可达楼层构造的无向完全图。
搜索使用类似广度优先搜索算法,找出所有换乘最少的线路,然后根据当前电梯中的人数、电梯运行速度等参数确定一条Cost最小的路线。
二、实现评估
经典OO度量和类图
电梯系列作业的代码架构在第一次作业时就已经确定,但是在实际完成过程中应实际代码编写的需求和性能方面的考虑,实际上还是有少许改动。
第一二次作业评估
第一、二次作业的类图和代码复杂度度量类似,在这里仅列出第二次作业的分析。
第二次作业类调用关系图:
类图大体上清晰明了,三类线程,三个共享对象,其中因为模拟用户输入和调度器之间、调度器和电梯之间的共享对象涉及到线程终止通信机制,所以继承了SharedObject
类。关于线程结束的机制在后面论述。
第二次作业类代码长度:
第二次作业代码复杂度:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
homework.Main.main(String[]) | 1 | 1 | 1 |
homework.common.SharedObject.SharedObject() | 1 | 1 | 1 |
homework.common.SharedObject.SharedObject(boolean) | 1 | 1 | 1 |
homework.common.SharedObject.ioStreamOpen() | 1 | 1 | 1 |
homework.common.SharedObject.setIoStreamState(boolean) | 1 | 1 | 1 |
homework.common.TwoTuple.TwoTuple(A,B) | 1 | 1 | 1 |
homework.common.TwoTuple.getFirst() | 1 | 1 | 1 |
homework.common.TwoTuple.getSecond() | 1 | 1 | 1 |
homework.elevator.Elevator.Elevator(ElevatorController,ElevatorState) | 1 | 1 | 1 |
homework.elevator.Elevator.atomRun() | 1 | 3 | 3 |
homework.elevator.Elevator.automaticRun() | 1 | 3 | 3 |
homework.elevator.Elevator.bootElevator(int) | 1 | 2 | 2 |
homework.elevator.Elevator.closeDoor() | 1 | 1 | 1 |
homework.elevator.Elevator.inOne(int,int) | 1 | 1 | 1 |
homework.elevator.Elevator.inPack(List<TwoTuple<Integer, Integer>>) | 1 | 2 | 2 |
homework.elevator.Elevator.move() | 1 | 4 | 4 |
homework.elevator.Elevator.openDoor() | 1 | 1 | 1 |
homework.elevator.Elevator.outOne(int) | 1 | 1 | 1 |
homework.elevator.Elevator.outPack(Set) | 1 | 2 | 2 |
homework.elevator.Elevator.run() | 3 | 5 | 5 |
homework.elevator.Elevator.targetRun(int) | 1 | 2 | 2 |
homework.elevator.ElevatorController.ElevatorController(ElevatorState) | 1 | 2 | 2 |
homework.elevator.ElevatorController.acceptRequest(PersonRequest) | 1 | 1 | 1 |
homework.elevator.ElevatorController.popInPack() | 1 | 1 | 1 |
homework.elevator.ElevatorController.popInPack(int) | 1 | 3 | 3 |
homework.elevator.ElevatorController.setDestFloor() | 8 | 7 | 9 |
homework.elevator.ElevatorController.setRunDirectionByFloor(int) | 1 | 3 | 3 |
homework.elevator.ElevatorController.setRunDirectionByPeopleNumber(int) | 1 | 2 | 2 |
homework.elevator.ElevatorController.wake() | 1 | 1 | 1 |
homework.elevator.ElevatorState.ElevatorState() | 1 | 2 | 2 |
homework.elevator.ElevatorState.getCurrentFloor() | 1 | 1 | 1 |
homework.elevator.ElevatorState.getDirection() | 1 | 1 | 1 |
homework.elevator.ElevatorState.getOutPack() | 1 | 1 | 1 |
homework.elevator.ElevatorState.getOutPack(int) | 1 | 1 | 1 |
homework.elevator.ElevatorState.isEmpty() | 1 | 1 | 1 |
homework.elevator.ElevatorState.personIn(int,int) | 1 | 1 | 1 |
homework.elevator.ElevatorState.personOut(int) | 1 | 1 | 1 |
homework.elevator.ElevatorState.setCurrentFloor(int) | 1 | 1 | 1 |
homework.elevator.ElevatorState.setDirection(Direction) | 1 | 1 | 1 |
homework.elevator.Floor.Floor(int) | 1 | 1 | 1 |
homework.elevator.Floor.PassengerCome(int,int) | 1 | 2 | 4 |
homework.elevator.Floor.downGroupSize() | 1 | 1 | 1 |
homework.elevator.Floor.getBuildingFloors() | 1 | 1 | 1 |
homework.elevator.Floor.getPeopleGoDown() | 1 | 1 | 1 |
homework.elevator.Floor.getPeopleGoUp() | 1 | 1 | 1 |
homework.elevator.Floor.isEmpty() | 1 | 2 | 2 |
homework.elevator.Floor.nearFloor(int,int) | 1 | 1 | 4 |
homework.elevator.Floor.upGroupSize() | 1 | 1 | 1 |
homework.request.RequestList.RequestList() | 1 | 1 | 1 |
homework.request.RequestList.add(PersonRequest) | 1 | 1 | 1 |
homework.request.RequestList.fetchAll() | 3 | 4 | 4 |
homework.request.RequestList.othersNotify() | 1 | 1 | 1 |
homework.request.UserInput.bindBuffer(RequestList) | 1 | 1 | 2 |
homework.request.UserInput.run() | 3 | 3 | 4 |
homework.schedule.Scheduler.Scheduler(RequestList) | 1 | 1 | 1 |
homework.schedule.Scheduler.assignRequestTo(PersonRequest,ElevatorController) | 1 | 1 | 1 |
homework.schedule.Scheduler.assignRequests(List) | 1 | 2 | 2 |
homework.schedule.Scheduler.register(ElevatorController) | 1 | 1 | 1 |
homework.schedule.Scheduler.run() | 3 | 4 | 5 |
Total | 74 | 98 | 108 |
Average | 1.25 | 1.67 | 1.83 |
从上面的类代码长度和经典OO度量来看,每个类的代码长度大致均衡,复杂度、耦合度较低,功能划分相对明确;空行、注释比列合理,代码风格良好。
第三次作业评估
第三次作业类图:
由于第三次作业IDEA自动生成的类图实在让人不忍细看,所以笔者手绘了一幅简化版,简化了很多类间调用的细节。
第三次作业类代码长度:
从类代码长度来看,最大的类是Elevator
电梯运行模拟类,纯代码189行;空行比例10%,注释比例21%;代码风格良好。
第三次作业经典OO度量:
Class | OCavg | WMC |
---|---|---|
homework.Main | 1 | 1 |
homework.common.SharedObject | 1 | 4 |
homework.common.TwoTuple | 1 | 3 |
homework.elevator.Elevator | 2 | 38 |
homework.elevator.ElevatorController | 2.08 | 25 |
homework.elevator.ElevatorState | 1.09 | 12 |
homework.elevator.ElevatorState.Direction | 0 | |
homework.elevator.Floor | 1.62 | 13 |
homework.request.RequestList | 1.5 | 6 |
homework.request.UserInput | 2.5 | 5 |
homework.schedule.Instructor | 2 | 20 |
homework.schedule.Scheduler | 1.86 | 13 |
homework.schedule.graph.Edge | 1 | 5 |
homework.schedule.graph.Graph | 3.29 | 23 |
由于方法数量过多,所以在这里仅展示类的平均圈复杂度和总圈复杂度。
从上面的数据可以看出,Graph
类方法复杂度较高,而Elevator
类体量过大。改进方案可以对Graph
类中的方法进行优化,并简化Elevator
类的运行逻辑。
除此之外还有两个方法复杂度超标。
其中一个是elevator.ElevatorController.setDestFloor()
,用于设置初始电梯运行楼层。实际上这个方法的并不复杂,这个方法中需要使用循环来读取各楼层中乘客的数量,其中还有分支语句,因而出现了了复杂度过高的问题。
另外一个是schedule.graph.Graph.route(int,int,int[])
,顾名思义,这个方法是用来搜索完成请求的最少换乘路径的。这个方法基于广度优先搜索算法来实现最少换乘路径的搜索,需要维护很多的状态,各个逻辑之间关联度较高,不易分割。而且图存储的数据结构使用的是两层HashMap
嵌套,这就造成了在读取数据时逻辑复杂度上升,编程和调试的体验都非常差。改进方案可以进一步封装图存储的数据结构,并提供各种各样的数据访问接口,虽然在程序的性能上可能会有所损失(实话这小破程序还不需要抠那点儿性能),但是带来的结构上的优化是不可小觑的。
线程间协作
线程间的协作可以分成两个部分,一个是业务逻辑之间的协作,一个是线程终止逻辑的协作。终止逻辑在下面漏洞分析部分详细阐述,这里只关注业务逻辑的协作。
下面是最具代表性的第三次作业的时时序图:
上面是业务逻辑线程协作图,为了时序图的简洁,笔者将两个相邻线程之间的共享对象省略,并以异步消息的方式表示使用共享对象通信。
SOLID分析
[S] Single Responsibility Principle (单一功能原则)
电梯系列代码基本上符合单一功能原则,每个类完成单一的功能。线程类仅仅涉及线程运行的逻辑,对于电梯类,笔者还将电梯的控制与运行分离;共享对象类有两个功能,一是数据共享,二是线程间状态通信,但是笔者封装了SharedObject
类进行状态通信,使用继承机制将两个功能的实现分开。
[o] Open Close Principle (开闭原则)
很遗憾本系列作业的设计基本上不符合开闭原则。笔者认为设计出符合开闭原则的代码需要有一个全局视野,即能够预见以后会出现什么样的需求。在此基础上,设计者便能够对系统的某些部分进行合理适当地抽象,对某些结构进行优化。
在第一次作业的时候,已经为后续算法的加入预留了接口,在添加调度算法的时候是直接在源代码中修改,所以不算应用了开闭原则。如果要符合开闭原则,可以实现一个调度算法接口,让调度算法实现此接口,这样便能够保证开闭原则。
[L] Liskov Substitution Principle(里氏替换原则)
里氏替换原则认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”。由于本次作业中没有大量使用继承机制,所以无所谓符合里氏替换原则。想来如果事先考虑了开闭原则,那么便能够用里氏替换原则来衡量开闭原则的实现质量了。
[I] Interface Segregation Principle(接口隔离原则)
接口隔离原则认为“多个特定客户端接口要好于一个宽泛用途的接口”。由于本次作业中没有大量使用继承机制,所以也无所谓符合接口隔离原则。
[D] Dependency Inversion Principle(依赖反转原则)
依赖反转原则认为一个方法应该遵从“依赖于抽象而不是一个实例” 。本次作业完全不符合依赖反转原则。可以看到,代码中类间调用关系比较复杂,而且都是直接调用,中间并没有接口之类的抽象层次。这是因为在最初设计的时候便没有考虑到提升抽象层次。
想来本次作业中可抽象的部分还有很多,例如共享对象的模型、线程终止时状态通信的机制等,这些都是可以再优化的点。
总结
由于在设计过程中没有考虑到设计原则,所以本次作业在SOLID原则方面还有很大的欠缺,定痛改前非!
三、漏洞分析
三次作业的强测均通过,没有发现Bug(但是不代表没有)。下面是笔者在编写代码过程中遇到的一些小问题。
线程间状态通信
在编写多线程程序的过程中很重要的一个问题便是怎样结束程序的执行。在电梯模拟运行的情境中,共有三个线程(实际上在第三次作业中存在四个线程,将调度器分离成 两个线程,一个用于队请求进行换乘寻路,一个用于向电梯分发指令;三个电梯算作一类线程),用户输入线程在检测到输入结束之后终止,调度器线程在检测到用户输入线程结束之后随即终止,电梯线程需要执行完所有调度器分配的指令并且调度器线程已经结束才可以终止。
从上面的分析可以看出,此情景下普遍存在一个线程依赖另另一个线程结束而结束的需求,所以笔者封装了SharedObject
类,只需要让所有的共享对象继承这个类,便能够完成线程间的状态通信。其代码如下:
public class SharedObject {private boolean ioStreamState;public SharedObject() {this(true);}/*** The initial state of {@code ioStreamState} is true.* @param ioStreamState true means the thread is still alive.*/private SharedObject(boolean state) {ioStreamState = state;}/*** Only one of two connecting thread should set {@code ioStreamState} to* update its statement under such scenario.* @param ioStreamState open mark of the IO stream.*/public synchronized void setIoStreamState(boolean ioStreamState) {this.ioStreamState = ioStreamState;notifyAll();}public synchronized boolean ioStreamOpen() {return ioStreamState;}
}
以模拟用户输入线程和调度器线程之间的通信为例,在线程结束之前,ioStreamState
为true
,表示模拟用户输入线程尚未结束。在模拟用户输入线程结束之前,将ioStreamState
设置为false
,表示线程已经结束,并通知调度器。调度器发现模拟用户输入线程结束后随即终止。
在最终确定下这个方案之前笔者走了些弯路。
最开始笔者使用Thread.getState()
方法来判断线程是否结束,于是在线程结束之前便只需要通知监听线程即可,而不需要反转标志位。考虑下面的情况,\(A\)通知\(B\)自己即将终止,然后\(A\)线程放弃CPU;\(B\)线程开始运行,\(B\)调用A.getState()
方法来判断线程\(A\)是否结束;显然此时线程\(A\)并没有结束,那么线程\(B\)重新进入等待,之后再也没有线程唤醒线程\(B\),于是造成程序无法终止。
后来笔者查看Thread.getState()
方法源码,源码中也很清楚的注释了本方法用于监控系统状态而不是线程同步:
/*** ...* This method is designed for use in monitoring of the system state,* not for synchronization control.* ...*/
public State getState() {...
}
线程间数据共享
生产者消费者
本次作业中共享数据大量采用了生产者消费者模型。在踩了很多坑之后发现,将线程同步的操作放在共享对象中是最省心的办法,也就是把synchronized写在共享对象中。
提高并发度
抱着提高并发度的想法,笔者考虑将共享对象拆分开。以电梯和调度器之间的共享对象为例,将每个楼层作为一个小共享对象。调度器添加请求和电梯获取请求时只需要取得特定楼层的锁即可,而大多数情况下二者不会同时需要获取一个楼层的锁,于是并发度便有所提高。
这样的想法对于第二次作业仍然是可行的。但是到了第三次作业,只获取局部的数据已经无法满足调度器寻路功能的需求了,调度器需要一个全聚的视野,而访问全局数据需要添加一个共享对象全局的锁,于是笔者之前设计的拆分共享对象来提高并发度便无用武之地了。如果不添加这个锁便有可能造成线程不终止。
有趣的是刚开始笔者没有注意到这个问题,直接提交评测结果没有发现错误。这也警示了多线程的Bug很多时候很难检测,因为Bug复现往往要求在特定的时间节点触发一定的事件。所以在编写多线程程序时工程化和纪律性非常重要。
其实仔细思考一下会发现,调度器和电梯大多数时间都是处在Wait状态,争夺锁的机会本来就不多,所以这种提高并发度的优化也是无关紧要的。
四、心得体会
经过上面的思考,笔者总结了下面的几条体会:
- 涉及到多线程程序共享对象的处理时,最好将线程同步的操作写入共享对象中。于是在外部看来,对共享对象的读取是一个阻塞式读取,能够大大简化读者和写者的逻辑。
- 测试往往不能检测出多线程程序的所有错误,在编写代码时熟悉各种多线程设计模式很重要,而且在优化代码的时候需要很小心,防止因小失大。
- 将SOLID原则应用到设计阶段中去,会大大提升代码质量。细致划分代码的功能,让一个类专注于某一个功能;整体分析需求,提炼代码功能和结构上的共同点,提升代码的抽象层次,进而提高代码扩展性。
转载于:https://www.cnblogs.com/javadrinker/p/10763634.html
面向对象课程第二单元作业总结相关推荐
- BUAA-OO 第二单元作业“电梯调度”总结与思考
一.需求分析 利用java线程的相关知识实现 1)单部多线程傻瓜调度(FAFS)电梯 2)单部多线程可捎带调度(ALS)电梯 3)多部多线程智能(SS)调度电梯 二.思路分析 1.基于度量的程序结构分 ...
- 【OO学习】OO第二单元作业总结
OO第二单元作业总结 在第二单元作业中,我们通过多线程的手段实现了电梯调度,前两次作业是单电梯调度,第三次作业是多电梯调度.这个单元中的性能分要求是完成所有请求的时间最短,因此在简单实现电梯调度的基础 ...
- BUAA_OO_第二单元作业总结
程序设计策略 第一次作业 第一次作业实现的是FAFS式傻瓜调度电梯,由于是第一次接触多线程,难度不是很大.在我的设计里,我借鉴了生产者消费者模式,设计了一个输入线程和一个电梯线程,控制器的设计使用了单 ...
- OO第二单元作业小结
总结性博客作业 第一次作业 (1)从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略. 第一次作业为单电梯傻瓜调度,可以采用生产者--消费者模型,是一个有一个生产者(标准输入电梯请求),一 ...
- OO第二单元作业分析
前言 这一单元关于线程安全的作业结束了,在助教提供的接口的帮助以及老师提供的设计模型的指导下,这三次作业还是相对轻松地完成了,中间也没有出现什么bug,可能就是因为简单的逻辑不容易出错吧,可惜两次都由 ...
- BUAA OO第二单元作业总结
一.作业设计策略 (一)第一次作业设计方案 模型:生产者消费者模型 两个线程:输入线程(生产者).电梯线程(消费者) 共享对象:请求队列 退出模式:输入线程读到null,退出run,并将null传入请 ...
- 软件工程课程第二次作业
项目 内容 这个作业属于哪个课程 课程地址 这个作业的要求在哪里 作业要求 我在这个课程的目标是 学习软件开发,软件测试以及团队工作.大型项目开发 这个作业在哪个具体方面帮助我实现目标 第一次作业帮助 ...
- 第二单元作业——电梯模拟总结
一.设计策略 1. 单部先来先服务电梯 第一次作业采用了最基本的生产者-消费者模型,电梯请求是模型中的商品,将控制器作为存储请求的仓库,主线程作为生产者向仓库存放请求,电梯作为消费者从仓库取出请求并处 ...
- OO第二单元作业总结
一:设计策略 第一次作业:第一次是单电梯傻瓜调度策略,因此我把调度器当作共享资源对象,有一个put和一个get方法,因为只有一个电梯,并且单次取出和投放一个请求,因此只需要同步控制一下这两个方法是互斥 ...
最新文章
- eeglab中文教程系列(15)-绘制独立成分ERP贡献
- 中外教育专家“支招”中国学生如何接轨国际教育
- 453. Minimum Moves to Equal Array Elements (python)
- qt4 与php 通讯,Qt与外部程序通信问题
- python shelve模块_说说 Python 的 shelve 模块
- [你必须知道的.NET]第十四回:认识IL代码---从开始到现在
- Go语言的interface
- 吴恩达深度学习神经网络基础编程作业Python Basics with Numpy
- sql联接查询_SQL联接
- python爬虫线上编辑_Python 爬虫入门《中》
- AlterID.exe 报错问题
- java:换行符“\n”
- StrngBuffer可变字符串
- linux下mysql5.6版本初始化数据库密码
- web service在java_java web Service的使用
- JAVA实现本月国际国内节日
- WorkLess可量化的团队协作软件源码下载
- java命令行调用格式工厂转码
- 7月16日周二晚上,陈勇,【敏捷网络课堂第六期】【免费】敏捷开发早期估算...
- 2017-11-06 日语编程语言抚子 - 第三版特色初探