OOUnit2分析总结
设计策略与度量结构分析
第一次作业
本次作业,需要完成的任务为单部多线程可捎带电梯的模拟。
设计策略
本次作业的设计策略如下:
-
共设计了三个线程,分别是主线程、电梯线程、输入线程。主线程负责其他线程的创建与启动,输入线程负责从输入接口获取乘客请求信息,电梯线程负责可稍带运输乘客。
-
采用生产者-消费者模式。输入线程为生产者,电梯线程为消费者,分配器为共享资源。输入线程获得乘客请求,放入分配器,分配器将乘客请求按照楼层放置,电梯每到一层先与分配器交互判断是否有人要上电梯,再判断是否有人要下电梯,然后再与分配器交互来确定电梯运行新目的地。
-
采用电梯自调度的调度策略。电梯采用
LOOK
调度策略,但也有少许不同。当电梯中无人时,电梯会停靠在某一楼层,并且寻找最近的新请求;当电梯中有人时,电梯会首先寻找同方向新请求,如果没有就寻找同方向乘客请求,最后才寻找反向的乘客请求,此时电梯转向。由于电梯容量不限,电梯每到一层,都会将该层的所有新乘客接纳,将所有电梯内乘客放出,以减少开关门次数,电梯会根据现有情况计算新的目的地。由于采取自调度模式,所以调度算法并不是很复杂,但个人认为简单有效。 -
分配器作为共享资源做到了线程安全。分配器作为托盘的角色沟通电梯与输入,除了拥有放入、取出乘客请求的方法外 ,还有获取最近乘客请求位置和获取同向最近乘客请求的方法,这两个方法在电梯的自调度中发挥了重要作用。分配器采用
synchronized
关键字(同步锁)实现线程安全。 -
当输入线程结束时,会将分配器中的标记位置为假,当输入线程结束、无人在等待电梯、电梯中无人时,电梯线程结束。
程序结构
代码规模
OO度量
类图
协作图
第二次作业
本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。
设计策略
本次作业较上次增加了电梯数量、楼层范围、电梯容量,大部分设计策略同上次,只有少部分需要修改优化。本次作业的设计策略如下:
-
共设计了三类线程,分别是主线程、电梯线程、输入线程。主线程负责其他线程的创建与启动,输入线程负责从输入接口获取乘客请求信息,电梯线程负责可稍带运输乘客。主线程、输入线程只创建一个,而电梯线程创建数量由输入决定。由于电梯数量由输入决定,
ElevatorInput
类需要在主线程中实例化,再作为参数传入输入线程。 -
采用生产者-消费者模式。输入线程为生产者,电梯线程为消费者,分配器为共享资源。输入线程获得乘客请求,放入分配器,分配器将乘客请求按照楼层放置,电梯每到一层先判断是否有人要下电梯,再判断是否可以接纳乘客,与分配器交互是否有乘客,然后再与分配器交互来确定电梯运行新目的地。
-
采用电梯自调度的调度策略。电梯采用
LOOK
调度策略,但也有少许不同。当电梯中无人时,电梯会停靠在某一楼层,并且寻找最近的新请求;当电梯中有人时,电梯会根据是否满员决定是否寻找同方向新请求,如果没有就寻找同方向乘客请求,最后才寻找反向的乘客请求,此时电梯转向。由于电梯容量有限,电梯每到一层,先下乘客,再判断是否可上乘客,如果可上先上同向乘客再上反向乘客,以减少开关门次数,电梯会根据现有情况计算新的目的地。由于采取自调度模式,所以调度算法并不是很复杂,但个人认为简单有效。 -
分配器作为共享资源做到了线程安全。分配器作为托盘的角色沟通电梯与输入,除了拥有放入、取出乘客请求的方法外 ,还有获取最近乘客请求位置和获取同向最近乘客请求的方法,这两个方法在电梯的自调度中发挥了重要作用。其中,取出乘客的方法需要重写,以满足取出固定数量、优先取出同向乘客的功能扩展。分配器采用
synchronized
关键字(同步锁)实现线程安全。 -
当输入线程结束时,会将分配器中的标记位置为假,当输入线程结束、无人在等待电梯、电梯中无人时,电梯线程结束。
-
由于增加了负楼层,所以要在循环中特别注意
0
层错误情况的处理。 -
电梯线程会在分配器中等待人数为
0
并且输入线程仍在工作时wait()
。
程序结构
代码规模
OO度量
类图
协作图
第三次作业
本次作业,需要完成的任务为多部多种类多线程可捎带调度电梯的模拟
设计策略
本次作业开始区分电梯种类,不同的电梯移动速度、最大容量、可停靠楼层、编号不同,同时性能检测部分多了对乘客等待时间的考核。这些变化产生了更多的优化选择与方案,还产生了新的换乘问题。本次作业的设计策略如下:
- 共设计了三类线程,分别是主线程、电梯线程、输入线程。主线程负责其他线程的创建与启动,输入线程负责从输入接口获取乘客请求信息并且解析创建新的
Person
类,电梯线程负责可稍带运输乘客。 Person
类通过解析PersonRequest
类生成,包含最基本的信息,除此之外多了私有属性goalFloor
,该属性是电梯换乘的关键。换乘策略通过Floyd
静态最短路径算法得到,故需要实现一个TransferTable
类来封装获得换乘策略的功能。在Person
类定义一个静态私有变量实例化TransferTable
类,通过fromFloor
与toFloor
来得到goalFloor
,goalFloor
即为该乘客当前要前往的楼层。每当乘客要下电梯时,判断goalFloor
与toFloor
是否相同,如果相同则乘客抵达目的地,如果不同则为换乘,需要该电梯线程创建新的Person
对象放入goalFloor
。Floyd
静态最短路径算法首先初始化dis矩阵
,将可以直达的楼层楼层连接起来,权值可以设为楼层差的绝对值,也可以根据不同电梯的速度不同设置为不同的值。换乘时,可以将换乘消耗近似为电梯移动一层楼的消耗,也可以具体精确计算。然后,通过经典的三层循环来得到seq矩阵
。当实例化Person
类时,调用封装类TransferTable
类的方法得到对应路径的第一个goalFloor
。到达goalFloor
后,如果goalFloor
与toFloor
不等,实例化新的Person
类,得到对应路径的第二个goalFloor
,循环往复,直到抵达目的地。- 采用生产者-消费者模式。输入线程为生产者,电梯线程为消费者,分配器为共享资源。输入线程获得乘客请求,放入分配器,分配器将乘客请求按照楼层放置,电梯每到一层先判断是否可以停靠,如果可以,先判断是否有人要下电梯,再判断是否可以接纳乘客,与分配器交互是否有乘客,然后再与分配器交互来确定电梯运行新目的地。
- 采用电梯自调度的调度策略。电梯采用
LOOK
调度策略。当电梯中无人时,电梯会停靠在某一楼层,并且寻找最近的新请求;当电梯中有人时,电梯会根据是否满员与电梯种类寻找同方向新请求,如果没有就寻找同方向乘客请求,最后才寻找反向的乘客请求,此时电梯转向。由于电梯容量有限,电梯每到一层可停靠楼层,先下乘客,再判断是否可上乘客,如果可上先上同向乘客再上反向乘客,以减少开关门次数,电梯会根据现有情况计算新的目的地。 - 分配器作为共享资源做到了线程安全。分配器作为托盘的角色沟通电梯与输入,除了拥有放入、取出乘客请求的方法外 ,还有获取最近乘客请求位置和获取同向最近乘客请求的方法,这两个方法在电梯的自调度中发挥了重要作用。其中,取出乘客的方法需要重写,以满足取出固定数量、符合电梯种类、优先取出同向乘客的功能扩展。放置乘客的方法需要重写,以区别新来的乘客与换乘的乘客。分配器设置了新的私有属性
personNotArrivedNum
,每当新乘客产生,该值加一,每当乘客到达最终目的地,该值减一,该属性用来判断电梯线程是否应该结束。分配器采用synchronized
关键字(同步锁)实现线程安全。 - 当输入线程结束时,会将分配器中的标记位置为假,当输入线程结束、所有人都到达目的地时,电梯线程结束。
- 由于增加了负楼层,所以要在循环中特别注意
0
层错误情况的处理。 - 以下两种情况至少发生一种时,电梯线程会
wait()
:- 等待该类电梯的人数为
0
并且输入线程仍在工作 - 等待该类电梯的人数为
0
并且还有乘客没有到达目的地
- 等待该类电梯的人数为
- 由于
TimableOutput
输出接口不保证线程安全性,需要将TimableOutput.println()
封装为线程安全的方法。
程序结构
代码规模
OO度量
类图
协作图
第三次作业架构设计的可扩展性
本章节旨在从功能设计与性能设计的平衡方面分析第三次作业架构设计的可扩展性。
笔者认为第三次作业的架构较为合理。在构建程序的架构时,笔者比较注重功能性设计,即功能的完整性和稳定性。而性能设计,是以不破坏架构、不威胁正确性为前提去做的,所以性能优化方面势必会有些欠缺。
设计模式中的SOLID原则,分别是单一原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。遵循五大原则可以使程序解决紧耦合,更加健壮。接下来就从这五方面简单分析第三次作业。
单一责任原则(SRP)
指的是一个类或者一个方法只做一件事。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化就可能抑制或者削弱这个类完成其他职责的能力。例如餐厅服务员负责把订单给厨师去做,而不是服务员又要订单又要炒菜。
在第三次作业中,发挥主要作用的有6个类,分别是Dispatcher
,ElevatorThread
,InputThread
,Person
,SafeOutput
和TransferTable
。其中InputThread
负责接收并处理请求,包括电梯请求和乘客请求,将乘客请求传给Dispatcher
,将电梯请求转化为新的电梯线程。Dispatcher
负责解析乘客请求实例化Person
类。SafeOutput
负责安全输出,TransferTable
负责计算换乘楼层。ElevatorThread
负责上下移动,接人送人,将乘客送往目的地。综上,第三次作业较好地符合了单一责任原则。
开放封闭原则(OCP)
对扩展开放,对修改关闭。意为一个类独立之后就不应该去修改它,而是以扩展的方式适应新需求。例如一开始做了普通计算器程序,突然添加新需求,要再做一个程序员计算器,这时不应该修改普通计算器内部,应该使用面向接口编程,组合实现扩展。
在第三次作业中,该原则没有做得很好。因为在前几次作业的迭代过程中,所有的修改与迭代工作都是通过修改原来的类与方法实现的,而不是以扩展的方式适应新需求,没有做到面向接口编程。这样的功能扩展很有可能导致迭代前程序的错误,是不好的迭代与扩展。
里氏替换原则(LSP)
所有基类出现的地方都可以用派生类替换而不会程序产生错误。子类可以扩展父类的功能,但不能改变父类原有的功能。例如机动车必须有轮胎和发动机,子类宝马和奔驰不应该改写没轮胎或者没发动机。
由于第三次作业没有实现相关的接口与基类,所以这一原则没有在本次作业中体现,故不作分析。
接口隔离原则(ISP)
类不应该依赖不需要的接口,知道越少越好。例如电话接口只约束接电话和挂电话,不需要让依赖者知道还有通讯录。
在第三作业中,虽然没有实现相关接口,但是在隔离原则的角度上,还是表现不错的。除了方法需要的信息之外,其余不需要的信息都没有出现在类内部。
依赖倒置原则(DIP)
指的是高级模块不应该依赖低级模块,而是依赖抽象。抽象不能依赖细节,细节要依赖抽象。比如类A内有类B对象,称为类A依赖类B,但是不应该这样做,而是选择类A去依赖抽象。例如垃圾收集器不管垃圾是什么类型,要是垃圾就行。
在第三次作业中,本原则没有被体现。原因有二:一,本次作业没有利用到接口、基类等写法;二,本次作业的任务与对象比较明确单一,只有几种基本的类在交互与协作,没有太过复杂的依赖关系。
总述
总体来说第三次作业并没有很好的符合SOLID
原则,但是相比上一单元的作业,这次有了一些进步。比如在单一责任原则方面,第三次作业做得比较好。在功能设计与性能设计的平衡方面,第三次作业更加注重功能性设计,在力所能及的范围内做了性能设计,并且最后结果较好。在扩展性方面,由于做到了单一责任原则,个人认为第三次作业扩展性较好。
程序BUG分析
本单元的三次作业在强测、互测中均未出现BUG。
在此分析一个在本地测试时出现的BUG。在第三次作业时,出现了所有公测点均CPU超时的状况,在使用JProfiler
工具分析之后发现可能是出现了轮询或者无法正常退出程序的BUG。
为了解决这个BUG,笔者采取了四个措施:
- 在
Dispatcher
类中设置了新的私有属性personNotArrivedNum
,每当新乘客产生,该值加一,每当乘客到达最终目的地,该值减一,当该值为0
时电梯线程才可能结束。 - 电梯线程结束的条件:输入线程结束并且所有人都到达目的地。
- 以下两种情况至少发生一种时,电梯线程才会
wait()
:- 等待该类电梯的人数为
0
并且输入线程仍在工作 - 等待该类电梯的人数为
0
并且还有乘客没有到达目的地
- 等待该类电梯的人数为
- 细化
hasPersons()
方法,旧版为只要有人等待电梯返回值即为真,新版为当有等待本类电梯的乘客时才返回真值。
Hack策略
在Hack时,笔者采用手动构造测试样例的方法,所以在多线程这单元一般Hack不到Bug。
除此之外,笔者尽力使自己的程序架构合理,逻辑清晰,在本地手动构造测试样例来检验程序的正确性,尽可能覆盖常见情况,让别人Hack不到Bug。
心得体会
通过本次多线程作业,笔者颇受启发,总结如下:
- 类的本质就是数据与维护该数据的方法的集合。明确每个类的功能,使其职责单一化,做到这一点会大大增加的可扩展性和健壮性。每次作业设计的重点在于最大限度降低耦合,每个对象只应该管自己该管的事。
- 优秀的架构一定是兼顾正确性与性能优化的。所以,在写程序之前一定要着重设计程序的架构,使其尽可能合理,尽可能提高其工程性。
- 尽量将数据结构和算法作为独立模块进行封装,并且实现充分的可重用、可移植、逻辑分离。
- 将代码中经常使用的部分提取出来封装成函数,以避免代码的冗余与重复。
- 设置好方法与属性的访问权限,只在类内被使用的方法权限应设为
private
。这样可以对外部隐藏类的实现细节和保护类的数据。