OO_Unit1_单元总结

OO_Unit1_单元总结

part 0 综述

面向对象课程第一单元作业的主题是对带有括号的表达式进行化简,最终化简到只含有必要括号的形式。从第一次作业的仅含有幂函数因子、常数因子和表达式因子到第二次作业加入三角函数因子、自定义函数因子和求和函数因子,再到第三次作业加入了支持嵌套因子的三角函数。

在不断的迭代开发中,对面向对象和层次化设计的理解不断深入。同时也认识到自己代码在结构上的不足,没有真正做到“高内聚,低耦合”,代码内的循环嵌套较多,模块间的数据耦合和函数耦合很强,类与类之间分工不明确等诸多问题。也因此导致了公测阶段debug过程的艰难,以及在强测和互测中出现了此前未曾发现的bug。因为第一单元正是“重结构,轻算法”的一个单元,我将重点反思自己不好的体系结构。

part 1 体系结构

结构的演进

由于我使用的是预处理的读入方式,所以相较于使用正常方式读入的同学省去了处理读入的表达式以及将自定义函数和求和函数带入表达式的过程。预处理的读入方式相当于将表达式由高向低解析完毕之后,再从低到高回溯计算过程,并且在读入过程中不会出现自定义函数和求和函数而是将它们转化为加法、减法、乘法和乘方运算。我的基本思路就是每读入一项就进行一次去括号处理,使存放函数的容器内存入的每个函数(f1,f2,f3,...,fn)都是已经去了括号之后的函数。最终得到我们的目标函数fn后,再对其进行优化和输出。

第一次作业仅含有幂函数因子和常数因子,故化简后得到的最简变量是\(a\cdot x^b\),我将最简变量这个类定义为Factor,定义函数的类为FunctionFunction中含有存放Factor的容器。

在第二作业中,最大的改动就是在因子中加入了三角函数因子,故化简后的最简因子由\(a\cdot x^b\)变成了\(a\cdot x^b\cdot \prod sin^c\cdot \prod cos^d\)(其中三角函数内部是因子为常数或幂函数),于是我新建了Circular类来定义三角函数因子,并在Factor类中加入了存放Circular的容器。

第三次作业中,出现了允许嵌套因子的三角函数。虽然最简因子依然是\(a\cdot x^b\cdot \prod sin^c\cdot \prod cos^d\)这种形式,但是三角函数内的因子已经不再是简单的常数或者幂函数了,而是Function类型的因子。所以Circular类中的因子类型由常数和幂函数换作为Function类型。

类图及说明

以最为完备的第三次作业的类图举例


其中,Parser类用于处理预解析模式读入的字符串,将每一行处理得到:目标函数标签、运算类型、操作数1和操作数2。如果操作数是常数因子或x,则建立新的Factor来存储这个操作数,并将这个Factor存入到新建的Function中;如果操作数是之前出现过的函数,则会在存放所有已有函数的HashMap中通过搜索函数标签,找到对应的Function。由此我们可以得到一个或者两个Function作为操作数,接下来通过识别运算符来进行计算。

Calculate类中包含了所有的运算。乘方的运算我将其转化为多次乘法运算,Function之间的乘法运算又可以转化为Factor之间的乘法运算。最终所有运算都会返回一个Function,而这个Function就是未经过化简的当前标签的函数。

其中FunctionFactorCircular是层次由高到低的存储结构。Factor中含有存放Circular的容器,Function中含有存放Factor的容器,Circular中含有Function作为自己的内部因子,构成了一个循环的结构。

Print类是主要负责输出的一个类,我并没有将toString方法写入到FactorCircular,而是将所有不同类型变量的输出方法都写入了Print类中。

化简方式

在化简方面,我主要实现了同一Factor中含有相同因子的三角函数的合并,Function中只有系数不同的Factor间的合并,还有三角函数内部负号的提出以及特殊三角函数值sin(0) = 0,cos(0) = 1的化简。

在同一Factor中相同因子的三角函数合并的实现中,最重要的任务就是要判断两个Circular中的Function是否相同。而判断两个Function是否相同需要调用判断两个Factor是否相同的方法。判断Factor相同又需要判断其中包含的三角函数容器中的元素是否完全相同,进一步需要判断两个三角函数是否完全相同。

通过以上分析,为实现合并我在Function中写了方法

+isEqual(a:Function):boolean

Factor中写入方法

//判断Factor除系数外是否相等(用于合并同类项)
+isEaqal(a:Factor):boolean
 //判断Factor相等(完全相等)
+isEqualp(a:Factor):boolean

Circular中写入方法

//判断三角函数内因子是否相同
+isEqual(a:Circular):boolean
//判断整个三角函数因子是否相等(相比于上一个增加了对外层指数的判断)
+isEqualp(a:Circular):boolean

part 2 复杂度分析

类复杂度分析

OCavg:每个类中的平均圈复杂度

OCmax:每个类中最大圈复杂度

WMC:每个类中方法的总圈复杂度

可以看到Factor类中总圈复杂度较高,Parser类和Print类的平均圈复杂度较高。

Parser类中我使用了大量的if语句来查找和获取两个操作数、判断对应的运算方式,进而大致了大量的分支。在Print类中同样如此,由于没有在每个类中分别实现toString()方法,所以相当于将toString()全部实现在了Print类中。而为了判断哪里需要加*,哪里需要加(),又加入了很多if语句。显然这也增加了Print类与Factor类和Circular类的耦合程度并不利于模块的扩展。Factor类堆叠了很多方法,如判断三角函数容器内元素是否完全相等的方法与Factor类内部联系很低,将这个方法置于Factor内部并不符合“高内聚”的原则。

方法复杂度分析

(1)圈复杂度(Cyclomatic Complexity (v(G)))
概念: 用来衡量一个模块的复杂程度

(2)基本复杂度(Essential Complexity (ev(G)))
概念: 用来衡量程序非结构化程度

(3)模块设计复杂度(Module Design Complexity (iv(G)))
概念: 用来衡量模块之间的调用关系复杂度越高,模块之间耦合性越高,越难隔离,维护和复用。

与类复杂度分析中对Parser类和Print类的一样,Print类中三个方法中的两个和Parser类中的parserStr()方法都有着很高的圈复杂度和模块设计复杂度。表明不仅方法内部的复杂度较高,并且模块间的复杂度很高,模块之间的耦合性很高,难以进行维护和复用。

除此之外,mergeFac(),calOper(),isSimple()方法也存在与其他模块耦合程度较高的问题,这是由于没有划分好各模块的功能导致了各模块间方法的复杂调用。

part 3 BUG与反思

出现的BUG

第一单元训练中的BUG集中出现在了第三作业

1.通过双层循环遍历容器时,如果找到后一个元素可以和之前的元素进行合并,那么将后一个元素合并进前一项,remove掉后一项的内容。但是没有考虑到类似于\(f_2\quad sub\quad f_1\quad f_1\)的情况。此时容器内的两个元素指向同一个内存对象,如果删去后一个元素则会将前一个也删除掉。

修改方法:在subFun增加判断方法,判断两个元素是否指向同一个对象。如果指向同一个对象直接返回只含BigInteger.ZEROFunction

if (a.equals(b)) {
            Function temp = new Function();
            temp.getFactors().add(new Factor(BigInteger.ZERO));
            return temp;
        }

2.输出格式不符合语法。没有正确判断三角函数内部的因子是否为“简单因子“(即为常数因子、幂函数因子或者三角函数因子)。导致出现输出\(sin(x\cdot cos(x))\)的错误。

3.没有考虑出现\(f_1\quad 123\)的情况。在强测和互测中出现了sum函数下边界大于上边界的数据,此时转换程序会将sum函数转化为\(f_1\quad 0\),没有考虑这种情况会直接返回0程序结束,而实际上还会有后续的运算。

反思

本单元作业正是强调好的体系架构的一个单元。但是我并没有在这方面做好,通过类复杂度分析中较高的平均圈复杂度和总圈复杂度、个别方法极高的圈复杂度和模块设计复杂度即可见一般。在最初的设计过程中并没有非常仔细的划分各个模块的具体功能,很多情况下是在写的过程中想到了应该有一个什么什么样的方法就找了一个看起来比较合适的类里写下了这个方法。如我没有在Factor类和Circular类中写toString()方法,而是将存储类型向字符串转化的方法都写入了Print类中,不仅增加了方法本身的复杂性容易出现bug,还增加了模块与模块之间的耦合程度。如果继续对结构进行迭代,则Print类无疑要进行很大的改动。

化简过程中,还出现了大量需要判等的情况,如FunctionFunction是否相等,FactorFactor是否相等,三角函数容器内元素间是否完全相等。不同的判断判等情况会相互之间进行调用,提高了模块与模块之间的耦合度。

在化简的过程中,我并没有使用HashMap来存储元素,而是使用了ArrayList进行存储。导致在化简时,需要通过双层循环来遍历容器,并进行合并和删除的操作,在遍历过程中删除元素又极易出现问题。出现的第一个bug就是在这种情况下出现的。

part 4 心得体会

通过第一单元的作业,我独立完成了一个并不完美的面向对象的程序,在开发和迭代的过程中加深了对面向对象内涵的理解。明白了面对复杂问题,各模块明确的职能划分,之间清晰简洁的接口;模块内部有序的组织方式,无疑会使我们的程序更容易维护,更容易定位bug,更容易扩展。这也是我在未来努力的方向。

因为存在诸多不足,下一阶段的学习要加大投入,积极思考,吸取他人所长,向着更好努力。

posted @ 2022-03-26 11:22  Selabarsayes  阅读(37)  评论(1编辑  收藏  举报