第一次做业我只使用了两个类,正像下面的类图所表示的那样,分别是Poly和ComputePoly。Poly类是不可变的,能保存一个多项式,能够进行加、减运算。ComputePoly是程序的主类,可以读取一个多项式加减运算表达式的字符串,并输出计算结果。parseExpression方法经过调用parsePoly方法和parseOperator方法将输入的字符串转换为Poly对象和运算符列表。compute方法从polyList和opList取出多项式和运算符进行计算,返回计算结果。正则表达式
总的代码量是248行(后面的度量分析图中能够看到),其中ComputePoly类占167行,Poly类81行;程序总共有28个方法,Poly占10个,而ComputePoly占18个。因为写Poly类的时候参考了教材的写法,Poly还算比较合适,不管是从两个类的代码量仍是方法个数来看,ComputePoly都显得不太协调。事实上也如此,ComputePoly作了除计算外全部的事情:处理输入(包括错误处理,提取多项式),将多项式和运算符们存到数组里,调用compute方法得出结果,最后输出结果。这是典型的面向过程式的思路,很有一点用C语言写程序的味道。数组
再仔细一想,ComputePoly作这么多事情合适吗?就这个不那么复杂的程序而言,管理ComputePoly所作的事情仍是可以接受的,可是再实现一个乘法功能呢?很明显,上面的两个类都须要作修改,对于Poly而言,只需增长一个计算乘法的方法就能够了,可是对ComputePoly来讲,第1,4,5,6共4个方法都须要修改。假如按照建议设计那样将ComputePoly进一步分红3个类InputHandler,PolyManager,PolyArithmetic,一样地,输入处理InputHandler要作修改,PolyManager也须要修改,但PolyArithmetic不须要修改。实际该改的都得改,说白了仍是前面所说的第1,4,5,6个方法。可是在这样一种设计下进行修改的复杂性就下降了,修改InputHandler时只须要记得正则表达式改一下能匹配乘号,而不需关心乘法和加法在一块儿时的谁先计算谁后计算的问题;而修改PolyManager时也无需关注输入是否处理好了,只用专心实现calculate方法和appendOperator方法(以下图)。这样一来不用在ComputePoly长长的代码中苦苦寻找某个方法,上改下改,减小出错的可能性,二来也不会被总体的复杂性所烦扰,分解成两个问题后可单独实现。app
不管如何,在初识面向对象设计前我仍是带着面向过程式的思惟,幸亏可以从教材上学习到如何编写Poly类(很是感谢第一次做业时有这么好一个范例),初步领略了OO之美。下图是用eclipse的metrics插件对第一次做业代码分析获得的结果。重点关注一下第一个McCabe Cyclomatic Complexity,它中文名叫圈复杂度,是流程控制图中独立路径的数目,主要由分支和循环个数决定,越大代表越复杂,测试时所需考虑的状况也就越多(由于每一个路径都要被测试),当它很是大的时候,程序的测试就变得十分复杂。这里的最大值是10,还能接受。仔细查看,形成最大值的方法是ComputePoly里的parsePoly方法,缘由多是须要进行输入检查,涉及的不一样错误输入种类数较多。eclipse
Nested Block Depth是if,while,for的嵌套深度。这里是最大值3,处于正常范围。最大值一个来自Poly的add方法,考虑到实现两个多项式加法的细节,是能够接受的,另外一个一样来自parsePoly方法,缘由一样是涉及输入错误处理。学习
再看一下Method Lines of Code,方法的代码行数,这里最大值是26,不算太大。测试
这次做业的bug在于正则表达式的匹配,我使用一个正则表达式试着去匹配整个输入的字符串,潜在的危险是堆栈溢出。在分类树上是有“压力测试”这一项的,并且助教也说过注意不要爆栈,可是当时没想明白什么会致使栈溢出,并且是第一次使用正则表达式,主观上也认为压力测试没什么用,就没有构造相应的测试样例,致使了这个bug的产生。bug出如今isCorrectFormat方法中,这个方法检查了输入的字符串是否符合规定的格式。要解决这个问题可采用分段匹配的方式,整个字符串是多个多项式,中间用加减号链接,所以能够分别匹配每一个多项式。优化
测试同窗代码时,我使用了每一个分支树结点的对应测试用例(除了压力测试)。同窗的代码中存在相似以“f”,“ff”,“fff”命名的变量,我尝试着去理解同窗的意图,再加上大量的分支循环嵌套,实在是难以揣测。编码
第二次做业一开始我花了一天的时间理解做业指导书,从头至尾读了好几遍才弄清楚。到第三次做业也是如此,我试着边读边用本身的语言去表达指导书的规定,而且举出例子,而后分条把以为重要的点写在纸上,这样作有些笨拙和繁琐,不过的确能帮助我理解指导书的意图。插件
课件中给出了提示,要构造电梯类、调度类、请求类、请求队列类和楼层类共5个类(以下图)。要怎样肯定该哪一个类该作什么,这是除了理解指导书外另外一件头疼的事情。冥思苦想,苦思冥想,难以划分各个类的职责。另外一个问题是调度器类的command和schedule方法,弄得我一头雾水,写完后都没能参透其中奥秘,直到看了互测同窗活生生的代码,才恍然大悟。设计
第二次做业的要点是如何把程序功能均衡地分配给各个类,如何让多个类之间协同工做,要避免出现Idiot Class和God Class。从下面的类图中能够看到Floor类就是比较白痴的一个类,它只知道楼层顶楼和底楼的编号。其实,能够考虑让楼层类知道更多的信息,好比某层楼是否有电梯到达。
除了出现了一个Idiot Class外,另外一个缺点是,在main方法里展开对输入的处理,这与第一次做业相同,因为本身没能意识到这种作法的坏处,在第2、三次做业时仍没有加以改正。
本次做业的设计是否均衡呢?下面就再用定量的方法分析一下。
每一个类的属性个数、方法个数、代码行数以下面的表格所示,其中方法个数包括了构造方法。从数据上看,方差较大。代码行数最多的类是ElevatorSystem,多是由于在这个类里作了输入处理,若是把输入的处理分开来,应该会更均衡一些。
类 | Elevator | ElevatorSystem | Floor | Request | RequestQuue | Scheduler | 均值 | 方差 |
属性个数 | 7 | 4 | 2 | 4 | 2 | 1 | 3.3 | 4.6 |
方法个数 | 14 | 2 | 4 | 9 | 10 | 6 | 7.5 | 19.1 |
类代码行数 | 95 | 107 | 18 | 48 | 38 | 55 | 60.2 | 1170.2 |
一个比较大的数据是电梯类的方法个数14,其中用于状态查询的方法占了一半。因为事先未规划好,在编码的时候为了方便,新增了一些方法。仔细分析会发现一些方法是冗余的,好比getStatus方法,事实上这个方法也从未被调用过。另外一个缘由多是题目要求的电梯状态是定义在左开右闭区间上的,有时候为了方便我会使用左闭右开区间,这也增长了必定的复杂性。
再看一下类的职责是否明确。
拿电梯类举例,它总共有14个方法,方法总数占到整个程序约1/3,但仔细看,只发现可以让电梯改变状态的只有前两个方法readyToGotoFloor和run,run方法是让电梯运行到0.5s后的状态,而前者肯定电梯的下一个目标。也就是说,别的类只能告诉电梯下一次去哪一个楼层,电梯只管去,而且本身决定方向,其余类不能干涉电梯的运动方向。假设其余类能直接修改电梯的方向,那么在这个设计中,若是调度器让电梯向下走,但又是去往楼层数高的地方,这明显是不合适的。电梯内部不存在请求队列,不管什么时候,电梯都只有一个目标,它不用操心有多少请求在队列中等着它执行,只用遵从调度类的指挥就能够了。
从功能的角度上看,电梯的职责是明确而单一的。可是这样作的复杂性在于,电梯调度类须要精心地设计,在每次给电梯发送命令前,须要使用电梯类提供的一系列状态查询方法检查电梯状态(之因此要检查是由于调用readyToGoToFloor会当即改变电梯的运动状态,例如当电梯向上运行时,调用方法让电梯去往比当前楼层数小的楼层,运动方向就会忽然改变)。所以,调度类必须充分了解电梯各个状态的含义(尽管它不须要了解电梯是怎样肯定本身的状态的)和一些内部细节,不然就可能会致使电梯出故障。这就在必定程度上增长了电梯类与调度类的耦合性,一是使编码时复杂性增长,二来修改、新增功能时容易出错(例如我在写第三次做业的时候就在这上面犯了不少错误)。
下图是第二次做业的度量分析结果。能够明显地看到标红的圈复杂度较高,最大值是17。进一步细看(图中未给出)能够发现高复杂度的来源主要是ElevatorSystem类的parseRequest方法和main方法,以及Elevator类的run方法。前者是因为输入的错误状况较多,我的写得比较凌乱,判断逻辑复杂,main方法里也作了输入的处理。后者是电梯运行时的逻辑稍微复杂,分支较多,也有两层嵌套的状况。
再看一下每一个方法的行数(图中最后一行),最大值是46,与第一次做业相比有所增长,其来源一样是parseRequest和main方法。若是将输入处理部分单独封装在一个类中,而且优化一下错误处理的逻辑,应该能使总体设计均衡一些。
此次做业的bug是在时间很大时运行的时间较长,须要十几秒,这是因为实现采用了每0.5s进行一次操做的方式。
本次做业在前一次做业的基础上增长了捎带功能,用继承的方式实现了ALSScheduler,对其余的类也作了一些调整。
因为保留了第二次做业的大部份内容,本次做业在均衡性上没有改进,反而因为增长捎带功能后变差。尤为是ALSScheduler,代码行数最多,逻辑也较复杂。
类 | Elevator | ElevatorSystem | Floor | Request | RequestQuue | Scheduler | ALSScheduler | 均值 | 方差 |
属性个数 | 8 | 4 | 2 | 4 | 2 | 3 | 4 | 3.9 | 4.1 |
方法个数 | 17 | 2 | 4 | 12 | 11 | 5 | 12 | 9 | 29.3 |
类代码行数 | 108 | 108 | 18 | 59 | 48 | 55 | 141 | 76.7 | 1857.9 |
细心的读者可能会发现此次的圈复杂度降低了1,但这不是由于进行了优化,只是作了点微调。这里最大值16也不是前面提到的输入处理带来的,而是来自ALSScheduler的command方法。为了实现捎带功能,我增长了不少条件判断,既难写,又难以理解和修改。
上面的3点都是给我互测的同窗发现的,这里要感谢这位同窗。
最后再分析一下第二、3个bug与设计结构的关系。这两个bug都位于ALSScheduler类中,具体在多个方法中都有体现,究其缘由,是使用了Java标准类库中的优先队列。这个队列专用于捎带队列,我按照到达楼层的时间(前后)做为各个请求的优先级,可以最先达到的,排在队首,晚到的,排在后面。问题出在同一时刻进队的请求,在出队时可能失去了输入时的顺序以及请求发出的时间顺序,这就致使了第2个bug的产生。另外一方面,当主请求执行结束时,处在捎带队列队首的请求未必是按照请求发出时间最先的。
我能想到的解决办法是专门实现捎带请求队列类,兼顾到达时间顺序与请求时间顺序。在下一次做业中我会尝试着改正。