面对对象 第一单元总结

面对对象 第一单元总结

设计分析

第一次作业

总体架构

Main类负责读入与输出,将读入的字符串传入方法类Regexfunc中,Regexfunc类专门用于正则表达式的处理,返回该字符串经过解析后的表达式Expression;在Expression中含有一个Polyitem的hashmap,用于保存表达式的每一项;而每一个Polyitem又含有一个Factor,作为项的因子。

类的分析

度量分析

下表是对每一个方法所取的度量分析,除了求导和正则表达式解析两个方法认知复杂度较高外,其他均在一个较低的水平上。

Method CogC ev(G) iv(G) v(G)
Expression.Expression() 0 1 1 1
Expression.addPolyitem(Polyitem) 2 1 2 2
Expression.derivative() 4 1 3 3
Expression.toString() 23 2 12 13
Factor.Factor() 0 1 1 1
Factor.Factor(BigInteger,BigInteger) 0 1 1 1
Factor.getCoefficient() 0 1 1 1
Factor.getIndex() 0 1 1 1
Factor.mult(Factor) 0 1 1 1
Factor.setCoefficient(BigInteger) 0 1 1 1
Factor.setIndex(BigInteger) 0 1 1 1
Main.main(String[]) 3 1 2 3
Polyitem.Polyitem() 0 1 1 1
Polyitem.Polyitem(BigInteger,BigInteger) 0 1 1 1
Polyitem.addFactor(Factor) 0 1 1 1
Polyitem.getFactor() 0 1 1 1
Polyitem.setFactor(Factor) 0 1 1 1
Regexfunc.getitem(String) 33 1 16 16

下表为对每一个类的度量分析,可见复杂度最高的是Expression和Regexfunc,这与上面的分析相同,是由于其中的求导方法和正则表达式解析较复杂导致的。

Class OCavg OCmax WMC
Expression 4.75 13 19
Factor 1 1 7
Main 3 3 3
Polyitem 1 1 5
Regexfunc 12 12 12
类图分析

  • Main

出于解耦的设计,Main只负责读入字符串与输出结果。

  • Regexfunc

这个类是一个处理正则表达式的方法类,只有一个用于解析字符串并返回表达式的方法。

下面是正则表达式的写法,根据题目中所给形式化表述,从下往上写,即处于上面的正则表达式可以“调用”下面的正则表达式:

String addorsub = "[+-]";//加减
String white = "[ \\t]";//空白
String whiteItem = "(" + white + "*" + ")";//空白项
String integer = "(" + "[0-9]+" + ")";//整数
String signedInteger = "(" + addorsub + "?" + integer + ")";//带符号整数
String powerfunction = "(" + "x" + "(" + whiteItem + "\\*\\*" + whiteItem + signedInteger + ")?" + ")";//...
...

最终得到的正则表达式如下所示:

  • Expression

这个类是根据题目,表达式由一系列的项相加得到,因此在其中的属性只有一个,即一个用来储存表达式各个项的hashmap。另外由于在这个类中即可获得所有项,因此将求导方法写在这个类中。

  • Polyitem

这个类是项类,其中保存了项中的因子。因此其属性只有一个Factor类,对应其因子。其方法只有一个非常简单的添加Factor的方法addFactor。

  • Factor

这个是因子类,其中保存了一个因子对应的属性:系数coefficient、指数index。在这个类中有一个额外的mult方法,用于将两个Factor相乘得到结果因子Factor。

优缺点分析
  • 优点
  1. 各个部分解耦,将每一个需要完成的功能设计分开至不同的类中,有利于后续的扩展。
  2. 由于解耦的设计,在debug时只需要观察出是哪一个功能部分出现了问题,即可快速定位到项目的相关地方,调试快速。
  • 缺点
  1. Factor与Polyitem实际上是等同的,即实际上不需要Polyitem,Polyitem中仅仅有一个Factor,这就给Expression求导时带来了一定的麻烦——需要先获取到Polyitem再获取到Factor,影响项目运行的效率。
  2. 正则表达式的解析写法较为混乱,在进行debug时耗费大量时间。

第二次作业

总体架构

Main类负责读入与输出,将读入的字符串传入NodeFactory得到表达式,其中Nodefactory是专门用于表达式解析的方法类,之后NodeFactory将创建的各种Node节点组成表达式树并返回根节点。其他的类都直接继承或间接继承Node,Node引出了两种类型的节点:Relation和Factor,分别代表表达式树中的关系和因子。关系又分为加法关系AddRelation和乘法关系MultRelation,因子又分为三角因子TrigFactor、幂因子PowerFactor、常数因子ConstFactor。

表达式解析

由于表达式存在嵌套关系,而正则表达式仅仅等价于一个有限状态机,无法完成对存在不确定嵌套次数的表达式匹配,因此使用下推自动机,即添加一个栈来配合正则表达式,完成表达式的解析。

下面是我的表达式读入的流程图:

getNode(String input)

首先将表达式作为参数传入方法getNode中,这个方法返回一个新的节点作为表达式。这个方法的第一步是把表达式中的最外层小括号改为大括号,这里我使用了一个简化的栈进行这个操作,由此即可实现把下一层的表达式因子通过正则表达式识别出来,在通过循环匹配后,把每一个项分割出来,传递给下一个方法 getItem。

eg:当字符串x+(x**2+1+sin(x))*x**2读入后,经过repalceBracket后变成x+{x**2+1+sin(x)}*x**2,这样匹配时就可以将表达式因子提供正则表达式的方式识别出来。另外,由于sin和cos的括号也会被进行相同的操作,我这里已经将sin和cos的匹配已经变成了sin{x}或sin(x)。

getItem(String input)

在经过getNode的处理后,这个方法负责把项对应的节点返回。首先是进行进一步的分割,通过正则表达式把项中的因子识别并分割,之后传给getFactor方法,得到项所对应的节点并返回。

getFactor(String input)

这个方法通过工厂模式的形式,把因子所对应的节点返回。首先是判断这个因子是否是表达式因子,如果是表达式因子的话,将该因子的字符串传给getNode获得该表达式的节点,其他情况均返回其对应的因子类型。

类的分析

度量分析

下表是对每一个方法所取的度量分析,除了表达式输出的toString方法认知复杂度较高外,其他均在一个较低的水平上。

Method CogC ev(G) iv(G) v(G)
AddRelation.AddRelation(Node,Node) 0 1 1 1
AddRelation.derivative() 0 1 1 1
AddRelation.normalize() 6 5 3 5
AddRelation.toItem() 11 7 7 10
AddRelation.toString() 0 1 1 1
ConstFactor.ConstFactor(BigInteger) 0 1 1 1
ConstFactor.derivative() 0 1 1 1
ConstFactor.getNumber() 0 1 1 1
ConstFactor.normalize() 0 1 1 1
ConstFactor.setNumber(BigInteger) 0 1 1 1
ConstFactor.toItem() 0 1 1 1
ConstFactor.toString() 0 1 1 1
Item.Item() 0 1 1 1
Item.Item(BigInteger,BigInteger,BigInteger,BigInteger) 0 1 1 1
Item.addItem(Item) 0 1 1 1
Item.derivative() 0 1 1 1
Item.getCoef() 0 1 1 1
Item.getCosIndex() 0 1 1 1
Item.getPowerIndex() 0 1 1 1
Item.getSinIndex() 0 1 1 1
Item.multItem(Item) 0 1 1 1
Item.normalize() 0 1 1 1
Item.setCoef(BigInteger) 0 1 1 1
Item.setCosIndex(BigInteger) 0 1 1 1
Item.setPowerIndex(BigInteger) 0 1 1 1
Item.setSinIndex(BigInteger) 0 1 1 1
Item.toItem() 0 1 1 1
Item.toString() 31 1 12 16
Main.main(String[]) 3 1 3 4
MultRelation.MultRelation(Node,Node) 0 1 1 1
MultRelation.derivative() 0 1 1 1
MultRelation.normalize() 12 7 9 13
MultRelation.toItem() 13 7 11 15
MultRelation.toString() 0 1 1 1
NodeFactory.getFactor(String) 10 7 8 10
NodeFactory.getItem(String) 3 1 4 4
NodeFactory.getNode(String) 1 1 2 2
PowerFactor.PowerFactor(BigInteger) 0 1 1 1
PowerFactor.derivative() 3 3 3 3
PowerFactor.getIndex() 0 1 1 1
PowerFactor.normalize() 0 1 1 1
PowerFactor.setIndex(BigInteger) 0 1 1 1
PowerFactor.toItem() 0 1 1 1
PowerFactor.toString() 1 2 1 2
RegexFunction.replaceBracket(String) 10 1 6 6
Relation.Relation(Node,Node) 0 1 1 1
Relation.getSubNodeA() 0 1 1 1
Relation.getSubNodeB() 0 1 1 1
Relation.setSubNodeA(Node) 0 1 1 1
Relation.setSubNodeB(Node) 0 1 1 1
TrigFactor.TrigFactor(TrigType,BigInteger) 0 1 1 1
TrigFactor.derivative() 7 4 5 5
TrigFactor.getIndex() 0 1 1 1
TrigFactor.getTrigType() 0 1 1 1
TrigFactor.normalize() 0 1 1 1
TrigFactor.setIndex(BigInteger) 0 1 1 1
TrigFactor.setTrigType(TrigType) 0 1 1 1
TrigFactor.toItem() 1 2 1 2
TrigFactor.toString() 1 2 1 2

下表为对每一个类的度量分析,可见复杂度最高的是RegexFunction和NodeFactory,这是由于对表达式处理时的复杂性所导致的。其他类均在一个较为合理的水平上。

Class OCavg OCmax WMC
AddRelation 3 7 15
ConstFactor 1 1 7
Item 1.81 14 29
Main 3 3 3
MultRelation 3.4 7 17
NodeFactory 4.33 8 13
PowerFactor 1.43 3 10
RegexFunction 6 6 6
Relation 1 1 5
TrigFactor 1.67 5 15
类图分析

  • Main

出于解耦的设计,Main只负责读入字符串与输出结果。

  • NodeFactory

这个类是工厂类,用于根据输入的表达式字符串返回相对应的表达式。其具体操作已经在上面的表达式解析中详细说明了。

  • AddRelation

这个类是一个加法关系,其储存了两个子节点,这两个子节点可以是任何Node,求导操作也非常简单,返回一个新的加法关系,其中两个子节点为之前的两个子节点的求导。

  • MultRelation

这个类是一个乘法关系,其储存了两个子节点,这两个子节点可以是任何Node,求导操作也非常简单,返回一个新的加法关系,其中两个子节点为两个乘法关系,其中一个乘法关系为子节点A与子节点B的求导,另一个为子节点A的求导与子节点B。

  • xxxFactor

3种因子,分别储存了其应含有的属性,TrigFactor有trigType和index,分别表示三角函数的类型(sin还是cos?)和三角函数的指数;PowerFactor有index,表示指数;ConstFactor有num,表示常数大小。

在拥有上面5种类后,我们即可将一个表达式树构建出来。如1+x*(x*sin(x)+1)+x**2可表示为:

  • Item

这个是专门用于优化的类,他包含了4个属性:coef、powerindex、sinindex、cosindex。即这个item可以把多个乘积合并为一个项,每一个节点都进行toItem()操作后,将把所有的节点替换为Item,由此实现优化操作。

优缺点分析
  • 优点
  1. 采用了表达式树的数据结构,每一个节点实际上都是等价的,符合面对对象的思想,也十分有利于程序的扩展,即只需要添加新的节点类型即可扩展新的类型。
  2. 所有节点都实现了求导方法derivative(),在进行求导时只需要对根节点进行调用derivative(),后续求导操作都会递归调用,完成表达式的求导。
  • 缺点
  1. 在优化时效率不高,需要遍历所有节点且优化程度低,基本的拆括号等等操作都没有实现。
  2. 仅仅采用了二叉树,出现大量嵌套时,表达式树会出现层数非常多的情况,不利于操作。

第三次作业

总体架构

这次作业加入了三角函数内可包含表达式因子,在第二次的基础上稍加修改即可,为三角函数类TrigFactor添加一个新的属性var,类型为Node,作为sin中的变量。表达式的读入基本与第二次相同。另外题目还要求了格式检查,而由于我是使用正则表达式进行表达式读入的,一旦输入不能与正则表达式匹配,说明格式有误。另外我对第二次的架构进行了稍微的修改,将之前的二叉树表示表达式树更改成了多叉树,这样可以提高表达式的化简效率,降低化简的难度。

表达式解析

在第二次的基础上进行更新,添加了TrigFactor中因子的判断,其他与第二次一致。

格式检查

格式检查与表达式的解析实际上是一体的,仅仅添加一点判断即可。

表达式因子判断

在每一个表达式的两边判断是否有括号来检查sin里表达式因子是否合法,为此,在一开始读入字符串时,在两边加上括号后再传入getNode,如此处理后可以保证所有的表达式最外面都有一对括号,否则不合法。

其他格式判断

对于其他的格式,使用matcher.lookingAt(),保证每次匹配都是从第一个字符开始匹配,如果在匹配之后发现字符串不为空,说明该字符串没有成功匹配,即为错误格式。

优化

对于这部分我只采用了合并常数项的操作,原因是经常出现的0和1往往会大大增加长度,因此将加法关系中的所有常数项化为一项,将乘法关系中的常数也相乘在一起。与第二次不同,由于使用了多叉树,不用去想方设法地把不在一层的同类因子合并在一起,另外也不用担心因为多层嵌套导致的多余的括号无法轻易消除。

类的分析

度量分析

下表是对每一个方法所取的度量分析,除了表达式输出的两种关系的简化方法toItem()认知复杂度较高外,这是由于在化简时需要判断当前节点的类型,并进行相应的处理造成的,其他均在一个较低的水平上。

Method CogC ev(G) iv(G) v(G)
AddRelation.AddRelation() 0 1 1 1
AddRelation.AddRelation(ArrayList) 0 1 1 1
AddRelation.derivative() 1 1 2 2
AddRelation.toItem() 56 12 16 16
AddRelation.toString() 1 1 2 2
ConstFactor.ConstFactor(BigInteger) 0 1 1 1
ConstFactor.add(Factor) 0 1 1 1
ConstFactor.derivative() 0 1 1 1
ConstFactor.getNumber() 0 1 1 1
ConstFactor.setNumber(BigInteger) 0 1 1 1
ConstFactor.toItem() 0 1 1 1
ConstFactor.toString() 0 1 1 1
Main.main(String[]) 2 1 2 3
MultRelation.MultRelation() 0 1 1 1
MultRelation.MultRelation(ArrayList) 0 1 1 1
MultRelation.derivative() 7 1 4 4
MultRelation.toItem() 58 14 18 18
MultRelation.toString() 3 3 2 4
NodeFactory.getFactor(String) 20 11 11 14
NodeFactory.getItem(String) 6 3 5 7
NodeFactory.getNode(String) 7 4 5 8
PowerFactor.PowerFactor(BigInteger) 0 1 1 1
PowerFactor.add(Factor) 0 1 1 1
PowerFactor.derivative() 3 3 3 3
PowerFactor.equals(Object) 3 3 2 4
PowerFactor.getIndex() 0 1 1 1
PowerFactor.setIndex(BigInteger) 0 1 1 1
PowerFactor.toItem() 0 1 1 1
PowerFactor.toString() 1 2 1 2
RegexFunction.replaceBracket(String) 10 1 6 6
Relation.Relation() 0 1 1 1
Relation.Relation(ArrayList) 0 1 1 1
Relation.addNode(Node) 0 1 1 1
Relation.deleteNode(Node) 0 1 1 1
Relation.getNodeArrayList() 0 1 1 1
Relation.multNode(Node) 8 5 6 6
Relation.setNodeArrayList(ArrayList) 0 1 1 1
TrigFactor.TrigFactor(TrigType,BigInteger,Node) 0 1 1 1
TrigFactor.add(Factor) 0 1 1 1
TrigFactor.derivative() 9 2 5 5
TrigFactor.getIndex() 0 1 1 1
TrigFactor.getTrigType() 0 1 1 1
TrigFactor.getVar() 0 1 1 1
TrigFactor.setIndex(BigInteger) 0 1 1 1
TrigFactor.setTrigType(TrigType) 0 1 1 1
TrigFactor.setVar(Node) 0 1 1 1
TrigFactor.toItem() 7 4 2 5
TrigFactor.toString() 3 2 2 3

下表为对每一个类的度量分析,可见复杂度最高的仍是RegexFunction和NodeFactory,这是由于对表达式处理时的复杂性所导致的。其他类均在一个较为合理的水平上。

Class OCavg OCmax WMC
AddRelation 3.83 16 23
ConstFactor 1 1 8
Item 1.81 14 29
Main 2 2 2
MultRelation 4.83 18 29
NodeFactory 7.33 12 22
PowerFactor 1.5 3 15
RegexFunction 6 6 6
Relation 1.57 5 11
TrigFactor 1.83 5 22
类图分析

  • Main

出于解耦的设计,Main只负责读入字符串与输出结果。

  • NodeFactory

这个类是工厂类,用于根据输入的表达式字符串返回相对应的表达式。其具体操作与第二次基本相同。

  • Relation

在这次作业中由于将二叉树修改为了多叉树,因此将之前的subNodeA与subNodeB修改为了ArrayList,并修改了相应的对ArrayList操作的方法。

  • AddRelation

这个类是一个加法关系,其储存了一个ArrayList,用于保存其子节点,这些子节点可以是任何Node,求导操作也非常简单,返回一个新的加法关系,包含每一个节点的求导结果。

  • MultRelation

这个类是一个乘法关系,其储存了一个ArrayList,用于保存其子节点,这些子节点可以是任何Node,求导操作也非常简单,返回一个新的加法关系,包含每一个节点的求导结果与其他节点相乘的乘法关系节点。

  • xxxFactor

在第三次作业中,TrigFactor添加了一个var,如下图所示,var作为sin或cos中的变量,即sin(var),其余类型的factor没有改变。

优缺点分析
  • 优点
  1. 将二叉树修改为了多叉树,使程序的递归次数减少,提高了程序的运行效率。
  2. 去除了不必要的类Item,简化了优化操作。
  • 缺点
  1. 对于除了常数因子的其他因子没有进行同类项合并,导致优化效果不佳。

Bug分析

第一次作业

  1. 读入时忽略了表达式的加减号,如果第一个是减号,我的程序将忽略该减号,导致求导结果错误

    • bug出现位置:Regexfunc.getitem(String)
    • 方法度量分析:

    如下表所示,可以分析该bug出现在全项目中各种复杂都最高的类和方法中

    Method CogC ev(G) iv(G) v(G)
    Regexfunc.getitem(String) 33 1 16 16

第二次作业

  1. 在将转化后的Item输出时,没有考虑到toString方法可能出现输出xxxx*-sin(x)的情况,即输出格式错误

    • bug出现位置:Item.toString()

    • 方法度量分析:

    如下表所示,可以分析该bug出现在全项目中各种复杂都最高的类和方法中

    Method CogC ev(G) iv(G) v(G)
    Item.toString() 31 1 12 16

第三次作业

  1. 在进行优化操作toItem()时,如果最后优化到该节点返回空时(如对1-1进行优化),在toString中没有考虑到该节点为空的情况,而是直接选取了ArrayList的第一个元素输出,这就导致了越界。

    • bug出现位置:
    • 方法度量分析:AddRelation.toItem()、MultRelation.toItem()

    如下表所示,可以发现两个方法都有很高的复杂度。

    Method CogC ev(G) iv(G) v(G)
    AddRelation.toItem() 56 12 16 16
    MultRelation.toItem() 58 14 18 18

总结

这三次的bug都发生在方法复杂度很高的地方,而在复杂度低的方法中往往不容易出现bug,这提醒我们在写方法时一定要注意方法的简易性、可读性、解耦性,否则bug往往就出现在这些没有严格按照规则编写的方法中。

Hack策略

肉眼Hack法

实际上肉眼很难找到别人的bug,特别是当一份程序写的很乱,同时没有注释时。不过我们可以从下面几点开始:

  1. 从表达式解析入手,将其表达式的正则表达式与自己进行比较,发现其中的不同之处,再思考两种写法是否等价,也可以再正则表达式可视化中进行比较。
  2. 观察求导的逻辑,是否是自洽的。
  3. 观察输出方法,是否可能在输出时出现非法格式的情况,考虑极端条件。
  4. 观察递归次数,是否可能由于递归次数多而无法处理大量嵌套的数据。

数据自动生成器

为了减少肉眼debug的工程量,通过自己写的正则表达式,通过python逆向生成数据,对于后续的存在递归的形式,采用逆向递归向下的方法,继续采用正则表达式的逆向生成,生成数据。由于正则表达式是严格按照形式化表述写的,因此数据的覆盖性有一定的保证。相比之下,这种方法比肉眼Hack法有效多了,因为仅仅看代码,很有可能跟着别人的书写思路走,最后没有发现问题所在,而大量的自动构造数据,可以找出一些意想不到的bug。

重构总结

总的来说,这个单元作业经过了一次大重构和一次小重构,其中从一到二基本上改变了整个项目的框架,在从二到三仅仅改了一点点,即将二叉树修改为了多叉树。

从一到二

储存结构

将第一次作业中的数组储存修改为了二叉树表达式树储存。

类图

第一次作业没有继承关系,也没有将常数因子和幂因子分开,求导也是直接在表达式中进行;在第二次作业中,将表达式作为一个表达式树处理,因此表达式树中的所有节点都继承了Node类,同时将各个类型的因子分开处理,并将求导操作作为接口,分散至每一个类型分别实现,在求导操作时只需要调用该类的求导方法即可。

度量分析

第一次作业的圈复杂度平均值为2.78,而第二次作业为2.42,可见在重构之后,程序的圈复杂度降低了,更加符合面对对象的思想了。

从二到三

储存结构

将第二次作业中的二叉树储存修改为了多叉树表达式树储存。

类图

删去了Item类,同时将Relation中的属性修改为了ArrayList,此外类图基本一致,没有做太多的修改。

度量分析

第一次作业的圈复杂度平均值为2.42,而第二次作业为2.56,圈复杂度变高是因为将优化的操作主要放在了AddRelation和MultRelation中,这导致了该方法变得十分复杂,拉高了整体的圈复杂度。

心得体会

心态篇

在这单元作业中,我第二次作业在提交前5小时才开始进行优化操作,心中难免有点紧张,很多地方都没有细想,直接开始修改代码,最后结果便是没有优化成功,以后要摆好心态,在ddl之前也要先细想之后再开始实现代码。

能力篇

这次是我第一次写面对对象的较大的程序,慢慢从第一次作业的不太面对对象转变到了能够写出符合面对对象的程序。另外作业的迭代开发,也让我学习到了如何为下一次扩展打好基础,以免大规模的重构。

另外这也是我真正第一次将数据结构的知识运用在自己的程序中,对比之前的项目,都是很多无脑for循环加ifelse,这次慢慢将数据结构的思想实现在了程序中。

posted @ 2021-03-26 21:15  BUAA-YiFei  阅读(390)  评论(1编辑  收藏  举报