OO 第二单元总结
- OO 第二单元总结
- 程序分析
- 第一次作业
- 基于度量的程序结构分析
- UML 类图
- 多线程协同分析
- 设计原则检查
- 第二次作业
- 基于度量的程序结构分析
- UML 类图
- 多线程协同分析
- 设计原则检查
- 第三次作业
- 基于度量的程序结构分析
- UML 类图
- 多线程协同分析
- 功能与性能设计平衡分析
- 设计原则检查
- 总结
- 基于度量的程序结构分析
- UML 类图
- 多线程协同分析
- 设计原则检查
- 第一次作业
- Bug 分析
- 被公测和互测发现的 bug
- 概况
- 分析
- 发现别人程序 bug 所用的策略
- 测试策略
- 发现线程安全问题的策略
- 与第一单元的差异之处
- 被公测和互测发现的 bug
- 心得体会
- 线程安全
- 设计原则
- 对对象新的认识
- 参考文献
- 程序分析
程序分析
与第一单元类似,由于第一次作业有 70+ 个方法,第二次和第三次作业有 150+ 个方法,所以在基于度量的程序结构分析部分,只能保留至少有一种度量高于一定阈值的类或方法。同时,如果前面作业中较为复杂的方法在后面作业中没有修改,就省略重复的对复杂度的解释。
度量及其阈值对应的关系如下:
度量 | 意义 | 英文名称 | 阈值 | 备注 |
---|---|---|---|---|
ev(G) | 循环复杂度 | Cyclomatic Complexity | 3 | MetricsReloaded 默认阈值 |
iv(G) | 设计复杂度 | Design Complexity | 8 | MetricsReloaded 默认阈值 |
v(G) | 本质循环复杂度 | Essential Cyclomatic Complexity | 10 | MetricsReloaded 默认阈值 |
OCavg | 平均方法复杂度 | Average Operation Complexity | 3 | MetricsReloaded 默认阈值 |
WMC | 加权方法复杂度 | Weighted Method Complexity | 30 | MetricsReloaded 默认阈值 |
LOC | 方法规模 | Lines of Code | 20 | 含注释但不含空行,来源[1] |
BRANCH | 方法分支数目 | Number of Branch Statements | 5 | |
CONTROL | 方法控制语句数目 | Number of Control Statements | 5 | |
LOC | 类总代码规模 | Lines of Code | 200 | 含注释但不含空行;来源[2] |
CSA | 类属性数目 | Class size (attributes) | 7 | |
CSO | 类方法数目 | Class size (operations) | 20 |
UML 类图和 UML 时序图可能较大,可以打开新标签页得到完整图片。UML 时序图使用未注册的 StarUML 3.0 绘制,所以会有水印,但是不影响查看。
第一次作业
基于度量的程序结构分析
method | ev(G) | iv(G) | v(G) | BRANCH | CONTROL | LOC |
---|---|---|---|---|---|---|
com.oocourse.LookScheduler.RequestSet.findNearestIndexConsideringNulls(Integer,Integer,Integer) | 7.0 | 1.0 | 8.0 | 0.0 | 6.0 | 25.0 |
com.oocourse.LookScheduler.oppositeDirection(DirectionPreference) | 4.0 | 2.0 | 4.0 | 0.0 | 4.0 | 10.0 |
com.oocourse.LookScheduler.RequestSet.hasRequestHere(TreeMap>,Predicate) | 4.0 | 3.0 | 5.0 | 0.0 | 3.0 | 19.0 |
com.oocourse.LookScheduler.RequestSet.findNearestIndex() | 4.0 | 4.0 | 5.0 | 0.0 | 3.0 | 19.0 |
com.oocourse.LookScheduler.probablyTurnAround() | 2.0 | 3.0 | 5.0 | 0.0 | 7.0 | 21.0 |
com.oocourse.LookScheduler.schedule(ScheduledElevator) | 1.0 | 7.0 | 9.0 | 0.0 | 7.0 | 37.0 |
com.oocourse.Main.main(String[]) | 1.0 | 3.0 | 4.0 | 0.0 | 2.0 | 21.0 |
Total | 96.0 | 122.0 | 151.0 | 0.0 | 78.0 | 573.0 |
Average | 1.352112676056338 | 1.7183098591549295 | 2.1267605633802815 | 0.0 | 1.0985915492957747 | 8.070422535211268 |
方法 | 复杂度较高原因 |
---|---|
com.oocourse.LookScheduler.RequestSet.findNearestIndexConsideringNulls(Integer,Integer,Integer) | 需要在一个方法内完成三个 Integer 的 unboxing 和处理 null 的情况,方法的本质决定了只能在一个方法中考虑很多特殊情况 |
com.oocourse.LookScheduler.oppositeDirection(DirectionPreference) | 方法中只有一个 switch 语句,其中因为 enum DirectionPreference 的取值而有三个分支,只是把 DirectionPreference 的方向变成相反的 |
com.oocourse.LookScheduler.RequestSet.hasRequestHere(TreeMap,Predicate) | 需要检查对应的楼层是否有请求,但有无请求用是否为 null 表示;然后用 enhanced for 遍历每个请求,再用 predicate 判断条件;两个控制语句增大了 ev(G) |
com.oocourse.LookScheduler.RequestSet.findNearestIndex() | 需要判断电梯是否为满或者为空,这样就会有三种情况,因此增大了 ev(G) |
com.oocourse.LookScheduler.probablyTurnAround() | 方法内有一个 switch 语句,因为 DirectionPreference 的取值而有三种情况,因此相关复杂度增大 |
com.oocourse.LookScheduler.schedule(ScheduledElevator) | 把 LOOK 调度器的大致细节囊括在一个方法中;因为某些比较小的逻辑(比如根据 DirectionPreference 判断向上走还是向下走)没有拆出方法,所以 v(G) 增大 |
com.oocourse.Main.main(String[]) | 线程创建和管理的逻辑略微复杂,因此代码行数略微超标 |
class | CSA | CSO | LOC | OCavg | WMC |
---|---|---|---|---|---|
com.oocourse.LookScheduler.RequestSet | 6.0 | 25.0 | 239.0 | 1.962962962962963 | 53.0 |
com.oocourse.LookScheduler | 6.0 | 6.0 | 105.0 | 3.5714285714285716 | 25.0 |
Total | 32.0 | 63.0 | 622.0 | 132.0 | |
Average | 4.0 | 7.875 | 77.75 | 1.8591549295774648 | 16.5 |
类 | 复杂度较高原因 |
---|---|
com.oocourse.LookScheduler.RequestSet | RequestSet 是管理请求的中心类,同时作为调度器控制电梯的中介;内部采用请求队列和 TreeSet 管理请求,因此操作较为繁琐;内部有一些方法并没有用上,只是作为实现其它调度器的备用方法;因此方法个数和方法复杂度都较大 |
com.oocourse.ScheduledElevator | 电梯本身操作较多,同时为了时序关系实现了一些 private 方法,因此方法个数较多 |
UML 类图
优点:
- 各个类职责比较清晰,电梯、调度器、请求处理器都有自己的类。
- 类和类之间的依赖关系大体上通过抽象表现出来。
- 各个类耦合度较低、替换调度器较容易。
缺点:
RequestSet
作为内部类职能太大,应该考虑拆出。BaseElevator
并没有通过抽象实现,虽然只有一部电梯。- 楼层转换用到的相关函数等与具体对象无关的辅助函数,应该放入单独的类中。
多线程协同分析
这次作业是典型的流水线结构,由两个线程完成任务。首先 RequestReader
读取请求,然后交给 LookScheduler
执行。运送人的主要过程在电梯线程进行。
设计原则检查
原则 | 问题 |
---|---|
SRP | 符合。每个类都只有一种责任。 |
OCP | RequestSet 不符合。虽然它实际上是一个内部类,但是许多方法不应该被暴露在外面,违背了对修改闭合的原则。 |
LSP | 符合。能够继承的类都可以用子类替换,不影响程序正确性。 |
ISP | Pipe 接口不符合。实际上如果客户不需要批量接收或发送,不需要 Pipe::(send|recv)All 。 |
DIP | ScheduledElevator 不符合。调度器依赖该类的细节。 |
第二次作业
基于度量的程序结构分析
method | ev(G) | iv(G) | v(G) | BRANCH | CONTROL | LOC |
---|---|---|---|---|---|---|
com.oocourse.scheduler.RequestSet.findNearestIndexConsideringNulls(Integer,Integer,Integer) | 7.0 | 1.0 | 8.0 | 0.0 | 6.0 | 25.0 |
com.oocourse.scheduler.RequestSet.hasRequestHere(TreeMap,Predicate) | 4.0 | 3.0 | 5.0 | 0.0 | 3.0 | 18.0 |
com.oocourse.scheduler.RequestSet.findNearestIndex() | 4.0 | 4.0 | 5.0 | 0.0 | 3.0 | 19.0 |
com.oocourse.dispatcher.RelevantDistanceFeedbackRequestDispatcher.run() | 1.0 | 5.0 | 5.0 | 0.0 | 4.0 | 27.0 |
com.oocourse.dispatcher.BaseFeedbackRequestDispatcher.findMinAndDispatch(PersonRequest,int[]) | 3.0 | 2.0 | 8.0 | 0.0 | 10.0 | 29.0 |
com.oocourse.scheduler.LookScheduler.schedule(BaseScheduledElevator) | 1.0 | 12.0 | 13.0 | 0.0 | 10.0 | 55.0 |
com.oocourse.Main.main(String[]) | 1.0 | 5.0 | 6.0 | 0.0 | 4.0 | 39.0 |
Total | 154.0 | 200.0 | 234.0 | 0.0 | 105.0 | 923.0 |
Average | 1.2622950819672132 | 1.639344262295082 | 1.9180327868852458 | 0.0 | 0.860655737704918 | 7.565573770491803 |
方法 | 复杂度较高原因 |
---|---|
com.oocourse.scheduler.RequestSet.findNearestIndexConsideringNulls(Integer,Integer,Integer) | 同上次作业 |
com.oocourse.scheduler.RequestSet.hasRequestHere(TreeMap,Predicate) | 同上次作业(重构后位置改变) |
com.oocourse.scheduler.RequestSet.findNearestIndex() | 同上次作业(重构后位置改变) |
com.oocourse.dispatcher.RelevantDistanceFeedbackRequestDispatcher.run() | 获取反馈用的相关数值(电梯人数、运行方向)时把获取和分派的逻辑写到同一函数中,使得运行的主要方法代码行数较大 |
com.oocourse.dispatcher.BaseFeedbackRequestDispatcher.findMinAndDispatch(PersonRequest,int[]) | 在数组里寻找最小值采用了 enhanced for 语法,同时为只有一部电梯可以发射请求时做了优化,通过 switch 语句实现,因此控制语句数和代码行数较大 |
com.oocourse.scheduler.LookScheduler.schedule(BaseScheduledElevator) | 为了减少开门和关门时间,BaseScheduledElevator::reentrant(open|close)door 函数被创建出来,负责可以重入的开关门,从而让调度器逻辑表达得更加简洁;调度器代码大体上没有改动,但可重入开关门的引入增大了 iv(G)、v(G) 和方法规模 |
com.oocourse.Main.main(String[]) | 电梯个数增多使得 Main 方法的代码行数进一步增大,但是逻辑并没有较大变化 |
class | CSA | CSO | LOC | OCavg | WMC |
---|---|---|---|---|---|
com.oocourse.scheduler.RequestSet | 8.0 | 31.0 | 261.0 | 1.7878787878787878 | 59.0 |
com.oocourse.elevator.BaseScheduledElevator | 12.0 | 27.0 | 178.0 | 1.3571428571428572 | 38.0 |
com.oocourse.elevator.TypicalScheduledElevator | 11.0 | 6.0 | 34.0 | 1.0 | 7.0 |
com.oocourse.scheduler.LookScheduler | 3.0 | 2.0 | 76.0 | 5.333333333333333 | 16.0 |
Total | 63.0 | 109.0 | 1014.0 | 207.0 | |
Average | 3.7058823529411766 | 6.411764705882353 | 59.64705882352941 | 1.6967213114754098 | 12.176470588235293 |
类 | 复杂度较高原因 |
---|---|
com.oocourse.LookScheduler.RequestSet | 同上次作业(重构时位置改变) |
com.oocourse.elevator.BaseScheduledElevator | 把电梯类的共用属性和方法抽象出来,因此情况和上次作业的 ScheduledElevator 类类似;引入了可重入的开关门方法使 OCavg 进一步提高;同时引入了方便反馈的许多 AtomicInteger 属性,又使 CSA 进一步提高 |
com.oocourse.elevator.TypicalScheduledElevator | 实际上只是继承 abstract class BaseScheduledElevator 的类,并没有独特的属性;但是可能是与具体电梯相关的几个常数,使得 CSA 较高 |
com.oocourse.scheduler.LookScheduler | 新引入的可重入开关门函数,使得调度器的调度方法更加繁琐,因此使 OCavg 超标 |
UML 类图
优点:
- 除了
RequestReader
以外,其余的值得考虑的类依赖都已经改为依赖抽象。 - 各个类的职责明确、耦合程度较低。
- 请求分配器单独实现,从而可以实现基于反馈的、较灵活的请求分配策略。
- 辅助函数单独开类实现。
缺点:
- 电梯和
RequestSet
的职责较集中、方法数和规模较大,虽然这是由它们的用途决定的。 - 电梯要用到的加速模式的常数没有放在
Main
类中。
多线程协同分析
本次作业采取简化的 Worker Thread 设计模式。实际上采用的,是 Main
类作为主线程来创建、管理和等待其它线程的实现。请求分配器单独作为一个线程,$ n $ 个电梯每个电梯一个线程,不拆分请求。这样做的好处是各个线程和类职责清晰,缺点是资源占用会稍高。
设计原则检查
原则 | 问题 |
---|---|
SRP | RequestSet 不符合。它也包括了一些用于判断请求和楼层类型的辅助方法。 |
OCP | 符合。所有的类都是对扩展开放、对修改关闭的。 |
LSP | 符合。能够继承的类都可以用子类替换,不影响程序正确性。 |
ISP | Pipe 接口不符合。实际上如果客户不需要批量接收或发送,不需要 Pipe::(send|recv)All 。 |
DIP | 符合。所有的依赖都是依赖接口或抽象类。 |
第三次作业
基于度量的程序结构分析
method | ev(G) | iv(G) | v(G) | BRANCH | CONTROL | LOC |
---|---|---|---|---|---|---|
com.oocourse.scheduler.LookScheduler.scheduleImpl() | 4.0 | 14.0 | 17.0 | 1.0 | 13.0 | 55.0 |
com.oocourse.scheduler.VrContinuumScheduler.findTarget() | 11.0 | 4.0 | 16.0 | 0.0 | 15.0 | 50.0 |
com.oocourse.arbiter.FloydDecisionMaker.floydInitEdgeWeightImpl(int,int) | 1.0 | 8.0 | 12.0 | 0.0 | 5.0 | 49.0 |
com.oocourse.arbiter.BaseDecisionMaker.sendToFreestElevatorByType(PersonRequestTransaction,ElevatorType,int,int) | 1.0 | 5.0 | 6.0 | 0.0 | 5.0 | 42.0 |
com.oocourse.arbiter.FloydPathTweakingDecisionMaker.makeDecision(PersonRequestTransaction) | 2.0 | 10.0 | 11.0 | 0.0 | 5.0 | 39.0 |
com.oocourse.scheduler.VrContinuumScheduler.scheduleImpl() | 4.0 | 9.0 | 10.0 | 1.0 | 7.0 | 33.0 |
com.oocourse.arbiter.BaseArbiter.spawnAndAddElevator(ElevatorType,String,BaseScheduler,Channel,Channel) | 2.0 | 2.0 | 5.0 | 0.0 | 8.0 | 29.0 |
com.oocourse.scheduler.SstfScheduler.scheduleImpl() | 4.0 | 7.0 | 8.0 | 1.0 | 6.0 | 29.0 |
com.oocourse.arbiter.FloydDecisionMaker.floydInitImpl() | 1.0 | 8.0 | 10.0 | 0.0 | 9.0 | 26.0 |
com.oocourse.scheduler.RequestSet.findNearestIndexConsideringNulls(Integer,Integer,Integer) | 7.0 | 1.0 | 8.0 | 0.0 | 6.0 | 25.0 |
com.oocourse.channel.BulkChannel.recvAll() | 2.0 | 4.0 | 4.0 | 0.0 | 4.0 | 25.0 |
com.oocourse.scheduler.BaseScheduler.handleTransportRequestMessage(TransportRequestMessage) | 3.0 | 3.0 | 4.0 | 0.0 | 2.0 | 22.0 |
com.oocourse.elevator.BaseScheduledElevator.BaseScheduledElevator(String,BaseScheduler,Channel,Channel,int) | 1.0 | 1.0 | 1.0 | 0.0 | 0.0 | 21.0 |
com.oocourse.arbiter.FloydDecisionMaker.floydGetPathImpl(int,int) | 4.0 | 3.0 | 5.0 | 0.0 | 3.0 | 21.0 |
com.oocourse.arbiter.FloydDecisionMaker.floydAddDoorCostImpl(int,int) | 3.0 | 2.0 | 6.0 | 0.0 | 9.0 | 20.0 |
com.oocourse.arbiter.DesignateDecisionMaker.isAtomicForElevatorB(int,int) | 1.0 | 5.0 | 18.0 | 0.0 | 0.0 | 20.0 |
com.oocourse.scheduler.RequestSet.findNearestIndex() | 4.0 | 4.0 | 5.0 | 0.0 | 3.0 | 19.0 |
com.oocourse.scheduler.RequestSet.hasRequestHere(TreeMap>,Predicate) | 4.0 | 3.0 | 5.0 | 0.0 | 3.0 | 18.0 |
com.oocourse.arbiter.DesignateDecisionMaker.makeDecision(PersonRequestTransaction) | 4.0 | 3.0 | 4.0 | 0.0 | 3.0 | 18.0 |
com.oocourse.scheduler.VrContinuumScheduler.hasEligibleRequest(int) | 4.0 | 4.0 | 5.0 | 0.0 | 3.0 | 15.0 |
com.oocourse.arbiter.DesignateDecisionMaker.getFloorType(int) | 4.0 | 4.0 | 7.0 | 0.0 | 3.0 | 15.0 |
com.oocourse.requestreader.RequestReader.run() | 4.0 | 4.0 | 4.0 | 0.0 | 3.0 | 14.0 |
com.oocourse.arbiter.TransactionalArbiter.arbiterImpl() | 4.0 | 4.0 | 5.0 | 0.0 | 3.0 | 12.0 |
com.oocourse.arbiter.DesignateDecisionMaker.getNextStepCompositeImpl(int,int) | 4.0 | 4.0 | 4.0 | 0.0 | 3.0 | 12.0 |
Total | 377.0 | 488.0 | 601.0 | 3.0 | 256.0 | 2139.0 |
Average | 1.356115107913669 | 1.7553956834532374 | 2.161870503597122 | 0.01079136690647482 | 0.920863309352518 | 7.694244604316546 |
方法 | 复杂度较高原因 |
---|---|
com.oocourse.scheduler.LookScheduler.scheduleImpl() | 同上次作业 |
com.oocourse.scheduler.VrContinuumScheduler.findTarget() | 为了效率使用了数组上的操作,因此需要手动找寻数组的最值;同时因为 V(R) 算法[3]的特点,需要根据电梯方向找寻最值,使用了最没有技巧但不易出错的方法(两个 for 循环) |
com.oocourse.arbiter.FloydDecisionMaker.floyd(InitEdgeWeight|AddDoorCost)Impl (int,int) |
需要根据三种电梯的情况分类讨论,得出换乘对应的图结构的边权;为了避免代码中有非法行为,并没有使用反射,但三种电梯可以达到的楼层是静态变量,因此实际上同样的代码写了三遍,分别涉及 Type[ABC]Elevator |
com.oocourse.arbiter.BaseDecisionMaker.sendToFreestElevatorByType(PersonRequestTransaction,ElevatorType,int,int) | 涉及到在一个 Collection 中选择所有某种指标最小的元素,需要手动写;同时对只有一种这样的元素的情况,提供了针对性优化,两个 if 分支都较大 |
com.oocourse.arbiter.FloydPathTweakingDecisionMaker.makeDecision(PersonRequestTransaction) | 决策时有对路径进行微调从而试图平衡电梯使用的优化代码,都放在该方法中 |
com.oocourse.scheduler.(VrContinuumScheduler|SstfScheduler|LookScheduler) .scheduleImpl() |
一方面同上次作业;另一方面又增加了重入检测代码和方便实时切换调度器的代码,这部分代码依赖调度器的内部逻辑和异常机制,从而无法解耦,使得调度方法复杂度进一步增大 |
com.oocourse.arbiter.BaseArbiter.spawnAndAddElevator(ElevatorType,String,BaseScheduler,Channel,Channel) | 需要根据电梯类型作出判断,用到了对 Enum 的 Switch 语句,从而导致控制语句数和行数较大 |
com.oocourse.arbiter.FloydDecisionMaker.floydInitImpl() | Floyd 算法中多个三重循环导致控制语句数和行数较大 |
com.oocourse.scheduler.RequestSet.findNearestIndexConsideringNulls(Integer,Integer,Integer) | 同上次作业 |
com.oocourse.channel.BulkChannel.recvAll() | 增加了对处理线程被中断的清理(cleanup)代码,因此代码行数较大 |
com.oocourse.scheduler.BaseScheduler.handleTransportRequestMessage(TransportRequestMessage) | 逻辑较为简单,是把 TransportRequestMessage 转换成对应的 PersonRequest 对象,从而能够沿用第二次作业的 RequestSet 设计并少出 bug,但是某些能够一行写完的代码因为行数限制变成多行 |
com.oocourse.elevator.BaseScheduledElevator.BaseScheduledElevator(String,BaseScheduler,Channel,Channel,int) | 需要初始化的属性增多,因此代码行数略微超标 |
com.oocourse.arbiter.DesignateDecisionMaker.isAtomicForElevatorB(int,int) | 已经被弃置的代码;一开始实现的简单换乘拆分需要考虑很多特殊情况 |
com.oocourse.scheduler.RequestSet.findNearestIndex() | 同上次作业 |
com.oocourse.scheduler.RequestSet.hasRequestHere(TreeMap,Predicate) | 同上次作业 |
com.oocourse.arbiter.DesignateDecisionMaker.makeDecision(PersonRequestTransaction) | 已经被弃置的代码;一开始实现的简单换乘拆分需要考虑很多特殊情况,以及判断请求适合送往哪类电梯 |
com.oocourse.scheduler.VrContinuumScheduler.hasEligibleRequest(int) | 需要判断电梯是否为空或为满,因此 ev(G) 较高 |
com.oocourse.arbiter.DesignateDecisionMaker.getFloorType(int) | 需要在一个方法中判断三类楼层类型,因此 ev(G) 较高 |
com.oocourse.requestreader.RequestReader.run() | 需要分是人的请求、电梯请求或其它无效情况,因此 ev(G) 较高 |
com.oocourse.arbiter.TransactionalArbiter.arbiterImpl() | 需要判断是否再接收控制消息,以及是否结束程序,使用了两重循环,因此 ev(G) 较高 |
com.oocourse.arbiter.DesignateDecisionMaker.getNextStepCompositeImpl(int,int) | 已经被弃置的代码;一开始实现的简单换乘拆分需要考虑四种拆分情况 |
class | CSA | NOAC | LOC | OCavg | WMC |
---|---|---|---|---|---|
com.oocourse.elevator.TypicalScheduledElevator | 17.0 | 6.0 | 47.0 | 1.0 | 7.0 |
com.oocourse.elevator.BaseScheduledElevator | 12.0 | 29.0 | 200.0 | 1.4 | 42.0 |
com.oocourse.scheduler.VrContinuumScheduler | 9.0 | 7.0 | 162.0 | 5.25 | 42.0 |
com.oocourse.scheduler.RequestSet | 9.0 | 31.0 | 268.0 | 1.8181818181818181 | 60.0 |
com.oocourse.arbiter.TransactionalArbiter | 8.0 | 13.0 | 105.0 | 1.4285714285714286 | 20.0 |
com.oocourse.arbiter.FloydDecisionMaker | 5.0 | 9.0 | 174.0 | 3.8 | 38.0 |
com.oocourse.scheduler.LookScheduler | 5.0 | 4.0 | 83.0 | 4.2 | 21.0 |
com.oocourse.scheduler.SstfScheduler | 4.0 | 4.0 | 83.0 | 3.8 | 19.0 |
com.oocourse.arbiter.FloydPathTweakingDecisionMaker | 2.0 | 1.0 | 54.0 | 3.3333333333333335 | 10.0 |
Total | 212.0 | 235.0 | 2393.0 | 498.0 | |
Average | 5.0476190476190474 | 5.595238095238095 | 56.976190476190474 | 1.79136690647482 | 11.857142857142858 |
类 | 复杂度较大原因 |
---|---|
com.oocourse.elevator.TypicalScheduledElevator | 同上次作业 |
com.oocourse.elevator.BaseScheduledElevator | 同上次作业 |
com.oocourse.scheduler.(VrContinuum|Sstf|Look) Scheduler |
一方面同上次作业;另一方面是实时换调度器机制的引入使得类更复杂 |
com.oocourse.scheduler.RequestSet | 同上次作业 |
com.oocourse.arbiter.TransactionalArbiter | 使用访问者模式,需要处理许多控制消息,因此方法数较多 |
com.oocourse.arbiter.FloydDecisionMaker | Floyd 算法集中在某几个方法中,因此 OCavg 较高 |
com.oocourse.arbiter.FloydPathTweakingDecisionMaker | 路径调整算法在进行决策的方法中,因此 OCavg 较高 |
UML 类图
优点:
- 全面依赖抽象,从仲裁器到电梯调度器,从电梯运行参数到换乘策略,一切都建立在抽象的基础之上,大大降低了耦合程度。
- 通过专门实现的附件类,提高了对楼层、
PersonRequest
、输出等的处理效率,提高代码可维护性。 - 完善的继承架构使得实现从原来策略派生出的新策略非常容易,比如
ScanScheduler
继承LookScheduler
、FloydPathTweakingDecisionMaker
继承FloydDecisionMaker
。 - 全面采用
ControlMessage
类实现的控制消息机制,让仲裁器和调度器都能方便地通讯。 - 通过访问者模式实现了调度器对电梯的调度。
缺点:
BaseScheduledElevator
和RequestSet
仍然职责较多。- 为了减小重构代价,把
TransportRequestMessage
转换成对应的PersonRequest
对象,再把对应的对象放入RequestSet
,实际上对数据的表示不统一。 - 对楼层的表示不是很统一,
PersonRequest
类用楼层号,其余的类用楼层索引,有些多余的互相转换。 - 对三种电梯的表示,有时用它们对应的类,有时用
ElevatorType
这个Enum
。(当然有出于避免反射的考虑。)
多线程协同分析
本次作业采取较为完整的 Worker Thread 设计模式,有线程层级的概念。首先 Main
线程启动请求读取线程和仲裁器线程,仲裁器线程负责通过请求读取并做出反应,它一开始也负责创建初始的电梯线程。所有工作线程统一通过 BaseControlMessage
进行通信,仲裁器管理事务并分配任务给电梯,电梯负责调度。进程分工比较明确、耦合性较低。
功能与性能设计平衡分析
第三次作业实际上功能与性能设计做得比较折中,扩展性较强。
首先,有一个仲裁器线程可以保证调度结果的准确性,而没有仲裁器,统筹规划不好实现。纯靠电梯本身,技术难度可能比较高。想要变成仲裁器负责几乎每个电梯的微操,就需要换一个全知全能的仲裁器,这时需要电梯的调度器配合,写一个假人(dummy)调度器,只听仲裁器发送的 ControlMessage
,没有任何自己的主见。
其次,电梯与调度器结合起来也是一个折中的表现。虽然这样会使调度器与电梯之间的依赖增强,但是开一个线程也降低线程开销。同时,在一个线程中让调度器操作电梯,也可以消除相关的线程安全问题。实际上在强测中,开一个线程大约占 0.5s CPU 时间,把所有人的请求调度完大概需要 1s CPU 时间,所以还是值得的。这样也可以通过异常来控制执行流程,从而支持调度器的现场重调度(live reschedule)。
然后,通过模块化的调度器与决策器,可以让它们有自己的状态和内部实现,却分别不与电梯和仲裁器相互影响,同时方便继承以实现新的调度器或决策器。这样虽然损失一点性能,却获得了更强大的功能。
因此,这次作业既做到了较强的可扩展性,又不至于损失太多性能。
设计原则检查
原则 | 问题 |
---|---|
SRP | 符合。每个类都只有一种责任。 |
OCP | 符合。所有的类都是对扩展开放、对修改关闭的。 |
LSP | 符合。能够继承的类都可以用子类替换,不影响程序正确性。 |
ISP | Pipe 接口不符合。实际上如果客户不需要批量接收或发送,不需要 Pipe::(send|recv)All 。 |
DIP | BaseArbiter 不符合。为了方便让 BaseArbiter 直接依赖 PersonRequestTransaction ,但是定义了它对应的抽象类 BaseTransaction ;后来因为时间关系没有重构 BaseArbiter 。 |
总结
基于度量的程序结构分析
这一单元仍然延续了上一单元方法多但关键逻辑和 bug 集中的特点,间接地验证了二八定律这一经验定律。通过基于度量的程序结构分析,可以定位到消除 bug 的关键点并在测试中用心,从而更好地消除 bug。
UML 类图
这一单元的 UML 类图呈现出比较明显的迭代趋势。架构没有太大的改动,逻辑较集中、不好分散的类仍然较集中,同时利用继承和分包把每个类的职责明确。
多线程协同分析
这一单元使用线程专门性和组织程度较高,除了反馈机制出于性能和实时性利用 AtomicInteger
类来实现线程通信之外,线程之间统一使用共享队列进行通信。这一方面使思路比较清晰、减少线程同步 bug 出现的可能性,另一方面也简化了进程的互相操作。但是,对思路也有一定的束缚。第三次作业为了应对新增电梯、换乘等需求,让所有线程统一使用继承自 ControlMessage
的控制消息类进行通信,更进一步简化架构。
设计原则检查
实际上,为了能够写出可扩展、易维护的程序,需要自觉不自觉地跟着 SOLID 走。为了调整架构,每一类别的类都单独负责了一种职能。之后又定义了它们对应的接口(如果需要共用的方法就是抽象类)。然后为了方便调试、使系统结构清晰,又把每个类细化到一种职能,再让它们明确。之后才发现使用某个接口的类有时不一定用到该接口的所有方法,但是没拆接口,因此这一单元的作业 ISP 做得不太好。实际上这些步骤就已经是 SOLID 的实践知识了。
Bug 分析
被公测和互测发现的 bug
概况
这一单元中,只被互测发现一个 bug,问题出在计时不准上。
修改以前,Main
关键代码如下:
/* ... */
public class Main {
/* ... */
private static long epoch;
/* ... */
public static long getEpoch() {
return epoch;
}
/* ... */
private void initStartTimestamp() {
TimableOutput.initStartTimestamp();
epoch = System.currentTimeMillis();
}
/* ... */
public static void main(String[] args) {
initStartTimestamp();
/* ... */
}
}
ScheduledElevator
关键代码如下:
/* ... */
public class ScheduledElevator {
ScheduledElevator(/* ... */) {
/* ... */
lastPrintlnTimestamp = Main.getEpoch();
}
/* ... */
}
然后互测中爆出了电梯第一次动作过快的 bug。
分析
实际上是 System#currentTimeMillis
不保证时间戳递增而且粒度比 1ms 大引起的。
Main
类初始化的 epoch 不一定在 TimableOutput
类认为的初始时间戳之后。电梯第一次动作时,又是简单地在上一次 TimableOutput.println
后的时间戳(也就是 this.lastPrintlnTimestamp
上)加对应的间隔,因此睡眠结束时可能打印的时机太早,导致 WA。
更深入的分析需要看 Hotspot 在各个 OS 上的 OS 相关代码,不过在主流的 OS 上获取时间本来就是不准的。
发现别人程序 bug 所用的策略
测试策略
策略 | 适用范围 | 有效性 |
---|---|---|
随机生成样例 | 自测、互测 | 抓到新 bug 的概率与测试量大概呈对数规律;能够发现的 bug 大多是大礼包型 bug,一个 bug 暴露很多问题 |
把睡眠时间变成原来的 $ \frac{1}{10} $ | 自测、互测 | 比较有效,因为至少可以加速测试;也可以暴露计时的不准确性;可能的线程调度变多,容易激发同步问题 |
自动投放测试样例 | 自测、互测 | 自动测试基本上必备,因为手工投放没有准确性 |
手动出极限样例 | 互测 | 比较有效,常犯的 bug 比较集中 |
发现线程安全问题的策略
最明显的方法就是小黄鸭 debug 法。把线程的执行逻辑自己分析一遍,找出逻辑不严密的地方,就能容易发现线程安全问题。其次是针对部分线程,想它们的合作策略。这样能够比较容易发现死锁。最后是最朴素的打印法,当然可以打印到 System.err
。
与第一单元的差异之处
主要有两大方面:多线程带来的并行性与不确定性,以及有状态对象的测试难度。
第一单元实际上可以采取不可变对象,基本上能够完成任务。因为表达式本身不需要状态,它的存在是外界唯一关心的东西,那么就不需要状态。但是这一单元的电梯,生来(inherently)就是有状态的。如果按照楼建模,楼跟电梯的情况也差不多。有状态对象比较适合单元测试,不太适合黑盒测试。因为利用单元测试,状态的复杂度会累加;利用黑盒测试,状态的复杂度会累乘。
因此,不但要对多线程进行压力测试,也要对有状态对象进行状态(stateful)测试。
心得体会
线程安全
线程本身具有不确定性,线程安全是证出来的而不是测试出来的。微机的普及和摩尔定律的瓶颈使本来出现在大型机和中型机的技术进一步下放,这使得多线程技术开始普及。早在 20 世纪 60 年代,线程同步的方法就开始提出,它们也是靠证明确定线程安全的。之后一直是沿用这个思路。因此,使用一个线程安全的架构,比打补丁式地打出线程安全更重要。
设计原则
设计模式和设计原则不是白提出的,也不是白做的。有了设计模式,面向对象的设计就能发挥威力;有了设计原则,面向对象的设计才能保持高雅。其实这两种理论的实践性都比较强,能经得住实践的检验。
对对象新的认识
对象相当于有某种用途的“东西”,无论这种“东西”和用途是否抽象。所以才会有工厂等抽象式的对象。类的职责要单一,所以需要对对象准确归类,这样对象的职责也会比较单一,使得架构更优雅。
参考文献
https://softwareengineering.stackexchange.com/questions/133404/what-is-the-ideal-length-of-a-method-for-you ↩ ↩︎
https://softwareengineering.stackexchange.com/questions/66523/how-many-lines-per-class-is-too-many-in-java ↩︎
https://dl.acm.org/doi/pdf/10.1145/7351.8929 ↩︎