BUAA-OO-Unit1
BUAA-OO-Unit1
度量分析
代码规模分析
先给出统计的三次作业每次的类数量、每个类的行数以及总代码行数。
和同学对比发现,我的代码结构总体控制的比较良好,总行数比较适中。在迭代中,主要的类始终都没有变化,都是分为Expression, Term, VariableFactor 三层结构。在第二次作业时,由于需要实现拓展函数功能,新增了 InputSolver 类,里面实现了函数的展开替换,并且把第一次作业放在 Main 类里的一些预处理字符串方法也封装进了 InputSolver 类中,使代码的结构更加合理,更符合面向对象的特点。
在迭代的过程中,除了第一次到第二次的过程中由于实现的两个新需求(函数展开,底层类的拓展)的代码量较大,导致了迭代时增加的行数较多,在第二次到第三次的过程中,由于整体架构比较良好,增添新需求并没有对架构进行改动,迭代时行数增加不多。代码规模总体控制的比较好。
经典 OO 度量
这一部分的分析依靠插件 MetricsReloaded 来完成。
首先从类的角度来分析,我们主要分析考虑以下三个指标。
在面向对象编程中,OCmax、OCavr和WMC是与软件复杂度和质量相关的度量指标。
1. OCmax(Maximum Cyclomatic Complexity):OCmax用于衡量程序中最大的圈复杂度。圈复杂度是一种软件度量,用于衡量一个程序的复杂程度,特别是针对程序中的控制流。较高的圈复杂度通常意味着代码更难以理解和维护。因此,通过监控和控制OCmax,开发人员可以努力降低代码的复杂性,从而提高软件的可维护性和质量。
2. OCavr(Average Cyclomatic Complexity):OCavr是指程序中平均圈复杂度的数值。通过计算平均圈复杂度,开发人员可以获得整个程序的控制流复杂性的平均水平。这有助于评估整体代码的复杂性,并且可以作为改进代码质量的参考依据。
3. WMC(Weighted Methods per Class):WMC是指类的加权方法数,用于衡量类的复杂性。这个度量指标将类中的方法数量进行加权,以反映出类的复杂程度。通常情况下,一个类拥有过多的方法可能表明其功能过于庞大,不够聚焦,这可能会导致代码的可理解性和可维护性下降。因此,通过监测和控制WMC,开发人员可以尝试设计更简洁、可维护的类结构。
根据这一数据,我们可以分析得出以下结论:
ElevatorRequestQueue
和ScheduleRequestQueue
的 OCavg 较低,但 WMC 分别为 29.0 和 10.0,数据表明这些类有多个方法,但大多数方法的复杂度不高。- 关于 Request 的类的 OCavg 和 OCmax 都不高,数据表明这些类只包含简单的方法。
Elevator
类是系统中最重要的类之一,但也是最具挑战性的类,因为它的 WMC 和 OCmax 都非常高。Schedule
类也有较高的复杂度,特别是OCavg值为5.0,表明这个类中的方法平均复杂度很高。
通过对每个类的复杂度指标进行分析和评价,我们可以更好地了解项目的结构,及时发现潜在问题,并采取相应措施来改进代码质量,从而确保项目的可维护性和稳定性。并且,联系项目结构的实际情况,发现实际情况和数据分析得出的结果基本一致,说明通过数据进行分析是一个可靠便捷的分析方式。
然后还可以从方法的角度来分析,我们主要分析以下三个指标。
ev(G)基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
方法的数量比较多,大部分方法的复杂程度都不高,在这里我们只分析小部分复杂程度较高的方法。
从基本复杂度CogC来看,可以发现在该数据集中,”VariableFactor.toString()”和”Expression.equals(Expression)”这两个方法具有较高的基本复杂度,分别为25.0和17.0。这个原因可能在于,VariableFactor.toString()方法由于考虑了在输出时可能遇到的各种情况,实现上进行了大量的分类讨论,是面向过程式的实现,方法也比较长;而Expression.equals(Expression)方法在进行表达式判等的过程中也相对比较复杂,且并不是很好降低复杂度。
模块设计复杂度Iv(G)方面,我们可以看到不同方法的表现差异。例如,”Lexer.nextToken()”这个方法的模块设计复杂度相对较高,为 10.0,这意味着这个方法与其他模块之间的耦合度较高。考虑到这个方法在整个表达式的解析过程中确实需要频繁的出现使用,这个高复杂度也是难以避免的。
判定结构复杂度v(G)方面,数据显示了各个方法的复杂程度。例如,”InputSolver.replaceConsecutiveSigns(String)”这个方法的复杂度较高,为6。这说明这个方法可能存在较多的独立路径。然而,考虑到这个方法只是一个简单的字符串替换操作,这个结果是可以接受和理解的。
类图
观察类图不难发现,除了主类 MainClass 外,其它类可以分为两种,一种是对输入字符串进行预处理的 InputSolver 类和 Function 类;另一种是对整个表达式进行解析展开的其他类。这两种类之间基本不存在相互调用的关系,在架构上是相对独立的。
我们主要关注对表达式进行解析展开的类。这些类中最顶层的是 Parser 类,里面包含了对 Expr, Term, Factor 三级表达式的解析展开方法。在实际应用时这三个方法其实也是逐级向下调用的,最主要的解析方法还是对 Factor 的解析,这个方法相对而言复杂一些,因为要考虑到 Factor 的多种情况。解析完 Term 和 Factor 后,parseExpr 方法主要对 Expression 类进行操作,将解析出的内容添加到答案中的 Expression 中。另外,parseFactor 方法中还存在对 parseExpr 的调用,这使得整个解析过程形成了一个递归下降的过程。
架构设计
最初完成第一次作业时,主要是仿造了实验部分中的代码结构,实现了最基本的 Parser 类和 Lexer 类,并在解析表达式时也分了 Expr, Term, Factor 三级,与实验部分代码的区别主要在于 Lexer 的 Token 识别和三级表达式结构的内部实现。不过,整个代码结构并没有很大的改动,并且这个架构也伴随了两次迭代,没有重构的需要,在我看来属于是很不错的架构。
在第一次作业过渡到第二次作业时,相对来说增加的代码量比较大,但项目结构没有很大的改变,一方面是拓展了最底层的 VariableFactor 类的逻辑,使其能够适应 exp 带来的变化;另一方面是新增了 InputSolver 类,在里面实现了函数的展开替换,这个操作也是用递归下降来实现的。
在第二次作业到第三次作业的迭代中,整个架构的优越性再次体现出来了,首先,第三次作业中新增函数的嵌套定义完全不需要再额外考虑,这可以完美的被预处理函数展开替换解决;其次,新增的求导因子实际上也只是一个新的运算,只需要新增求导方法,并在 Token 识别中新增求导符号的识别即可。
在可拓展性方面,如果需要新增运算符号因子,仿造求导因子的实现方法,在 Expr 和 VariableFactor 类中新增相应的运算方法并修改 Token 识别即可;如果需要新增像 exp 这种符号,如 sin, cos 等,整体的架构不需要过多的改动,但需要根据新的“最底层因子”修改 VariableFactor 的内部逻辑含义,且不少方法的实现需要进行相应的调整。
bug 分析
在我三次迭代的代码编写过程中,很少遇到 bug。但在第二次作业中,在对 exp 里的括号进行优化时,出现了错误,导致强测和互测中损失了一定的分数。
在判断是否对 exp 内的项增添括号时,我选择直接考虑 exp 内表达式 toString 后是否出现”*“号。其实,在大部分时候这个考虑也是正确的,然而,由于我也对常数项”-1”做了省略乘号的优化,如果在 exp 内的项是类似“-x”,这个判断方式就会出错。
修复 bug 也很简单,在先前的基础上新增一个对负号的判断即可。
不过有趣的是,我在课下对我的代码进行了充分的测试,但仍然没有测试出这个 bug。究其原因,是因为我在测试的过程中更多的关注在输出表达式的计算结果是否正确,而忽略了对输出合法性的判断。这给我们带来了教训,在测试时应该充分考虑所有可能的评价因素。
互测分析
很遗憾,在三次互测中,我都没有发现其它同学的 bug。这很大程度上与上文叙述的我的测试方式有关。
我在测试其他同学的代码时,主要是采用构造随机数据的方式来进行测试。首先随机数据并不能很好的覆盖到一些边界情况,更重要的是,我在测试时忽略了对输出合法性的检测,这导致我无法发现一些 bug。
我也尝试通过阅读他人代码架构来发现 bug,但发现的效果并不理想。这可能由于,大部分同学的整体架构都没有太大的问题,问题主要会出现在一些细节上,而这一般是很难直接通过阅读代码发现的。
因此,我更倾向于采用构造随机数据的方式来进行测试自己和他人的代码,在随机出问题数据后,再通过阅读源代码,定位找到具体的问题点。不过,这依赖于测试强度以及测试全面性,我便在测试全面性上考虑欠妥当,因此既没有发现自己的 bug,也没有成功 hack 其他人。
优化策略
在优化方面,我只考虑了 VariableFactor 的输出和同类项合并这两个点。
在输出方面,我考虑了输出表达式时,对常数项和变量项进行省略乘号的优化。
在合并同类项方面,主要问题在于,由于 exp 的存在,判断 expr 是否相等变得困难,因为这似乎是一个耦合在一起的东西:要合并同类项,就要判断 expr 是否相等;要判断 expr 是否相等,又要先对 expr 进行合并同类项…不过,这个过程也可以直接递归下去考虑。在编写代码时,其实并不需要考虑太多,递归的本质就是不需要过度在意它到底是如何逐渐向下递归的。只要把边界条件定义好,然后把递归的逻辑写好,剩下的递归过程就交给编译器了,可以直接认为递归调用后可以完成想要的结果。进而,这个合并同类项代码的编写也并不是很困难。
这两个优化点对于代码的正确性都没有危害。而对于代码的简洁性而言,合并同类项的方法代码量不大,但输出优化是很典型的面向过程式的处理,我直接用 if-else 语句来判断各种情况,导致 VariableFactor 类的 toString 方法有些臃肿。
心得体会
通过第一单元的学习,我对面向对象编程有了更充分的认知,也掌握了递归下降进行文法分析的相关技巧。
同时,在公测和互测的过程中,通过经历过的成功与失败,我更加深刻的认识到了测试的重要性,以及测试技巧、测试全面性的重要性。
通过平时和讨论课上与同学的交流讨论、互测时阅读其它同学的代码架构,我也收获了许多宝贵的设计经验。在后续的学习中,我将继续学习面向对象编程的相关知识,并尝试使用面向对象的思想来优化代码。
未来方向
总体来说,第一单元的设计非常不错,体验非常不错~