基于度量的程序结构分析
由于平时使用了NetBrains出品的IDEA作为IDE,在分析程序的时候我使用了IDEA的插件Metrics Reloaded。然而在使用时发现不懂得很多分析项目的含义,因此花费了一些时间了解,并在这里总结。
Complexity Metrics(复杂度分析)
这部分我们需要使用的主要是方法和类的复杂度分析。
方法的复杂度分析主要基于循环复杂度的计算。循环复杂度是一种表示程序复杂度的软件度量,由程序流程图中的“基础路径”数量得来。
ev(G):即Essentail Complexity,用来表示一个方法的结构化程度,范围在$[1,v(G)]$之间,值越大则程序的结构越“病态”,其计算过程和图的“缩点”有关。
iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在$[1,v(G)]$之间,值越大联系越紧密。
v(G):即循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。
对于类,有OCavg和WMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。
Dependency Metrics(依赖度分析)
依赖度分析度量了类之间的依赖程度。有如下几种项目:
- Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。
- Dcy和Dcy:计算了该类直接依赖的类的数量,带表示包括了间接依赖的类。
- Dpt和Dpt:计算了直接依赖该类的类的数量,带表示包括了间接依赖的类。
第一次作业
类图如下:
这次作业比较简单,当时也很很努力的用来OO思想。但从算法角度来说效率比较低。除了面向对象对象思想之外,还学习到了正则表达式和异常处理。
第二次作业
类图和度量分析图如下:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
"Building.Building(int,int)" | 1 | 1 | 1 |
"Building.getMaxFloor()" | 1 | 1 | 1 |
"Building.getMinFLoor()" | 1 | 1 | 1 |
"ERequest.ERequest(int,double)" | 1 | 1 | 1 |
"Elevator.Elevator(Building,double,double)" | 1 | 1 | 1 |
"Elevator.getFinishTime(int,double)" | 1 | 1 | 1 |
"Elevator.gotoFloor(int,double)" | 1 | 3 | 3 |
"FRequest.FRequest(int,double,String)" | 1 | 1 | 2 |
"FRequest.getDirection()" | 1 | 1 | 1 |
"FRequest.isSame(Object)" | 2 | 1 | 2 |
"Request.Request(int,double)" | 1 | 1 | 1 |
"Request.getFloor()" | 1 | 1 | 1 |
"Request.getTime()" | 1 | 1 | 1 |
"Request.isIllegal(int,double,Building)" | 1 | 3 | 3 |
"Request.isSame(Object)" | 4 | 1 | 4 |
"Request.parse(String,Building)" | 5 | 3 | 5 |
"RequestQueue.RequestQueue(Building)" | 1 | 1 | 1 |
"RequestQueue.finishRequest()" | 1 | 1 | 1 |
"RequestQueue.getRequest()" | 1 | 1 | 1 |
"RequestQueue.inQueue(Request)" | 3 | 2 | 3 |
"RequestQueue.isEmptyQueue()" | 1 | 1 | 1 |
"RequestQueue.isIllegal(Request,double)" | 4 | 7 | 7 |
"RequestQueue.readAll()" | 12 | 8 | 12 |
"RequestQueue.updateNext()" | 1 | 1 | 1 |
"RequestQueue.updateUntil(double)" | 1 | 4 | 4 |
"Scheduler.Scheduler(Elevator,Building)" | 1 | 1 | 1 |
"Scheduler.command()" | 1 | 3 | 3 |
"Scheduler.main(String[])" | 1 | 1 | 1 |
由于程序本身比较简短,度量工具看不出太多的问题。程序的耦合状况还可以。主要是用于输入的RequestQueue.readAll()
的复杂度较高,可以通过将方法进行拆解,提高可读性。
我将两种请求继承自一个Request父类,而在最近阅读《Java核心技术》时,我发现可以在Request类中增加一个工厂方法来进行字符串的解析,而不是把字符串的解析放到请求类本身的构造函数中。
第三次作业
类图和度量分析图如下。
Class | OCavg | WMC | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
---|---|---|---|---|---|---|---|
ALS_Scheduler | 3.5 | 21 | 0 | 10 | 10 | 0 | 0 |
Building | 1 | 3 | 0 | 0 | 0 | 5 | 8 |
ERequest | 1 | 2 | 2 | 1 | 5 | 2 | 7 |
Elevator | 1.75 | 21 | 0 | 4 | 6 | 3 | 4 |
FRequest | 1.5 | 6 | 2 | 3 | 5 | 3 | 7 |
HitchHikerQueue | 2.22 | 20 | 0 | 5 | 9 | 1 | 1 |
Movable.Direction | n/a | 0 | 0 | 0 | 0 | 5 | 8 |
Request | 1.88 | 15 | 2 | 3 | 5 | 7 | 7 |
RequestQueue | 2.25 | 27 | 1 | 3 | 8 | 3 | 3 |
Scheduler | 2.5 | 10 | 1 | 6 | 8 | 2 | 3 |
限于篇幅没有给出方法的度量,但是可以发现复杂度较高的方法有HitchHikerQueue.getNextFloor()
,RequestQueue.readAll()
,ALS_Scheduler.isHitchHikable()
。这三个方法的作用分别是得到在考虑捎带请求的情况下的下一个停留楼层,读入所有请求,以及判断当前请求是否可以被捎带。其中读入请求的方法不可避免,而另外两个方法则由实现方法导致。不过仔细检查之后确实发现了getNextFloor()
方法可以简化的地方。
这次作业的思路是由请求队列RequestQueue
读入和保存当前正在等待的请求,增加一个HitchHikerQueue
类保存正被捎带的类,电梯类Elevator
则保存了当前运动信息,具有有输出功能,Scheduler
负责调度。
这次作业的设计基本延续上次的思路,为了努力达到OO思想(实际上是偷懒符合符合直观),我设计了等待请求队列,捎带请求队列和带有所有运动信息和输出功能的电梯。但这样也给我带来了很多麻烦。原因如下:
- 将三个队列分开来使得时间信息被打乱,尤其是会有同时间但是先后顺序不同的请求。一开始我以为只要三个队列保持各自的顺序就行了,实际上后期对拍发现会有更加复杂的情况。因此我后来给每个请求都加上了时间戳,每次对队列进行修改都需要重新排序一次。现在回想,其实可以队列用两个优先队列实现,保存相同的元素,但是一个以时间戳作为key,另一个以楼层作为key(因为我的设计中需要不断找出下一个最近的楼层)。
- 因为指导书中提到用电梯的
toString()
输出,而且为了更好的OO,我把保存运动信息,输出状态,判断捎带的部分功能都移到了电梯中,又大大增加了复杂度。 - 出现了一些语法错误,例如在foreach语句中不能删除所迭代对象的元素。不过这些错误倒是帮助我学习到了一些关于迭代器,lambda的知识。
互测部分
说实话,三次作业拿到的代码都不算太好,还是比较面向过程的。三位同学都采用了一个个扫字符串处理输入而不是正则表达式反向匹配,而且在之后电梯的代码量又较大,让我很难把代码读进去。
因此,在测试过程中,我主要是把自己的测试数据或者对拍来找出对方的错误,然后调试程序找到错误点,找到原因并进一步发掘更多的错误点。
第一次作业
对方:第一位同学主要出错在数据处理上,比较原始的字符串处理方式难免的弊端。
我方:这次作业是我唯一被测出问题的一次,问题在于对指导书的理解有问题。我将指导书所说的不输出零项理解为了不输出次数为0的项,从而造成错误。
第二次作业
对方:第二位同学则是由于INVALID格式的问题,错误树上几乎被挂满。但我还是找出了一些边界条件上处理的错误,这位同学把关门时刻的请求当做了非同质请求,十分可惜。另外,他的输入在某些情况下会导致指针越界,是使用原始数组的弊端了。
我方:这次没有被测出Bug
第三次作业
对方:第三位同学在我一系列时间的对拍后都未出现错误,倒是代码中通过以0.5s为粒度的模拟方式比较有意思。虽然比较低效,但是使得程序难以出错。
我方:这次没有被测出错误
总结
这三次作业中,我对第三次作业比较不满意。一是因为开始写代码之前没有想清楚,造成后来拆东墙补西墙。另外这次作业还是有一些算法的意思在里面,以我的实现方法,就需要一个高效的数据结构,然而当时只是为了让程序跑起来,并没有想太多。