面向对象程序设计第一单元作业总结

面向对象程序设计第一单元作业总结

第一单元的总任务是实现多项式的展开,三次任务难度递进。从第一次任务实现单一变量多项式括号展开,到第二次作业完成多项式的括号展开与函数调用、化简,再到第三次作业完成多层嵌套表达式和函数调用的括号展开与化简,我在这三次作业中不断深化了层次化设计的思想,学习了使用面向对象的思想解决问题的方法。

1.第一次作业分析

第一次作业需要完成单一变量多项式括号展开。根据课程组提供的训练题目,我尝试使用递归下降的方法完成这次作业。与训练教程中通过分析下一个字符来确定当前字符属于因子、项、表达式三种不同层次对象的方法不同的是,我使用了正则表达式来直接捕获表达式中的项,再从项中捕获因子进行分析。

1.1程序类图

image
通过类图可以看出,整个程序分为三大部分。其中红色部分为构成表达式的类,蓝色部分为处理表达式并生成对应类的主体程序,橙色为化简部分。程序对表达式处理步骤如下:

  • 首先表达式会在parseExpr()中通过正则表达式分割成多个项
  • 每个项进入parseTerm()后分割成单个因子进入parseFactor()
  • 若因子为非表达式因子,则在此部分直接生成Factor返回parseTerm(),若为表达式因子则将表达式再次传给parseExpr()处理并得到Factor
  • 因子处理完成后返回的FactorparseTerm()经过乘法得到Term中的termResult
  • 处理好的Term的会返回到parseExpr(),根据符号将项添加到Expr
  • SimplifyExpr中的结果进行合并同类相,BuildString生成结果表达式,StringSimplify对生成表达式进行长度优化,得到最终结果。

1.2复杂度分析

image

在方法复杂度分析中可以看出,生成结果表达式,表达式化简这类需要一次或多次遍历整个表达式结果的方法复杂性较高,其次表达式和项的分析方法也具有较高的复杂性(这两种方法对输入的内容进行了遍历和查找)。在合并同类相时,我使用Hashmap作为合并同类项结果的容器,将指数作为key,能够仅遍历一次ArrayList就得到最终结果,这也使得合并同类项的复杂度并不高。

image

通过类的复杂度分析也能得到相似的结论。

1.3正则表达式

第一次作业的因子只有常数、幂函数、表达式因子三种,且不存在括号嵌套问题,使用正则表达式能够正确的捕获需要处理的对象


image

PT用于从表达式中获取项以及位于该项前的运算符号;PF则完成从项中提取因子的任务。正则表达式的书写依据是指导书中的形式化表达。


2.第二次作业分析

第二次作业的因子种类更多,增加了三角函数、求和函数和自定义函数,需要完成多项式的括号展开与函数调用、化简。虽然要处理的对象种类增加了,但是从表达式到项再到因子的分析顺序不需要改变。我认为第二次作业的难点在于新增的三种因子如何处理,计算后的结果如何存储。我的解决方法如下:

  • 为三角函数构造新的类,求和函数和自定义函数转化为表达式后返回表达式因子类。
  • 每个项构建一个ArrayList<Factor>用于存储当前项内的因子,这些因子间的关系是乘法
  • 表达式构建一个ArrayList<Term>用于存储表达式中的项,项之间的关系是加法
  • 对于非表达式的因子之间的乘法,只需要将因子加入所在项的ArrayList<Factor>
  • 对于表达式因子,则需要遍历表达式因子ArrayList<Term>中的每一个Term,将当前项的ArrayList<Factor>与每个Term中的合并添加得到新的ArrayList<Term>作为乘法的结果

经过上述步骤处理后,最终得到的Expr中的每个TermArrayList<Factor>仅包含非表达式因子(即常数、幂函数、三角函数因子),便于后续化简和输出。

2.1程序类图

image
相比第一次作业,本次作业进行了以下增改:

  • 增加了函数读入部分,创造Function类用于存储自定义函数信息
  • 增加更改表达式括号的功能,用于正则表达式匹配
  • 将原本位于Term类内部的乘法方法Multi单独作为一个类
  • 增加FactorSelector用来分辨因子种类并调用该类的创造方法得到Factor

2.2复杂度分析

image

由于方法较多,图片只截取了复杂度较高的方法。复杂度较高的方法为括号替换和函数使用。复杂度较高的方法中都出现了多重循环嵌套和多个条件判断。

image

高复杂度的类都是包含了上述复杂方法的类。

2.3正则表达式

image

第二次作业因子种类增多,使得正则表达式的数量也大量增加。由于我对自定义函数的处理方法为使用因子实参替换形参,为了保证替换的正确性,我对每个实参因子都外加了一层括号,而这也产生了括号嵌套的问题。正则表达式虽然功能强大,但无法处理括号匹配问题。我通过ChangeExprBrackets方法将表达式中表达式因子的最外层的括号()替换为{},并修改表达式因子的正则表达式解决了表达式因子括号匹配问题。但这又带来了新的问题,在sum()函数中的因子可能是表达式因子,若不改变sum()内部表达式因子的括号,则sum()的正则匹配无法正常捕获求和函数因子。因此我又新增了ChangeSumBrackets来改变函数内部表达式因子的括号。通过改变括号,我终于解决了正则匹配括号的问题。也成功创造了两个复杂度超高的方法。

3.第三次作业分析

第三次作业需要完成多层嵌套表达式和函数调用的括号展开与化简。对于我来说这次作业的最大难点已经不再是框架设计、因子处理,而是解决各种因子嵌套带来的正则表达式括号匹配问题。

3.1程序类图

image

第三次的程序与第二次相比在结构上的变化只有一处:

  • 将表达式化简的过程提前到parseExpr返回Expr前,即返回的表达式是合并同类相后的结果。

这样做的原因是:

  1. 由于三角函数内部因子可以是表达式,构造时可能会调用parseExpr并返回Expr。为了便于后续合并同类相和化简时比较两个三角函数是否相等,需要保证其内部因子均是合并同类相后的最简结果

  2. 可以一定程度上提高程序速度

3.2复杂度分析

image

不出所料,两个括号修改方法又荣登复杂度第一和第三的宝座。与第二次相比,其复杂度在经过一定优化后有所下降。parseTerm()方法需要根据返回的因子种类和当前乘法结果ArrayList<Term>进行多次判断,因此复杂度较高。而CreateSumFunction.create()方法需要进行字符串构造,并需要在构造后遍历,修改不符合表达式规范的地方,因此复杂度较高。

image

类的复杂度如上图

3.3正则表达式

image

相比起第二次,第三次的正则表达式略有简化:

  • 由于在输入表达式之前去除了括号,因此正则表达式中的空字符判断全部去除
  • 自定义函数,三角函数,表达式因子允许嵌套,只能通过更改括号匹配,因此括号内部内容可以省略,正则表达式缩短

由于正则表达式不能支持嵌套定义,无法实现括号匹配,因此在第三次作业中,所有出现嵌套结构的因子都需要进行最外层括号替换,且不同因子需替换不同的括号(以避免相互嵌套导致更换后的括号也无法匹配)。于是我选择将三角函数括号换为[],表达式因子换为{},自定义函数换为<>。与第二次作业情况相同,修改这三个因子的正则表达式也会影响其他函数中因子的匹配。除了之前提到的sum()函数,在自定义函数使用时实参因子匹配也会受到影响。对此我的解决办法是:

  • 在自定义函数调用时,先将实参因子通过ChangeBracket类变换括号,成为能够被正则表达式匹配的因子
  • 同时保留一个未被修改括号的实参因子字符串用于生成自定义函数的表达式
  • 由于更改括号不会改变因子的长度和相对位置,通过匹配修改过后的因子就能得到实参因子在字符串中的位置(起始位置+字符串长度)
  • 之后在构建函数表达式时,从未修改的实参因子字符串中直接调用实参因子加入表达式即可

如此便构建出一个符合形式化表达的表达式,能够再次调用parseExpr()分析得到结果。

4.bug分析

4.1第一次作业

第一次作业我在公测和互测中均没有bug产生。

4.2第二次作业

第二次作业我在互测中被测出一个bug。产生bug的位置是表达式化简部分的StringSimplify.changeFirstOp()方法。这个方法的作用是在表达式首个项的符号为-时,从表达式中寻找一个+项替换它,让字符串长度减1。在第一次作业中,我对项的判断方法是在两个+||-号之间。在第二次作业中我直接套用了之前的方法,但忽略了三角函数中的因子为负数时也含有-的问题,导致字符串修改错误。修改方法就是增加括号判断,由于三角函数中的符号一定在括号内,则在判断符号是否为+||-的同时判断当前括号数是否为0即可。

4.3第三次作业

第三次作业我在强测中出现一个bug,在互测中出现两个bug。其中互测的一个bug与强测错因相同。三个bug产生的位置均是ManageTerm类(也就是第三次作业与第二次作业结构不同的地方)。

第一个bug是integrateZero()方法造成的。在这个方法中,我将项内带有0的项全部删除,这使得在一个只有0的表达式中,化简的结果是表达式为空。当这个表达式与别的因子相乘时,表达式为空相当于乘1而不是乘0,由此产生了bug。对此我对解决方法是在返回表达式后,如果结果为空,就替换为一个常数0因子。

第二个bug来自simplify()方法。这个方法的作用是合并同类相。然而在合并过程中,我忽略了项的正负,导致当两个项的正负不同时,合并出的结果会缺少符号,使得结果错误。通过将符号赋给合并项中的常数因子就可以解决这个问题(没有常数因子的项直接添加一个因子再乘符号即可)。

4.4总结

两次作业产生bug的方法和类复杂度均高于平均值。且两次bug产生的原因均是因为原有方法不能适应改变而造成的。这说明在迭代设计过程中,需要明确两个版本之间的差别,并根据差别来判断旧方法是否能继续使用。

5.测试策略

在互测时我会挑选2到3名同学的代码阅读并寻找设计漏洞。如果能够找到设计漏洞,就构造相应的样例进行hack。而当我没有发现别人的设计漏洞时,我会使用以下几类数据进行hack:

  • 我在debug过程中产生错误的数据
  • 特殊数据(如0次幂,求和函数上限小于下限等)
  • 边界数据

这些数据虽然不能保证hack,也不具备根据设计漏洞找到的bug的价值,但也一定程度上反应了程序设计的严谨性和对程序设计过程中细节的把控。

6.架构设计体验

三次作业的整体架构其实在第一次作业时已经基本确定。从整体架构来看,第一次作业中,整个程序分成三个部分:解析表达式,化简结果,输出表达式。而二三次作业我也沿用了这样的设计架构。

在作业的迭代过程中,变动最大的更多是具体方法的实现过程。第一次到第二次作业,我重写了乘法、加法、合并同类项的方法以及正则表达式,添加了三角函数、求和函数、自定义函数的构造方法;而第二次作业到第三次作业,我重写了括号替换、正则表达式和多种因子的构造方法。在具有架构的前提下,这些重写更多是为了适应新的需求,提高方法的效率,重写的过程也相对容易(因为方法的功能没有发生改变)。

7.心得体会

这三周设计经历让我有了以下体会:

  • 不要被眼前的任务吓倒。当第一次看到作业指导书,面对各种复杂的定义和要求,我感到有些无力,甚至有些绝望。在焦虑了一晚之后,我根据网站提供的教程逐渐明晰了自己的设计思路,并将任务细分到每一天。虽然在每天的编程中都会遇到新的问题,但每一个方法的成功实现都会激励我继续努力,最终成功完成了作业。
  • 迭代开发中设计眼光要放长远。第一次作业由于不存在多层括号嵌套,我使用了正则表达式的方法来匹配项和因子。但当时并没有考虑到正则在多层括号嵌套时匹配括号的问题,给自己挖了个大坑。而第二次作业的修改括号思路让我找到了解决类似问题的共性方法,并成功沿用到第三次作业。
  • 充分测试,精益求精。由于没有进行自动化测试,自己构造的样例也没有对程序的各个功能进行完整的测试,我的第二、三次作业都出现了bug,留下了小小的遗憾。希望在下次作业中能够对程序进行更加充分的测试,提前发现bug,解决bug。
posted @ 2022-03-25 17:56  yysrW  阅读(42)  评论(2编辑  收藏  举报