作业1——多项式运算
基于度量和类图分析设计
先看Metrics插件做出的复杂度分析:
乍一看没有红色报警,其实是因为选中某一行时会自动将该行改为黑色,无论之前是红色还是蓝色emmm
真正展开第一行时,惨不忍睹:
为什么圈复杂度这样大?先看看类图:
- 【当时的设计思路】有个poly类,既是poly的数据结构(系数和幂指数的数组),又是其方法载体(能够实现加减、打印自身信息);还有一个负责统筹规划的计算者类,能够分析输入的字符串、进行多项式运算、打印错误信息。
- 【如今看来需要做的改动】
(1)把分析字符串合法性的工作放给poly,他应该知道自己是不是一个合格的poly。
(2)打印错误输出这件事,应该另起一个类,实现功能的划分。
(3)不要洋洋洒洒就用数组。
(4)字符串匹配这事儿,不要用状态机来做。
- 【我为什么当时用状态机】这是一个令人心酸的故事QAQ
首先,我花了一上午熟练掌握了正则表达式的通用知识,打算在C程序(作业的另一个要求)中试试手,便去下载C程序所要的相关的头文件等,结果一直用不了...大神跟我说C最好不要用正则表达式emmm 于是我想起来学姐提示过的有限状态机,我画了个大状态机,反复分析论证其正确性,然后在C程序实现了。发现时间不多了赶紧跑去干java,可能是太紧张了吧,java正则表达式教程我看了俩小时没懂,突然失去自信...那就先稳一点用状态机写吧,之后就没时间改过来了...
这个故事告诉我们,做事前一定要分轻重。
后来第一次实验课时,要求我们会用java使用正则表达式,于是我打开被分配到的互测作业,有例子果然学起来就是快,10分钟我就懂了...
所以同样是紧张,为何效率如此不同...答案或许是,面向例子学习法万岁!
- 【PS】状态机唯一的优点(安慰自己一波),就是能在实现状态的同时,顺带对错误的输入进行准确的错误提示,比如,在第几个字符处出现了错误,用户输入了什么,而本程序期望怎样的输入。
- 【状态机导致了高复杂度】既然我用了状态机,那么swith-case, if-else就暴增了,而McCabe圈复杂度:
认为程序的复杂性很大程度上取决于程序图的复杂性。
单一的顺序结构最为简单,循环和选择所构成的环路越多,程序就越复杂。
Bug分析
- 公测:通过
- 互测:我方安全,对方正则表达式爆栈
- 课下自我调试:发现poly打印自己的函数有bug——在考虑 结果多项式 的 某个term 的系数为0 的情况时,漏掉了一些情况导致的bug,后来对系数为0的term出现的位置进行了首、中、末的遍历,来确保自己的打印输出格式正确。
- 今晚是个平安夜。
作业2——傻瓜电梯
基于度量和类图分析设计
先看Metrics插件做出的复杂度分析:
这次是真的没有红色报警~撒花~
那么再看类图:(自己手动调整了位置看起来 局部对称、思路清晰,建议全屏看大图)
- 【类设计思路】
首先明确指导书的要求:需要实现的类 包括但不限于Elevator, Floor, Request, RequestQueue, Scheduler 这5个类(我承认 命名的时候去搜过翻译...)
是否要加其他类:为了让两种请求有所区别,我设计了RequestER, RequestFR 两个请求类的子类; 为了让main函数不要放在某个其他类里导致没有逻辑关系,我加了Main类; 我吸取第一次作业的教训,将打印错误输出抽出来作为一个Err类。
- 程序开始了,字符串输入进来,谁来处理呢?为了让Floor类有事干(哭),我把parseStr分别放在了Floor类以及和它对应的Elevator类里面,也就是说楼层和电梯都能够判断自己接收的请求是不是合格的,同时也能够根据 合格与否 的判断结果,来构造FR或ER类型的请求并返回这个请求。
- 【那么问题1来了】
判断是否为”合法请求“这件事,所需要的条件不仅仅是内部信息(即输入的一行字符串),还需要外部信息(即:1. 这条请求是否是第一条请求,如果是需要满足请求时间为零 2. 这条请求的时间输入顺序是否正确,因为我们要求这堆请求的请求时间应该是非递减的)。
当一个面试者知道自己能力和颜值已经合格的时候,面试官还会把这个面试者和其它面试者进行比较,”比较“这件事就是外部条件吧,那么我们可以有两种方式来判断这个面试者是否内外兼修地合格:
A. 面试官勤奋些,把 自我判定内部条件为合格 的面试者拿出来,做做比较。
B. 面试者的工作全面些,也就是把 其他面试者的信息等 也交给 这位面试者,让面试者自己去判断 自己是否同时满足内、外部条件。
脱离这次作业,从更加抽象的角度来讲,我觉得这两种方法各有利弊。 A似乎更贴合现实,使得上层面试官看起来更加统筹规划、更有权力的样子。 B似乎把 “判定合格”这件事包装得更好,不用把“内、外条件判定”这件事 进行分离(可是这样的话,总觉得面试官是不是没有存在的意义了...)
我无法做出判断,不过根据本次作业的实际考虑,我选择了A。
- 程序继续,把合格的请求挑出来放到队列里面去,然后应该做什么呢? 让调度器去 删除同质请求 得到真正的请求队列、然后让电梯运动。
- 【那么问题2来了】
调度算法如何实现?根据我后来的了解,大体上有这么2类思路:
A. “先知”式的,只面向请求队列,对请求队列进行一定的扫描,得出真正要执行的请求队列 以及 其中每个请求 的开始执行时间和执行结束时间,然后跟傻瓜电梯说,你就按着我这个计划来动就行。(为什么叫先知?先看看生活现实:随着时间的推进,一方面电梯在运动,同时,请求也在不断地到来。而“先知”式调度, 是 坐了时间穿梭机去未来,把所有请求都拿来分析,然后穿梭回过去,跟电梯说你应该怎么怎么动。总的来说,就是交互性不强。)
B. “模拟”式的,从现实生活得到启发,模拟时间推进 或 模拟电梯爬楼.
笔者写第2次作业时,只想到了A,然后就动手了,效果还不错。而且我很懒,觉得电梯既然那么傻,干脆不给他运动的方法了,直接让调度器跟他说:你就把我传给你的参数按照某个格式打印出来就行,都不用动的...
- 【问题3和问题1有点像】
删除同质请求这件事,我放给了Request类及其子类,但是又遇到内外部条件的问题。
如果 r2 是 r1 的同质请求,那么首先需要满足这个时间条件: r1的请求时间 <= r2的请求时间 <= r1的执行结束时间。一开始我把这个时间条件当成外部条件,由Scheduler判断 r2 r1是否满足时间条件,再由 r1 判断 r2 是否满足 请求类型、目标楼层、请求方向 等内部条件,这样就把 “判断同质” 这事儿拆分成了两部分,当时就是感觉心里有点小疙瘩,忍一忍就过去了...
后来我做第三次作业的时候,受到一些同学的启发,觉得完全可以把时间条件做成内部条件,也就是请求类增加一些属性,这样就可以实现功能的包装了。
反过来去思考问题1,如果要实现这样的功能包装,或许要把请求队列传进来给Request,这样Request才能知道自己在队列中的位置 以及 队列中其它请求的情况,也才能判断自己是不是一个真正合格的请求,但是显然我们不应该让 碗里的豆豆 直接知道 这个碗的信息以及碗里装的其它豆豆的信息 。而且如果真的实现了这样的功能包装,RequestQueue就真的成为 傻瓜式纯容器 了(哭)。
【PS】聪明的你或许已经发现,我的设计 可扩展性 有点弱,到第三次作业就不得不大换血了...
Bug分析
- 公测:通过
- 互测:我方安全,对方没有交readme被我报了2个bug
- 课下自我调试:没有什么可说的bug
- 今晚是我的平安夜。
作业3——“耍点小聪明”的电梯
基于度量和类图分析设计
先看Metrics插件做出的复杂度分析:
一共红了两处,分别是 McCabe圈复杂度 和 嵌套块深度,摘录出来,发现是 A Little Smart Scheduler 出了问题:
作业1的分析中,我对McCabe圈复杂度进行了一定的说明,回顾一下:
它认为程序的复杂性很大程度上取决于程序图的复杂性。
单一的顺序结构最为简单,循环和选择所构成的环路越多,程序就越复杂。
那么这个 A 圈复杂度 和 B 嵌套块深度 的区别,我觉得是,前者兼顾了广度和深度两方面的复杂检测,后者专注于 深度的复杂检测。有点像B能推出A,A不能推出B的关系。
那为什么这两个度量值会报警呢——耍点小聪明的ALS_Scheduler类里面,由于具有 扫描队列来取出主请求、扫描队列来得到能够被捎带的请求、扫描队列来删除被捎带的请求的同质请求、扫描队列来删除主请求的同质请求 这4个扫描遍历,以及 配套的许多判断条件,所以这两个度量值 直接爆表。(尽管我在课下de完bug之后,尝试着把其中一些块抽取出来包装成方法)
类图如下:(笔者将类的位置进行了调整,方便和作业2的类图进行很好的对比)
- 【思想大换血】
一开始,我打算沿袭第二次作业的“先知”式调度,但是却发现,在遍历请求队列的时候,主请求的执行结束时间是动态变化的(如果有请求会被它捎带的话),{ 每次一发现可以被捎带的请求R,就必须删除掉R的同质请求,更新 主请求的执行结束时间,然后再次从头遍历请求队列 },然后重复执行上述{ }的内容,直至没有可以被捎带的请求了。
这么一听,好像不过是稍微复杂了些而已。但是有个问题,当电梯处于STILL状态时,被捎带的条件就变得不大一样了,所以必须记录电梯在 什么时候 在 哪个楼层 为了捎带而停下来开关门,然后我就懵了,这个信息要如何记录,怎样记录才能方便我查询。突然失去信心。
于是我便和学霸交流了下,发现学霸模拟电梯爬楼,意识到其中的几个大优点:
- 和模拟时间相比,不会出现 “由于输入的请求时间过大 而导致程序运行时间变大” 的现象。(这个现象其实可以通过某种办法消除掉)
- 没有 “先知”式调度 的 “一找到被捎带请求就要再次从头遍历” 的问题,以及由此带来的输出顺序问题。
- 在处理STILL状态时的捎带判断时,十分方便,不需要什么数据结构来记录。
- 模拟电梯爬楼,每次执行完一条请求,就让电梯运动并打印运动状态,十分方便,不用再拿数据结构去存这条请求的执行结束时间等,而且方便调整输出顺序。
- 【类设计变更】听了我的设计思路变化,对比于作业2的类设计图,聪明的你应该已经知道我的类设计变化了:
- 电梯没有那么傻了,给它增加了 几个运动的方法 以及 一个时间属性。
- 耍小聪明调度器 继承了 直男癌调度器,但是并没有用到 start[] end[] 数组,这两个数组在第二次作业中记录的是 每个真正有效请求 的 开始执行时间 和 执行结束时间;现在,由于电梯里面记录了时间,而且程序是在每次处理完一条请求后,就打印输出,所以这两个数组就没有存在的意义了。
- 其它适应性的小变化。
Bug分析
- 公测:通过
- 互测:我方安全,对方公测过了但被我另找出了3个bug
- 课下自测:输出顺序有问题,这也是大家最容易忽略的一个问题吧。
- 今晚还是我的平安夜~
其它总结
发现bug的策略
- 解剖指导书。我遍历了几遍指导书,而且自己用笔把一条条规定都简单列了出来,用红笔标记不同条列之间的关系以及易忽略的点。然后进行一些深入思考,比如指导书说到电梯可能的几个状态的时候,我就画了个状态机出来。深入了解指导书是前提。
- 给自己程序打过的补丁,也可能是别人容易漏掉的。
- 不同的设计方式会有不同的易错点,如果你抽到的代码的设计方式和你不同,可以找找其他使用这个设计方法的同学,问问他们课下de出来了什么bug。
- 讨论区和微信群里大家的讨论也都一条条列出来了,并和3合并。
- 与你亲爱的同学交换测试样例。
- 把问题抽象到一定层次,以更加宏观的视角去看待,就更容易发现问题所在。比如我第三次作业的时候,就画了个时间轴,以最复杂的捎带情况为基准来分析,然后遍历所有可能出现的情况。同时,这个时间轴也很方便我构造测试样例。(在这里表示羡慕大佬 只需要动动脑子 就可以模拟出电梯运动情况,反正对我来说,每看到一个测试样例,我基本都要画个时间轴,不过越画越快了嘿嘿嘿)
还可以改进的地方
- 加入枚举的使用。
- 通过让方法 抛出异常 来更好地包装方法。比如,如果传入的A满足条件,请返回关于A的某个具体的内容,如果传入的A不满足条件,就抛出异常。
总之,我现在的设计状态是,必须把设计的大体思路、以及某些实现细节,都想清楚了,伪代码也写得比较细致了,我才会开始码代码,而且想清楚之后 写代码贼快,也便于debug。而且,在设计和思考的时候,脑子处于及其活跃的状态,这个时候容易想到各种你之后可能想不到的细节,尽管这个过程比较痛苦。另外,我会请给我指点迷津的大佬次饭的,再次感谢~
预祝大家 习得OO,策马归来~