- HW1
- 思路
- 程序结构
- UML分析
- Class Diagram
- Sequence Diagram
- 代码规模分析
- 复杂度分析
- UML分析
- HW2
- 思路
- 程序结构
- UML分析
- Class Diagram
- Sequence Diagram
- 代码规模分析
- 复杂度分析
- UML分析
- HW3
- 思路
- 程序结构
- UML分析
- Class Diagram
- Sequence Diagram
- 代码规模分析
- 复杂度分析
- UML分析
- 总结
- Test n Bugs
- 测试方法
- 测试情况
- SOLID原则
- SRP
- OCP
- LSP
- ISP
- DIP
- 心得体会
- 线程安全
- 设计原则
- Test n Bugs
HW1
思路
本次作业,需要完成的任务为单部多线程可捎带电梯的模拟。
在IO处理中,本次作业使用官方包,不需要进行格式检查。
电梯系统可以采用任意的调度策略,基于LOOK算法,本次作业中的调度策略思路为:
如果电梯中有乘客,电梯按照原始方向继续运行,捎带中途所有乘客;
如果电梯中没有乘客,电梯运行到当前有请求的最近楼层或停止运行等待请求;
当电梯所移动的方向上不再有请求时立即改变运行方向。
本次作业采用生产者消费者模式:
Scheduler
类维护了共享的请求队列,以及volatile
类型的end flag
;
Input
类为生产者,读入Passenger
请求,加入请求队列;
Elevator
类为消费者,取出Passenger
请求,加入电梯内部的Passenger
队列。
程序结构
分析工具:UML,MetricsReloaded,Statistic,DesigniteJava
UML分析
Class Diagram
Passenger
类封装了以官方包中PersonRequest
为基础的一些新操作,方便之后的迭代开发。
Scheduler
类为共享对象,共享请求队列与结束标志变量,有基于调度策略的methods
。
Elevator
类作为消费者,包含描述电梯状态的各种变量,封装了电梯的基本运行、调度运行methods
。其中维护了电梯内Passenger
的Set
,使用其有序性方便电梯调度。
Input
类作为生产者,调用官方输入包读入请求。
在Main
中,创建了共享Scheduler
对象的一个生产者线程和一个消费者线程。
Sequence Diagram
代码规模分析
Total LOC analyzed: 295
Number of classes: 5
Number of methods: 30
属性和方法规模见Class Diagram
复杂度分析
其中OCavg代表类的方法的平均循环复杂度,WMC代表类的方法的总循环复杂度。
其中Essential Complexity (ev(G)、Module Design Complexity (iv(G))、Cyclomatic Complexity (v(G)。
总体上代码复杂度低。Elevator
类中的run
方法进行了结束判断以及等待请求判断。这里的等待请求处理利用了阻塞容器的方法,增加了一些判断,造成了复杂度高。实际上到后边的作业发现还是需要自己扩展取请求方法。
Type Name | Method Name | Implementation Smell | Cause of the Smell |
---|---|---|---|
Elevator | updateState | Complex Conditional | The conditional expression ((boardList.first().getToFloor() > currentFloor) && (currentState == -1)) || ((boardList.last().getToFloor() < currentFloor) && (currentState == 1)) is complex. |
updateState
这一method
中判断电梯是否需要转换方向这一判断条件复杂。
HW2
思路
本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。另外,相比第一次作业,增加了电梯的载客量限制。
思路和HW1基本相同,沿用HW1的架构。增加了ElevatorControl
类,用于创建多部电梯。
程序结构
UML分析
Class Diagram
ElevatorControl
类根据输入的电梯数创建多部电梯。
Elevator
类中增加了capacity
属性,只需在进入电梯的boarding
方法中加入容量限制即可。
其他类同HW1。
Sequence Diagram
代码规模分析
Total LOC analyzed: 353
Number of classes: 6
Number of methods: 33
属性和方法规模见Class Diagram
复杂度分析
整体在HW1基础上修改不多。
Elevator
类中的run
方法进行了结束判断,当电梯内无人且输入结束且请求队列为空时结束,这里复杂度高。但是相比HW1,封装了取出请求的调度方法,复杂度有所下降。
当电梯运行到每一层,都会调用Scheduler
类中的boarding
方法进行捎带。此方法中遍历请求队列加入可稍带对象,此时复杂度高,在后续的作业中使用map
容器,以floor
为key
,可以简化遍历操作。
Type Name | Method Name | Implementation Smell | Cause of the Smell |
---|---|---|---|
Elevator | updateState | Complex Conditional | The conditional expression ((boardList.first().getToFloor() > currentFloor) && (currentState == -1)) || ((boardList.last().getToFloor() < currentFloor) && (currentState == 1)) is complex. |
同HW1。
HW3
思路
本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。相比上一次作业,增加了实时增一部可以使用的电梯的功能,并且本次多部电梯的可停靠楼层,运行时间,最大载客量为不同的固定值。
沿用上一次作业的架构,另外:
将请求以
queue
修改为请求map
,以FromFloor
为key
,简化调度相关的methods
;由于存在中转乘客,
Elevator
类既是生产者,又是消费者。中转乘客在第一程结束后出电梯,产生第二程新请求加入请求队列;对于中转的处理:
根据电梯的可达楼层可发现中转乘客仅需中转一程;
不预先规划路线,根据实时电梯运行情况进行捎带(即不更改捎带规则),之后随机选择中转站避免电梯负荷不平均;
设置
passenger
的type
、directWay
、nextRoute
属性描述passenger的中转信息。属性使用one-hot
编码方便计算。修改结束标志。设置
cnt
记录目前请求数量,中转乘客出电梯时不减cnt
(中转乘客的第二程一定为直达类型);封装官方输出包为线程安全包。
程序结构
UML分析
Class Diagram
Scheduler
类为共享对象,共享请求请求hashmap
、Input
线程结束标志变量、Elevator
线程结束标志变量。增加types
属性用于调度中的捎带方法。
Elevator
类作为生产者和消费者。
Input
类作为生产者,调用官方输入包读入请求。
ElevatorControl
类增加实时添加一部电梯方法。
其他同HW2。
Sequence Diagram
代码规模分析
Total LOC analyzed: 532
Number of classes: 7
Number of methods: 48
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
SafeOutput | println | 3 | 1 | 1 |
Scheduler | Scheduler | 9 | 2 | 0 |
Scheduler | setElevatorEnd | 4 | 1 | 0 |
Scheduler | getEnd | 3 | 1 | 0 |
Scheduler | setEnd | 4 | 1 | 0 |
Scheduler | updateTypes | 13 | 3 | 2 |
Scheduler | biType | 11 | 3 | 1 |
Scheduler | add | 7 | 1 | 2 |
Scheduler | boarding | 29 | 8 | 3 |
Scheduler | findNearest | 23 | 5 | 3 |
Scheduler | toString | 3 | 1 | 0 |
Scheduler | isEmpty | 3 | 1 | 0 |
Scheduler | isEmpty | 8 | 3 | 2 |
ElevatorControl | ElevatorControl | 7 | 1 | 1 |
ElevatorControl | createElevator | 19 | 4 | 3 |
ElevatorControl | runDefault3Elevators | 6 | 2 | 0 |
ElevatorControl | addnRun | 5 | 1 | 2 |
Elevator | Elevator | 14 | 1 | 8 |
Elevator | getnickname | 3 | 1 | 0 |
Elevator | toString | 3 | 1 | 0 |
Elevator | printState | 8 | 2 | 2 |
Elevator | printState | 8 | 2 | 3 |
Elevator | aMove | 12 | 3 | 0 |
Elevator | sleep4awhile | 8 | 1 | 1 |
Elevator | rush | 17 | 5 | 1 |
Elevator | in | 14 | 4 | 1 |
Elevator | out | 19 | 5 | 0 |
Elevator | update | 14 | 3 | 0 |
Elevator | updateState | 18 | 5 | 0 |
Elevator | run | 20 | 4 | 0 |
MainClass | main | 9 | 1 | 1 |
Input | Input | 5 | 1 | 2 |
Input | run | 26 | 5 | 0 |
Passenger | Passenger | 14 | 3 | 4 |
Passenger | getType | 3 | 1 | 0 |
Passenger | isdirect | 3 | 1 | 2 |
Passenger | isinrange | 3 | 1 | 3 |
Passenger | addTransit | 12 | 3 | 3 |
Passenger | splitRoute | 6 | 1 | 3 |
Passenger | getDirectWays | 3 | 1 | 0 |
Passenger | getNextRoute | 3 | 1 | 0 |
Passenger | getFromFloor | 3 | 1 | 0 |
Passenger | getPersonId | 3 | 1 | 0 |
Passenger | getToFloor | 3 | 1 | 0 |
Passenger | equals | 10 | 3 | 1 |
Passenger | hashCode | 3 | 1 | 0 |
Passenger | compareTo | 9 | 3 | 1 |
Passenger | toString | 3 | 1 | 0 |
复杂度分析
飘红情况同HW2,另外,由于在Scheduler
类的增加封装了调度的方法,Elevator
类的复杂度相比HW2稍有下降。
Type Name | Method Name | Implementation Smell | Cause of the Smell |
---|---|---|---|
Elevator | updateState | Complex Conditional | The conditional expression ((boardList.first().getToFloor() > currentFloor) && (currentState == -1)) || ((boardList.last().getToFloor() < currentFloor) && (currentState == 1)) is complex. |
Input | run | Long Statement | The length of the statement "scheduler.add(new Passenger(((PersonRequest)request).getPersonId()'((PersonRequest)request).getFromFloor()'((PersonRequest)request).getToFloor()`1)'1);" is 151. |
Scheduler | isEmpty | Complex Conditional | The conditional expression (!storageTakers.get(key).isEmpty()) && (floors.contains(key)) && ((biType(key) & (tp)) != 0) is complex.isEmpty |
第一条同HW2。
中转乘客结束第一程后会增加新的第二程请求,为了重用Passenger
的构造函数,更改参数列表后,对于输入中的创建请求语句长度过长。这里可以override
。
在调度策略中,当电梯为空时,将寻找最近请求楼层,此调度方法中就包含了复杂判断:此楼层存在请求且此楼层为可达楼层且此楼层中请求可稍带。
总结
Test n Bugs
测试方法
本单元的测试方法和上一单元有所不同,设置断点的方法往往不能发现线程安全问题,所以主要采用System.err.println
通过输出信息进行debug
。
对于一个bug
,通过批量随机测试+重复测试先判断此bug
为线程安全性bug
或非线程安全性bug
。非线程安全性bug
按照以往经验定位修改即可;由于线程安全问题的不可复现性,通过随机测试发现总结错误特点,将涉及共享资源部分逐步检查是否存在不安全问题。
- 基础性测试
构造所有可能出现的进出楼层电梯搭配进行基础性测试。
- 自动化测试
使用bash,python package subprocess、random、signal
进行随机测试用例生成、合法检查。
生成测试用例并结合重定向运行代码;
with subprocess.Popen([r'java', r'-jar', r'Elevators.jar'], stdin=subprocess.PIPE, text=True, stdout=open(r'stdout.txt', "w"), stderr=open(r'stderr.txt', "w") ) as subpcs: covernRun(subpcs, 'test.txt')
按照指导书中的正确性要求进行检查;
保存结果到文件。
测试情况
- 互测中主要采用自动化测试的办法。在第一次作业中,hack到电梯层数的bug(floor=0)。
- 在第三次作业中,被hack到关于结束标志设置的bug,当所有乘客请求均满足的一段时间后以增加电梯请求为输入结束,这时我的程序无法结束。是因为我在调度算法的“当电梯为空则寻找最近请求”这一方法中的结束标志设置不完全。这里出现问题也因为盲信自动随机评测,没有手动构造有针对性的覆盖性测试用例。比如关于程序的结束是一个重要问题点,对于这一点应进行重点测试。
- 在第三次作业中room内大多数同学都hack到了bug,但是我跑的自动评测机一直没有hack成功,后来总结发现手动设计测试用例的重要性。
SOLID原则
设计原则:关于设计的整体要求和约束,通过满足设计原则来获得好的设计质量。
设计原则是对OO设计思维的整体要求。
SRP
Single Responsibility Principle每个类或方法都只有一个明确的职责,所以类所管理的数据应“聚焦”。
Elevator
类管理电梯的状态,实现了电梯简单的开关门,上下人,上下运行的方法。Scheduler
类负责调度的方法,职责划分明确,互不影响。
OCP
Open Close Principle无需修改已有实现,而是通过扩展来增加新功能。
这次电梯的设计可扩展性较好,三次作业均改动不多,沿用架构。总体架构基于生产者消费者模式。
Scheduler
类维护共享资源,根据调度策略可扩展相关调度方法;结束标志也在三次作业中体现了扩展性。
Input
类作为生产者,调用官方输入包读入请求,当输入请求类别增加时,Passenger
类相应扩展了其构造函数。
Elevator
类作为生产者和消费者,当电梯增加新维度的约束时可在此类增加属性和方法,电梯参数在构造方法时传入,方便新增电梯。
LSP
Liskov Substitution Principle任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误。
本次作业中Passenger
类封装了官方包中PersonRequest
,相当于继承了此类,并实现了Comparator
接口,用于建立电梯内乘客的有序set
。
ISP
Interface Segregation Principle通过接口来建立行为抽象层次具有更好的灵活性。
本次作业中没有使用接口。
DIP
Dependency Inversion Principle通过接口来建立行为抽象层次具有更好的灵活性。
输入和输出模块互不依赖,各个电梯互相独立。
但是对于调度器和电梯的通信应通过一个消息类抽象,而不是具体的容器。
心得体会
线程安全
在HW1的自动测试中就发现了线程安全问题的不可复现性,因为习惯于断点定位的debug pattern,第一次main对此就有些措手不及,但是随着后边几次作业测试中陆续遇到了线程安全问题进行debug,发现多线程的安全性问题从测试结果中就可大致定位,并且代码中对于共享资源的维护与访问一共也不多,逐部排查总能发现(go off my loop _)。另外,线程安全问题也在很大程度上依赖于手动设计魔鬼测试去发现,在这一点上本次作业做得还不够(比如自己写着写着第三次作业发现结束信号设置的有问题-> 我是怎么通过第二次作业的 -> 第二次作业中没有构造这种测试情况用例进行测试)。
System.err.println
真的很好用。
我遇到的线程安全问题有:
电梯在“空时寻找最近请求”中轮询。后修改了进入寻找的条件以及增加wait。
运行不结束。后修改了结束标志,通过输入请求结束标志、电梯为空、请求队列为空、电梯线程结束标志协同控制。
当无输入请求时,电梯在“空时寻找最近请求”中死锁等待。后修改了进入等待的条件。
设计原则
多线程部分总体设计比较简单,在实现安全性时只需要维护共享资源的安全性。
我认为要遵守OCP原则,SRP原则是很重要的一点。在第一次设计时就将职责合理划分,输入、调度、电梯,于是可扩展性就很好。