OO第一单元总结

一、第一次作业

1、类图

优点:结构清晰简单,每个类的规模适中

缺点:Expr、Term、Var类在进行计算时都需要获取HashMap,可以将这个方法抽象到接口Factor中(让Term也实现Factor接口)

2、设计思路

  • 读取、预处理

首先在MainClass中读取表达式字符串,利用preProcessing方法进行预处理:处理空白字符、处理**+和*+、处理三+-相连、处理两+-相连、处理带括号的乘方(将乘方变为若干个括号相乘)

  • 递归下降解析表达式字符串

将读取的表达式字符串传入词法分析器Lexer,再将Lexer传入解析器Parser,递归下降调用Parser的parseExpr、parseTerm、parseFactor方法,对表达式字符串进行解析

  • 计算最终结果
    • 由于此次作业只涉及到常数和幂函数,所以使用HashMap<Integer, BigInteger>结构来表示最终结果,其中key为指数、value为系数

    • 合并同类项使用HashMap的merge功能(非常好用!

      newTermVars.merge(newKey, newValue, BigInteger::add);
      
    • 解析与计算同步进行,在将某个Term装入Expr的ArrayList<Term>中时,计算该Term的HashMap,并将该HashMap与Expr中的exprVars合并(将Factor装入Term的ArrayList<Factor>中同理)

  • 输出

因为解析与计算同步进行,所以当递归下降解析完毕后,返回的expr中的exprVars即为最终结果。在Expr中重写toString方法将exprVars输出即可

3、指标度量分析

(1)总代码规模
(2)类复杂度和方法复杂度
  • 类复杂度

OCavg:类平均圈复杂度,继承类不计入

WMC:类总圈复杂度

  • 方法复杂度

ev(G):非抽象方法的基本复杂度,度量一个方法的控制流结构

iv(G):设计复杂度,度量方法控制流与其他方法之间的耦合程度

v(G):圈复杂度,度量每个方法中不同路径执行的数量

其中Expr类中的toString方法的复杂度较高,原因是toString方法中为了输出更短的结果而进行了多次if-else判断特殊情况。Lexer.getVar()和Parser.parseFactor()也存在类似的问题。

二、第二次作业

1、类图

优点:解析、计算、输出三个部分结构清晰

缺点:由于单独设置了一个统一形式的类Unified(最终计算结果可以表示为HashSet<Unified>),该类的方法数量较多;由于Expr没有指数这一属性,所以在Term中添加Factor时,可能需要将某个Expr添加多次

2、设计思路

这次作业在上次作业的基础上增加了自定义函数、三角函数、求和函数,使得表达式字符串的结构复杂度瞬间提高不少,需要对之前的程序进行修改。

  • 之前用于表示最终结果的HashMap<Integer, BigInteger>不再适用,故在这次作业中新增了一个统一形式的类Unified,其包含指数、系数、sin、cos。对于sin、cos的处理,本次作业限制了sin、cos的括号里面只能是常数和幂函数,且sin、cos整体可以带指数。当时觉得三个变量无法使用HashMap进行处理,便只好采用IdentityHashMap存储sin、cos的相关信息。(IdentityHashMap允许key的值相同。但此处注意,比方说key为Integer类型的变量,IdentityHashMap允许两个Integer类型的变量值一样,但是不允许这两个变量其实是同一个变量
    • 后来发现可以使用HashMap<HashMap<Integer, BigInteger>, Integer>进行sin、cos信息的存储,且sin、cos的括号里面甚至可以为多项式
    • IdentityHashMap由于允许key的值相同,所以对于含有指数的sin、cos来说,会在IdentityHashMap中存储多个相同sin、cos,导致输出不简洁,长度过长
  • 增加了CusDefine类用于存储自定义函数定义的相关信息,增加了Customize类来存储自定义函数调用的相关信息,且将所有的自定义函数定义的CusDefine类存储在了Customize类的CusDefs属性(static)中
  • 增加了Sum和Tri类分别记录求和函数和三角函数的相关信息
  • 将解析、计算、输出三者分开,增加了Arithmetic类进行计算、PrintResult类进行输出
    • 计算的过程即获得递归下降最终返回的expr的HashSet<Unified>的过程,乘法即HashSet<Unified>与HashSet<Unified>的相乘、Unified与Unified的相乘,加法即HashSet<Unified>与HashSet<Unified>的合并
    • 输出的过程即输出HashSet<Unified>中各个Unified的信息

3、指标度量分析

(1)总代码规模
(2)类复杂度和方法复杂度
  • 类复杂度
  • 方法复杂度

(仅截取标红部分)

由于表达式字符串结构变得更加复杂,导致Lexer、Parser、Customize类的复杂度较高,PrintResult复杂度较高的原因与第一次作业相同。

Lexer.next()中由于curToken具有多种类型,所以与Lexer类中其他方法的耦合度较高。

Parser.parseFactor()中由于Factor的类型较多,所以控制流结构复杂,与Lexer类的耦合度较高,圈复杂度较高,后来也发现这部分的bug较多。这也说明了圈复杂度越高,越容易出现bug。

Customize.getUnifieds()中为了判断参数数量使用了多个if-else语句,因此圈复杂度较高。

三、第三次作业

1、类图

优点:解析、计算、输出三个部分结构清晰

缺点:没有进行含sin、cos的合并同类项

2、设计思路

本次作业与第二次相比,区别在于:sin、cos的括号内不止为常数和幂函数,可以为因子;自定义函数调用时可以嵌套。

  • Tri中的coe、index改为Expr和index
  • Expr中增加指数这一属性
  • Customize增加属性newFactor1、newFactor2、newFactor3,增加方法cal,便于自定义函数的嵌套调用

3、指标度量分析

(1)总代码规模

可以看到,由于第二次设计架构时考虑到了一些限制条件可能会在第三次作业中取消,第三次作业的代码量并未增加多少,甚至比第二次作业更少。

(2)类复杂度和方法复杂度
  • 类复杂度
image-20220325194107115
  • 方法复杂度

(仅截取标红部分)

image-20220325194649807

类复杂度和方法复杂度与第二次作业类似

四、三次作业中出现的bug

1、第一次作业

Expr.toString():代码行数多,圈复杂度高

  • 在输出HashMap时,如果系数为0就不输出,则当最终结果为0时得不到输出。所以需要对最终结果为0进行特判

MainClass.preProcessing():

  • 在预处理时,当乘方的指数为0时,将这个整体替换为1。当判断指数是否为0时,如果使用字符判断,则无法正确判断出00…0这种情况。所以需要使用Integer类型进行判断

    if (Integer.parseInt(index) == 0) { //if (index.equals("0")) {
    	newStr.append("1");
    } else {
    	……
    }
    
2、第二次作业

Parser.parseFactor():代码行数多,圈复杂度高

  • sin、cos括号中的常数或幂函数,是当作Expr类处理的,得到expr的一个HashSet<Unified>,其中其实只有一个Unified有实际意义,且sinHashMap和cosHashMap为空,但是如果仅仅使用以下代码中的if是错误的,需要加上else分支

    Tri tri = new Tri(mark);
    HashSet<Unified> unifieds = expr.getUnifieds();
    	for (Unified item : unifieds) {
        if (item.getCoe().compareTo(BigInteger.ZERO) != 0) {
        	tri.setCoeIndex(item.getIndex(), item.getCoe());
            break;
        } else { tri.setCoeIndex(0, BigInteger.ZERO); } //有可能全为0!!!
    }
    
  • 自定义函数调用时读取其中的实参时,由于参数数量不定,极其容易写错。漏了好几个lexer.next(),导致当自定义函数存在三个实参时,该自定义函数调用后的部分无法执行

  • 求和函数中也漏了一句lexer.next()

MainClass.main():

  • 忘记对自定义函数定义的表达式进行预处理

CusDefine的构造函数:

  • substring的首尾值,substring(0, 1)代表第一个字符

Arithmetic.mulUnified():

  • 在进行两个Unified的乘法时,合并两个sinHashMap和cosHashMap时,如果key的@相同,则无法重复加入。(出现key的@相同的原因是往Term中添加Factor时,因为表达式因子和三角函数因子指数可能大于1,所以可能将一个Expr或Tri对象往Term的ArrayList<Factor>中添加了多次。

    • 解决方法1:往Term中添加Factor时使用深克隆

    • 在mulUnified()中采用new,解决key的@相同导致无法加入sinHashMap的问题

      IdentityHashMap<Integer, BigInteger> sinHashMap = new IdentityHashMap<>();
      sinHashMap.putAll(a.getSinHashMap());
      
      IdentityHashMap<Integer, BigInteger> bbSinHashMap = b.getSinHashMap();
      for (Map.Entry<Integer, BigInteger> item: bbSinHashMap.entrySet()) {
      	//采用new,解决键值对的@相同导致无法加入sinHashMap的问题
      	Integer key = new Integer(item.getKey());
      	BigInteger value = new BigInteger(String.valueOf(item.getValue()));
      
      	sinHashMap.put(key, value);
      }
      	//sinHashMap.putAll(b.getSinHashMap());
      	result.addSinHashMap(sinHashMap);
      

Arithmetic.mulHashSet():

  • 需要考虑在双层for循环后HashSet<Unified> result为空的情况,此时需要往里面添加一个"值"为0的Unified

Parser.findSumExpr():

  • 本意是寻找sum函数的求和表达式,但是未处理好while循环的结束细节,使得curToken带上了求和表达式之后的右括号

Customize.getUnifieds():

  • 在进行字符串替换时,需要优先将x替换!如果先将y进行替换,且y替换后的式子中含有x,则将x替换时会导致这个式子中的x也被替换,这是我们不希望看到的。例:f(x,y)=x+y,f(x**2, x)=x**2+x

Sum.cal():

  • 将i进行替换时,sin中的i不能被替换

PrintResult.toString():代码行数多,圈复杂度高

  • sin、cos中的常数和幂函数前不能有+号,否则会导致格式错误
3、第三次作业
  • 仍然是Arithmetic.mulUnified中的深浅克隆的问题,这次采用的方法是修改Unified的addSinHashMap类和addCosHashMap类

    public void addSinHashMap(HashMap<Expr, Integer> addedSin) {
    	for (HashMap.Entry<Expr, Integer> item : addedSin.entrySet()) {
    		this.sinHashMap.merge(item.getKey(), item.getValue(), Integer::sum);
    	}
    }
    

三次作业bug总结:bug主要出现在代码行数多、圈复杂度高的方法、lexer中pos指向的位置、特殊情况等部分

五、发现别人bug所采用的策略

第一次作业主要关注了预处理、输出以及输出的化简:某位同学预处理无法正确处理---的情况;零一位同学将最终输出字符串中的0+直接去掉,会导致100+20*x+x**2变为1020*x+x**2

第二次作业某位同学的程序无法处理

0
(-+sin(x)**1)**2

第三次作业主要关注了sum函数的上下界可以超出Integer类型,应该设为BigInteger类型

六、架构设计体验

在这次作业中,我第一次接触到递归下降法,由于之前很少用到递归,所以第一次作业做得比较痛苦。对着训练栏目的代码照猫画虎,以为自己差不多写完的时候才发现好像压根没开始计算……(当时以为直接使用toString就行了)赶在ddl前发现了可以使用HashMap进行结果的存储,却因为时间紧迫没来得及深入思考,便将解析和计算糅杂在了一起。(尽管后来发现第一次作业这样设计也还行)

第二次作业在第一次作业的基础上增加了三个新的因子,因此Lexer、Parse等类需要做修改,还需要新增新的类,当时考虑过将三角函数与第一次作业常数、幂函数所在类Var融合在一起变为一个新的类,但考虑到这个类的特性和功能会比较复杂、不清爽,于是还是决定为三角函数新增一个类。后来在第三次作业的设计中也证实了这个做法的合理性。

第二次作业在第一次作业的基础上进行了很大的改动,将解析、计算、输出三个部分分开,新增了一个统一形式的类Unified,将最终结果表示为HashSet<Unified>,并且考虑到了某些限制条件可能会在第三次作业中取消,所以将某些只能为常数、幂函数、因子的地方全部将其当成了表达式Expr类进行处理,很大程度上减少了第三次作业的修改量。

好的架构具有很好的扩展性,能够灵活地添加各种新的功能,因此在写代码前做好设计规划是非常重要的。

在迭代开发的过程中,从第一次作业的直接开写,到第二、三次作业的先设计后编码,我实实在在地体会到了好的设计给编码带来的便利。很多bug可以在设计的时候就发现;有了设计图纸,在编码时思路会更加清晰。

七、心得体会

对Java的语法以及一些非常便利的用法还是不太熟悉,需要加强。在三次作业中,努力地尝试将面向过程思维转换为面向对象思维,目前已经体会到了面向对象构造与设计的一些优点,但对架构的设计还不够熟练,希望能够在后续的课程中不断培养自己的架构设计能力与编程能力。

posted on 2022-03-25 23:24  lr20  阅读(77)  评论(1编辑  收藏  举报