OO Unit1 总结

OO Unit1 总结

目录

  1 程序结构分析

    1.1 第一次作业

      1.1.1 设计思路

      1.1.2 UML

      1.1.3 Metrics

    1.2 第二次作业

      1.2.1 设计思路

      1.2.2 UML

      1.2.3 Metrics

    1.3 第三次作业

      1.3.1 设计思路

      1.3.2 UML

      1.3.3 Metrics

  2 bug分析

    2.1 自己出现的bug

      2.1.1 第一次作业

      2.1.2 第二次作业

      2.1.3 第三次作业

    2.2 发现别人的bug

      2.2.1 第一次作业

      2.2.2 第二次作业

      2.2.3 第三次作业

  3 设计模式

  4 心得体会

1 程序结构分析

1.1 第一次作业

1.1.1 设计思路

  第一次作业刚从面向过程过渡到面向对象的设计思想,所以诸如抽象层次、可扩展性、设计模式之类的问题都没有考虑。但已经有了基础的封装思想,将多项式封装成一个Poly类,维护各项的系数和指数,包含deriv方法用于求导。而对于处理输入数据和输出数据,则没有考虑到,因此堆积在主类中。

1.1.2 UML

  

  由于没有抽象层次的概念,只设计了Poly类和主类。

1.1.3 Metrics

  方法复杂度

  

  类复杂度

  

  因为问题本身的复杂度并不高,所以代码复杂度整体上不高。但由于很多功能堆积在主类中,主类的复杂度较高。这一点在之后的作业中得到了改进。

1.2 第二次作业

1.2.1 设计思路

  第二次作业加入了三角函数作为因子,使我开始思考抽象层次和可扩展性。

  首先我设计了Func接口,用于统一一切函数类。Func接口包含deriv和toString方法,分别用于求导和转化为字符串。此外,Func接口还包含add和mul方法,用于计算函数间的加法和乘法,这是因为函数之间是通过加法和乘法结合到一起的。后来,为了进行化简,我又在Func中添加了addSim和mulSim方法,用于判断两个函数是否可以通过加法和乘法合并为一个函数。

  具体的函数类我首先设计了Sum和Pro,分别表示多个函数相加和多个函数相乘。这两个类的数据结构十分相似,都是维护一个列表,存放相加或相乘的各个函数。后来我思考其中的原因,是因为加法和乘法这两种运算有着许多相似的性质,如交换律、结合律等。但由于加法和乘法的优先级不同,所以这两种函数类的许多方法的实现又有很大差异,因此难以再进行抽象把它们统一起来。

  考虑完加法和乘法两种运算后,还有各种具体的函数形式。因此我设计了Const、Var、Power、Sin、Cos类,分别表示常数、变量、幂函数、正弦函数、余弦函数。这里考虑到可扩展性,我在每一个函数类的属性中加入了对自变量的引用,为之后函数的嵌套做准备。

  第二次作业中,我开始使用工厂模式来实例化对象,将实例化的工作分配给各个工厂类来进行。在处理输入数据方面,设计了Parser类来进行输入的格式判断和预处理。通过这两步,将主类的复杂度大大地降低,解决了第一次作业中的问题。

1.2.2 UML

  

  这里只展示了各个函数类。其中Term是对因子的抽象,即在加法和乘法意义下都不可分的函数。Depend是对含有自变量的函数的抽象,Const是常数,不含自变量;Var本身就是自变量。

1.2.3 Metrics

  方法复杂度

  

  类复杂度

  

  因为在后续化简的过程中,我没有单独设计化简类,而是将化简的工作分为加法合并和乘法合并,分别交给Sum和Pro类,因此最终这两个类的复杂度明显过高。此外,因为Sum和Pro类在化简时需要对各种函数类进行处理,所以对其他函数类依赖性也很高。这个问题到第三次作业也没有得到解决,也是我程序中出现的很多bug的间接原因。

1.3 第三次作业

1.3.1 设计思路

  第三次作业引入了函数的嵌套,这是我在第二次作业中就已经考虑到的,所以对于已有的函数类我几乎没有做任何修改(最后的结果说明这是不明智的)。

  由于输入中存在括号的嵌套,所以在前两次作业中使用的正则表达式匹配的方法不再适用。我选择为每一种函数形式设计一个处理输入的方法,递归地解析输入表达式,在解析的同时就生成函数对象。我认为这种想法是高明的,只是由于我的写法粗糙,在处理输入时引入了许多bug。

1.3.2 UML

  

  这里只展示了各个函数类,与第二次作业没有变化。实际上,我的第三次作业的结构与第二次作业也几乎完全相同,只是在处理输入的方法上有差异。

1.3.3 Metrics

  方法复杂度

  

  类复杂度

  

  因为Parser类承担了有关输入处理的所有工作,包括格式判断、预处理、解析表达式同时实例化对象,所以其复杂度明显高于其他类。应当将不交叉的工作交给不同的类。

2 bug分析

2.1 自己出现的bug

2.1.1 第一次作业

  第一次作业我并没有使用自动测试,而是自己构造测试用例,覆盖率比较低。但问题本身并不复杂,所以没有出现任何bug。

2.1.2 第二次作业

  第二次作业中,为了更方便地使用正则表达式处理输入,我对输入进行了一系列预处理,如去掉空格、合并加减号、去掉指数中的正号等。这些预处理在第二次作业中带来了较大的便利,也很容易实现。但进行这些预处理时,需要十分谨慎地整理那些可能在这一过程中引入的格式变化,避免改变输入的语义,或是将原输入中的错误格式隐藏了。在第二次作业中,我就因为预处理时不够小心,产生了一些格式判断上的bug。这个问题在第三次作业中更加明显,导致了极大的麻烦。

  从第二次作业开始我使用了自动测试。输入数据生成方面,由于指导书中给出的合法输入的形式化表述是上下文无关文法的形式,所以我选择利用文法随机生成输入数据。仅仅利用上下文无关文法生成数据存在不能在有限步结束的问题,好在作业中对输入长度进行了限制,所以我设定了一个最大步数,如果生成步数达到最大步数,则按照我设定的法则将所有非终结符替换为终结符。输出数据检验方面,根据讨论区中给出的方法利用sympy包进行表达式的求导和比较。随机自动测试相比人工构造测试用例,在覆盖率方面肯定有优势,但在一些边界情况上往往无法覆盖,这种情况就需要自己仔细地排查,构造特殊测试。

2.1.3 第三次作业

  第三次作业中,由于我的预处理方式基本沿用了第二次作业,而没有进行谨慎的整理,导致原输入中的一些错误格式在预处理中被隐藏了,而又引入了一些新的错误格式。因此,我在格式判断环节出现了许多bug。因为这些bug杂糅在一起,修复起来也十分复杂。

  第三次作业的自动测试程序几乎和第二次作业相同,只是在生成输入数据时用第三次作业的文法替换第二次作业的文法。由于我没有意识到考虑边界条件的重要性,我并没有人工构造足够多的测试用例,导致很多bug并没有被测试出来。

2.2 发现别人的bug

2.2.1 第一次作业

  第一次作业中,我是用人工构造测试用例的方式发现别人的bug。人工构造测试往往能覆盖一些边界情况,在互测阶段比较有效。此外,我也认真阅读了其他人的代码,从中发现了一些可能的问题。

  第一次作业中发现的一些bug原因都是诸如结果为0时无输出、忽略符号之类的小错误。这些bug如果自己不进行仔细的测试是很难发现的。

2.2.2 第二次作业

  第二次作业中,我开始使用自动测试来批量地测试其他人的程序,并且完全抛弃了构造测试用例。这样的方法虽然很省事,但是效果并不好,因为出现bug的地方往往是十分特殊的情况。当然,我也阅读了一些人的代码,但并没有从中找到问题。

2.2.3 第三次作业

  第三次作业中,我使用批量自动测试后观察测试结果,推导可能的问题,并针对性地构造测试用例。这样的方法在这一次互测中比较有效,对今后作业中自己做测试有重要的借鉴意义。但这种方法同样存在问题,在人工推导可能的问题时也许会发生偏差,导致最后发现的bug并不是刚开始测出的bug,因而忽略一部分bug。在这一次互测中,由于大家的代码量都很大,我并没有仔细地阅读其他人的代码。

  第二次作业中发现的bug原因多种多样,但主要集中在格式判断和输出合法性方面。

3 设计模式

  在第二次和第三次作业中,我都使用了工厂模式来实例化对象,并对这一设计模式有了比较深刻的理解。

  工厂模式的优势在于将“实例化”这一操作也抽象化了。工厂类是一种工具类,它是对实例化操作的抽象。通过建立工厂类,可以将各种不同类的实例化操作统一处理,并且可以解除需要需要实例化对象的类与被实例化的类之间的耦合,增加了可扩展性。

  这种思想还可以用于其他场景,对于有着相同形式或是本质上相同的操作,我们也可以将其抽象出来,由一个独立的类来完成。这样就可以将一些操作统一起来处理,并且通常都能达到解耦的目的。

4 心得体会

  在第一单元的作业中,我对程序设计的认知已经完全过渡到面向对象的思想。相比于从前遇到问题时首先考虑算法,现在我已经习惯于从数据之间的层次与依赖关系开始思考,首先考虑抽象的方法。

  此外,我还意识到了测试的重要性。在今后的程序设计中,应当将测试工作贯穿于整个设计之中,并且应当将随机自动测试与人工测试结合起来,不能迷信自动测试的效果。

posted @ 2020-03-20 21:18  Somny  阅读(131)  评论(0编辑  收藏  举报