OO第一单元作业总结——表达式求导

前言

  • 本次博客将从五个方面对OO第一单元作业进行总结:

    • 一、基于度量分析程序
    • 二、程序出现的bug
    • 三、互测数据构造
    • 四、重构经历总结
    • 五、心得体会

---

一、基于度量分析程序

前言

  • 这一部分会基于IDEA自带的度量工具分别对我的三次作业进行分析,内容比较杂且多,下面是每次作业总结中遵循的逻辑思路

第一次作业

  • 第一次作业概览

    • 任务:实现多项式的求导,表达式中只含x的若干次幂

    • 实现思路:

      • 正则表达式识别
      • 求导运算
      • 优化性能:合并同类项,幂次合并
    • 第一次作业架构概览:

  • 整体来看,由于第一次作业所要实现的功能较为简单和单一,因此采用相对较简单的线性结构:即逐级下降,分级处理,实现化繁为简的效果。
    • 从具体实现的功能来看,本次作业中所有要实现的功能都是基于这种线性结构逐层递归调用,主要体现在以下三个方面:
      • 对输入字符串的解析:多项式拆解为项,项拆解为常数因子与幂函数因子。
      • 求导运算:多项式的求导调用项的求导,项的求导调用因子的求导。
      • 优化性能:在每个项中合并每个因子,化简成ax**k的形式;在多项式中合并每个项,利用HashMap合并具有相同幂次的项。

  • 方法的复杂度分析

    • 方法复杂度的宏观指标:认知复杂度Cogc、圈复杂度v(G)、设计复杂度i(G)与基本复杂度ev(G):

  • 对于认知复杂度Cogc,有柱状图可以看出本次作业中的toString()方法普遍复杂度偏高,这说明第一次作业所写的toString方法可读性较低。反思所写的toString代码,Poly类的toString方法如下:
  //toString in Class Poly
	@Override
    public String toString() {
        BigInteger zero = new BigInteger("0");
        BigInteger one = new BigInteger("1");
        BigInteger posOne = new BigInteger("-1");
        String coef = (this.exponent.equals(zero)) ? this.constant.toString() :
                (this.constant.equals(one)) ? "" :
                        (this.constant.equals(posOne)) ? "-" :
                                this.constant.toString();
        String exp = (this.exponent.equals(zero) || this.exponent.equals(one)) ? "" :
                ("**" + this.exponent.toString());
        String sign = (this.constant.compareTo(zero) == 1) ? "+" : "";
        String x = (this.exponent.equals(zero)) ? "" :
                (this.constant.equals(one) || this.constant.equals(posOne)) ? "x" : "*x";
        return sign + coef + x + exp;
    }
  • 分析:本次作业中在toString优化中进行了表达式的优化输出,分支过多导致代码的可读性降低,因此其认知复杂度较高,且众多的分支更容易出现bug。以后的代码应尽量避免这种多分支的写法
  • 基本复杂度ev(G)较为平均
  • 设计复杂度方面,仍然是toString方法复杂度较高,原因是判断逻辑过于复杂
  • 圈复杂度依然是toString方法复杂度较高,原因是分支过多

  • 方法复杂度的具体指标:调用其他方法次数与方法的控制分支个数:

  • 分析:在此图中能更清楚地验证,toString调用次数与控制分支过多,从另一个角度验证了本次作业的toString方法确实存在复杂度过高的问题。
  • 此外值得注意的是,在作业中为了方便,我加了一个在项中处理因子的方法numHandler。由图可以看出它的调用次数过多,这说明此方法过于简单粗暴,仅仅是为了解决某一需求而强行出现的“集大成者”,复杂度高,具有很差的可维护性和可扩展性

  • 类的复杂度分析(Class Complexity)

  • 分析:除了MainClass以外,其他三个类的操作复杂度较为平均,没有出现“一家独大”的情况,但这也可能是第一次作业比较简单的原因。

  • 类的代码规模

  • 类的规模(CSOA)来看,除了MainClass以外的三个类较平均。
  • 代码行数(LOC)语句个数(STAT)来看,三个类都比较平均且维持在较低的水平
  • 本次作业类的代码规模较小且较平均,是由于作业任务简单的原因。

  • 第一次作业优缺点分析

    • 优点:组织架构是较简单的线性结构,所有的方法都是基于这样的结构进行,且识别、求导和优化三个动作实现了解耦,代码逻辑整体较清晰
    • 缺点:为了优化性能,使第一次作业中的toString方法过于复杂,具体表现为调用其他方法次数过多以及控制分支的个数过多,导致这部分代码的可读性与可维护性非常低。

----

第二次作业

  • 第二次作业概览

    • 任务:支持正余弦函数的表达式求导
    • 实现思路:同第一次作业
    • 第二次作业架构概览
  • 第二次作业要求实现支持正余弦函数的求导运算,因此第一次作业的多项式架构不再适用于此次作业,需要重构。
    • 本次作业参考老师和助教的提示,将二元加、乘、正余弦、幂函数、常数因子、x变量分别抽象成一个类,运用建立表达式树的方法完成本次的求导任务。
    • 为了方便建立表达式树,我参考了面向对象设计中的“工厂模式”,建立了工厂类。但是由于对工厂模式理解不是很准确,只能勉强对付这两次作业,并不是真正意义上的工厂模式。
    • 为了方便优化性能,我针对此次作业专门设置了一个类Triples,意义为三元组。针对本次作业,所有的项都可以化简成带一定系数的xsin(x)cos(x)的若干次幂的乘积,再利用HashMap为每一个类设置了规则,递归调用化简表达式,这种优化方法会在后面专门讲到。

  • 方法复杂度分析

    • 方法复杂度的宏观指标:认知复杂度Cogc、圈复杂度v(G)、设计复杂度i(G)与基本复杂度ev(G):

  • 总体上来看,工厂类生成表达树的方法generatItem和三元组最后的输出方法复杂度比较高。

  • 下面我们从复杂度的一些具体指标来分析背后的原因

    • 方法复杂度的具体指标:调用其他方法次数与方法的控制分支个数:

      Method CALL CONTROL
      Factory.generateItem(String) 83 12
      TripleMap.output(HashMap<Triples, BigInteger>) 55 9
      MainClass.main(String[]) 23 0
      Sub.setMap() 20 3
      Add.setMap() 16 3
      Cosine.derivation() 16 3
      Factory.signOutOfBra(String,String) 14 6
      Multiply.setMap() 14 3
      Sine.derivation() 10 2
      Add.derivation() 9 2
      Power.setMap() 9 2
      Power.derivation() 8 0
  • 将调用方法次数和控制分支个数大致按从高到低的顺序排列,如上表所示;

  • 工厂类的generateItem方法调用其他方法的次数高达83,而控制分支个数也达到了12。这是由于我在写generateItem方法时将所有表达式树生成的逻辑都聚集于此,因此控制分支个数较多。其次,由于代码行数的限制,不得不提取一些函数方法放在工厂类中,再在generateItem方法中调用这些“子方法”,因而导致generateItem的调用次数过多。当然generateItem方法中需要调用所有类的构造函数,而每个类的构造函数又都是递归调用的,所以也会导致调用次数较多,但我认为这一部分的调用是无法避免的。

  • 对三元组Triples的输出逻辑同样也较复杂,为了优化输出存在着较多的if-elif-elif-……-else的逻辑判断,另外还有不少的三目运算符。这种比较复杂的逻辑判断大大增加了控制分支的个数。另一方面,在优化的过程中需要调用各种类的equals方法加以判断,这种特判的语句会使调用次数大大增加;与此同时,这个output方法需要调用所有类的Triples的方法,因此会使调用的方法次数比较多,但我认为这一部分的调用并不使代码变得臃肿。

  • 总结:上述提到的两种方法均存在着输出逻辑过于复杂、为了判断而增加许多繁琐调用的缺点;但是与此同时,这两个方法又是表达式树递归调用的必经之路,这样的方法势必要调用更多的方法来完成它的功能,因此我认为这是我在争取设计逻辑简洁时所付出的其他复杂度的代价,本质上是一个trade-off的过程,这一部分额外增加的调用是无法避免的


  • 类的复杂度分析

  • 第二次作业类的复杂度的度量如上图所示
  • 操作复杂度方面,Factory类和TripleMap类的操作复杂度偏高,具体原因上述已经分析过。而其他类的操作复杂度普遍较低。可以看出在第二次作业中,已经出现了Factory“一家独大”的局面
  • 加权平均方法数方面,同样也是Factory类较为突出。

  • 类与类之间的耦合与依赖

  • 第二次作业类之间的依赖关系如上图所示
  • 循环依赖方面,工厂类和除了ConstantXvar类之外的其他“运算对象”类,相互依赖的程度都非常高。这是因为本次作业的架构是为了第三次作业中的嵌套因子而准备的,这样的设计势必会导致“你中有我,我中有你”的局面,造成了类之间耦合度高的后果。
  • 不考虑出于嵌套考虑的相互耦合,本次作业中的Factory类仍然是耦合其他类的“集大成者”,这是本次作业设计的不足之处。

  • 类的代码规模

  • 代码量上来看,第二次作业各类的代码规模明显比第一次的大。
  • 代码量的分布来看,代码规模主要集中在Factory类和较复杂的Multiply类。

  • 第二次作业优缺点分析

    • 优点:第二次作业是我真正意义上编写面向对象程序的初体验,虽然仍然对面向对象思想理解不深刻,但向前迈出了一大步。第二次作业的优点在于,将所有的运算对象看作是一个相同的类,递归下降构建表达式树,在表达式识别、求导运算以及优化输出阶段,均采用了递归思想,化整为零,降低了问题的难度。
    • 缺点:第二次作业的缺点也十分明显。在初次尝试“工厂模式”的情况下,我所建立的工厂类过于臃肿,是整个项目的集大成者,这样的坏处是代码的可维护性和可读性非常低,且很明显可以看出本次作业中我所采用的其实并不是真正意义上的工厂模式,只是为了Handle整个表达式树而创立的一个”一家独大“的类。这样做的坏处是,使项目中的某个类规模过于庞大,当需求增多时,代码复杂度和代码行数会膨胀得非常快,失去了程序的可读性和优雅性

---

第三次作业

  • 第三次作业概览

    • 任务:支持嵌套因子的表达式求导,需要检查格式
    • 思路:与第一、二次作业相似,格式检查用递归下降的方法来实现
    • 第三次作业架构概览
  • 第三次作业将第二次作业中的二元加与二元乘改成了连加和连乘
  • 由于第二次作业中的三元组化简方法不再适用,故删去Triples类和TripleMap
  • 新增的格式检查的需求在构造过程中利用递归下降的方法实现,即按照形式化表示贪心地读取表达式、项和因子,若不能成功匹配,说明有格式错误。

  • 方法的复杂度分析

    • 方法复杂度的宏观指标:认知复杂度Cogc、圈复杂度v(G)、设计复杂度i(G)与基本复杂度ev(G):

  • 由于加入了格式检查,第二次作业中的构造表达式树方法generateItem复杂度依然较高。且换成连加与连乘后,加入了addParsermultiplyParser方法识别字符串并返回Item的Arraylist(构建表达式树的过程),导致这一部分的复杂度较高。
  • 与此同时,在构建表达式树的过程中,还要加入一系列需要多次用到的方法:例如isRemovablesignOutOfBra分别用于判断是否能去括号与识别不在括号内的符号(用于分项),这种一系列的调用使得代码的复杂性大大增加。
  • 依旧是格式检查的问题,由于三角函数内的嵌套因子的限制,在构造三角函数Item的时候,需要递归判断里面的嵌套因子是否合法,因此需要建立一个方法triCheck,用于递归调用函数检查格式合法性,这也大大增加了代码的复杂度。
  • 此外,由于每个构造都是按照多项式—项—因子的逻辑顺序去解析,在这一过程中构建出来的表达式势必会包含很多不必要的括号(例如因子从多项式开始解析就会自带两个无谓的括号)。针对于此,我在Item接口中设置了symplify方法,用于多项式的降级,以及1、0特殊数的处理,使表达式的结构大大简化。但同时也付出了复杂度增加的代价。

  • 方法复杂度的具体指标:调用其他方法次数与方法的控制分支个数:

    Method CALL CONTROL
    Factory.generateItem(MyString) 88 13
    Factory.addParser(String) 46 11
    Factory.triCheck(String) 32 4
    Factory.multiplyParser(String) 25 8
    Cosine.derivation() 19 3
    Factory.powerCheck(String) 16 0
    MainClass.main(String[]) 15 1
    Multiply.symplify() 15 4
    Factory.signOutOfBra(String,String) 14 6
    Multiply.derivation() 13 3
    Sine.derivation() 13 2
    Add.symplify() 12 3
    Power.derivation() 11 0
    Power.symplify() 8 2
    Factory.isRemovable(String) 5 7
    Add.derivation() 4 1
    Power.equals(Object) 4 2
    Add.equals(Object) 3 2
    Constant.equals(Object) 3 2
    Cosine.equals(Object) 3 2
    Factory.previousNotBlank(String,int) 3 4
    Factory.subStrCount(String,String) 3 2
  • 由上表可以看出,由于重构难度较大,因此第二次作业中较为复杂的构造方法维持不动,且新加入了格式检查相关的语句,使得generateItem方法的复杂度不降反升。

  • 为了格式检查所派生出来的许多方法例如addParsertriCheck等等,虽然可以直接递归调用,且不需要加很多的特判(只用根据表达式正确文法来就行了),但是也付出了复杂度增加的代价。这再一次体现了程序设计中的trade-off。


  • 类的复杂度分析

  • 第三次作业的复杂度两极分化更加严重,Factory类的复杂度比上次有所增加

  • 类与类之间的耦合与依赖

  • 相较于第二次作业而言,第三次作业的类的循环调用次数大大降低。原因在于我改变了第二次作业中的构建表达式树的方式:第二次作业中是在构造出一个Item的时候,首先要就地构造出这个Item所包含的Item,这样进行下去势必会导致类之间的循环调用次数过高。而第三次作业中,每个Item的构造都是按照多项式-项-因子的逻辑顺序构造的,而且使构造出来了以后再添加为表达式树的一个节点,这种构造方式减少了类之间的相互调用。
  • 由于加入了格式检查的需求,工厂类的复杂度依然较高

  • 类的代码规模

  • 第三次作业的代码量大大增加,就Factory类而言,代码量增加了一倍,这是非常不好的。由于没有处理好用递归下降进行格式检查的问题,使得Factory类的代码变得臃肿而冗余。这是我认为我第三次作业做得最失败的地方。

  • 第三次作业优缺点分析

    • 优点:本次作业改变了构建表达式树的方式,使类之间的循环调用次数大大降低;与此同时,将二元加、乘改为连加与连乘,使表达式树的结构更简洁。
    • 缺点:为了格式检查而使工厂类的代码急速膨胀,调用复杂度大大增加。没有处理好递归下降的方法,反而是实现的过程非常地复杂、非常地面向过程。

---

二、程序出现的bug

本单元作业中均在本地做了充分的测试,在强测和互测中都没有被找到bug,下面简要分享我在本地测试中遇到的bug。

第一次作业

bug描述 出现原因 所在方法 所属的类 测试数据(输入) 错误输出 方法代码行数 圈复杂度
对‘0’不输出,输出空串 缺少特判 toString Item x-x (空串) 16 10
对‘-1’不输出 缺少特判 toString Item x**2-x 2*x- 16 10
'*'与‘**’识别错误 正则错判 numHandler Power \ \ 26 3

第二次作业

bug描述 出现原因 所在方法 所属的类 测试数据(输入) 错误输出/报错原因 方法代码行数 圈复杂度
括号错配 分支控制不全面 isRemovable Factory (x)*sin(x) 将开头与结尾的括号匹配 26 9
运行严重超时 重复递归调用toString toString \ \ \ \ \
减法类的错误 误加了”减法类“,识别到减号即将其后面的表达式用括号括起来 generateItem Factory x-x**2+x**3 1-2*x-3*x**2 55 19

第三次作业

bug描述 出现原因 所在方法 所属的类 测试数据(输入) 错误输出/报错原因 方法代码行数 圈复杂度
‘**’匹配错误 出现多个‘**’时不能准确识别 powerCheck Factory sin(x**2)**2 报错(将第一个'**'识别为外层指数符号) 16 5
三角函数内嵌套因子判断错误 在嵌套因子的判断时,仅在字符串形式上识别与判断,没有递归判断其正确性。 triCheck Factory sin(sin(x)*cos(x)**2) 没有报格式错误,即识别成sin((sin(x)*cos(x))**2) 32 10
三角函数嵌套因子提取错误 将左括号也包含进因子 triCheck Factory sin( x**2 ) 报错,将因子识别为 '( x**2' 32 10

总结

  • 由于本单元作业使用递归下降的方法完成,所有的构造严格按照指导书的文法来进行。因此即使是在第三次作业的格式检查中,也没有出现很多琐碎的bug,出现的都是比较明显的、结构性的bug,比较容易发现与修改——这得益于本单元作业架构的相对清晰与采用递归构造的方法,缺点是复杂度代价太高,出现了”一家独大“的工厂类。
  • 由三次作业出现的bug来看,出现bug的位置几乎全在Factory,也就是本单元作业中我所构建的”一家独大“的类。从所在方法的代码行数与圈复杂度来看,出现bug的方法大多也是行数较多,分支控制较复杂(圈复杂度较高)的方法,因为这样的方法一方面可读性不高,不好维护;另一方面判断逻辑过于复杂会使潜在的bug更容易出现,因为程序员很难一次在脑海中将一个复杂的问题考虑得十分周全。

---

三、互测数据构造

​ 第一单元的互测采用随机黑盒测试+手动构造数据的方法对互测屋内的同学进行测试,但并未仔细阅读其他同学代码,也就是并没有实现用白盒测试找到别人的bug。

​ 下面依次分享我的数据构造思路与程序、互测结果与互测心得


  • 数据构造思路

    • 以第一次作业为例,具体构造思路是:(1)准备常数库;(2)准备前缀(符号和前缀0)库;(3)分层随机生成

    • 为了使数据在有限的长度内具有一定的强度,测试数据采用针对性构造的原则,即随机生成的数据只对某个方面具有很强的测试强度,例如嵌套层数、大数问题、01特殊数的处理、符号的处理……,可以在程序中修改不同的参数来实现

    • 数据生成程序如下:

      import random
      
      numList = [0, 1] ##常数库,天然含有0和1
      numSignList = [" ","+", "-", "+0000000000000000000", "-0000000000000000000"]  ##前缀库1
      itemSignList = ["+", "-"]
      PolySignList = [" ","+", "-"]  ##前缀库2
      numofFactor = []
      
      for i in range(0,40): ##想常数库中加入想要加入的其他数字
          numList.append(random.randint(9223372036854775807,9223372036854775999))
      
      def generateNum():
          return random.choice(numSignList) + str(random.choice(numList))
      
      
      def generateFactor(pre):
          Posibility = 0.3
          x = random.uniform(0, 1)
          if (x >= Posibility):
              return pre + generateNum()
          else:
              P = 0.1  ##调小,无指数的x会多
              y = random.uniform(0, 1)
              if (y > P):
                  return pre + "x"
              else:
                  return pre + "x" + "**" + generateNum()
      
      
      def generateItem():
          times = random.randint(1, 8)
          s = ""
          for i in range(0, times):
              if (i == 0):
                  pre = ""
              else:
                  pre = "*"
              y = generateFactor(pre)
              if (y == ""):
                  y = "-x"
              else:
                  pass
              s = s + y
          return s
      
      
      def generatePoly():
          times = random.randint(1, 30)
          s = ""
          for i in range(0, times):
              y = generateItem()
              if (i == 0):
                  s = s + random.choice(PolySignList) + random.choice(itemSignList) + y
              else:
                  s = s + random.choice(itemSignList) + y
          return s
      
      def generate():
          while (True):
              s = generatePoly()
              if (len(s) < 1000 and len(s) > 980):
                  return s
                  break
      

  • 互测结果

    • 第一次作业中,运用程序生成的数据找到2个人的bug,但并没有考虑到简单的情况,即输入'x-x'输出空串的bug。这是因为第一次互测只依赖程序而忽略手动构造数据的缘故。
    • 第二次作业中,运用程序生成的数据找到1个人的bug,屋内并没有找到其他人的bug。
    • 第三次作业中,运用程序生成的数据无法找到bug,这是因为经历了三次作业以后求导运算与外层的识别中大家做得比较完善。而最后我是通过手动构造找到bug,但依然没有仔细阅读代码进行白盒测试

  • 互测心得

    • 第一次进互测屋并没有太多的经验,从最开始的依赖程序进行随机黑盒测试,到后来意识到需要手动构造数据进行极端性测试。三次作业下来由于怠惰与时间限制,我也并没有仔细阅读同学的代码进行白盒测试,希望在接下来的作业中能够不断提高自己阅读代码的能力,在时间允许的情况下尽量要阅读代码,进行白盒测试。

---

四、重构经历总结

  • 由于第一单元的作业任务较简单,所以本单元作业仅在第二次作业重构了一次,第三次作业仅仅是在第二次作业的基础上增加了格式检查,改动了加和乘的存储方式。

  • 第二次作业重构是因为第一次作业的简单线性结构仅能支持多项式,当混入三角函数以后并不能支持各种复杂的求导运算。且受到了老师和助教的启发,开始逐渐有了”划分对象“和”抽象“的概念,不再简单将表达式看成多项式-项-因子,而是各种运算类的嵌套和组合,这使我在第二次作业难度提升的同时也能比较逻辑清晰的完成,但是缺点是复杂度大大提升

  • 第三次作业在第二次作业的基础上增加了interface,使类的管理和方法调用更加规整和统一,类与类之间的结构变得更加有条理(类图在上述分析时已经给出)。同时改进了构造表达式树的方式,使类与类之间的循环调用次数与耦合度大大降低。但是同时也引入了一些为了简化而简化的方法,没有在本质上对程序的简洁性有所提高。

  • 总体而言,三次作业下来,我在不断的重构与改进中加深了对面向对象的理解,从第一次作业的面向过程到二、三次作业领悟到了“抽象”的概念,是对我而言一个小小的进步。


---

五、心得体会

  • Everything is Object

    • 万物皆对象。第一单元作业的三次作业使我对面向对象的概念理解更深了一步,具体而言,我开始理解”对象“和”抽象“的思想。程序设计中对象的划分不一定是按照天然的逻辑,例如本次作业中,天然的逻辑是多项式-项-因子,这固然可以成为一种对象划分的方式。但另一方面,如果再将所涉及到的表达式进行抽象,可以抽象总结出常量、x变量、加、乘、三角函数、幂函数这些抽象的运算类。这么做的好处在于将对象划分得更加细致,在进行求导运算时将每个类的求导运算过程写好,整个表达式的求导自然而然就完成了。
    • 对象的划分是个永恒的话题,如何找到一个合理的对象划分方式(或者说是抽象方式)是我学习这门课时需要不断思考的。本次作业的求导运算只是一个小小的引子,在以后面对更加复杂的现实工程时,一定要时刻提醒自己学会合理抽象,在重构和挫折中吸取经验,找寻一种更合理的抽象方式。
  • Structure Decides

    • 结构决定一切。第一单元的作业虽然简单,但是倘若组织架构不合理,也会出现很多琐碎的bug,甚至影响项目的可扩展性。具体而言,我在第一次作业使用的线性结构就并没有考虑到求导需求的扩展,因此在第二次作业中花了很大的精力进行重构。在进行第二次作业的构造时,我会考虑到第三次作业可能的需求,因此我在第二次作业就可以实现第三次作业的求导功能,到了第三次作业的时候仅需要修改以下具体的细节即可。此外,这种组织架构也使我出现的琐碎的bug比较少,出现的bug都是结构设置本身的不合理,可以很容易地发现和解决。
    • 架构的重要性不言而喻,这甚至可以体现在本次作业的性能优化上。在第二次作业优化时,一开始我苦于建造的表达式树过于繁琐,输出长度过长,于是我开始从字符串的角度开刀,对一些可能的情况进行省略和化简——但这并没有从本质上改变树的结构,且化简的效果也极其有限。走了很长一段时间的弯路以后,我认为”解铃还须系铃人“,组织结构产生的问题应该由组织结构自身来解决和优化。于是我设计了Triples类(三元组),对每个类设置了其合并的算法并返回优化后的三元组,之后从表达式树的根节点自上而下递归调用即可,最终化简的效果也非常显著,性能分接近满分。
  • Simplicity Wins

    • 简洁为王。这主要是针对于我第二、三次作业而生发出的感想。由于我没有把握好递归下降的处理方式,对功能的归属划分不合理,导致出现了一个极其复杂的Factory。而在整个类里面,为了一劳永逸实现一些零零碎碎的功能,我又写了很多圈复杂度较高的方法,且在这些方法里面出现的bug比较多。诸如此类不好的代码编写习惯使我的三次代码量急速膨胀,这是我在之后更加复杂的作业项目中需要更加警惕的地方。
  • Trade-off is Necessary

    • 权衡是必要的。没有十全十美的设计方式,任何一种设计方式都存在它的优势和缺点。以我本单元的作业为例,优点在于构建表达式树的过程逻辑清楚简单,甚至在格式检查中也不容易出现零碎的bug,对表达式树的操作都只需要设计好规则,任计算机递归调用即可;而这样的设计方式也存在明显的缺点,就是代码量和复杂度比较高,而且在性能优化的时候也会出现一定的困难。具体例子就是我的第三次作业没有实现更好的化简,原因是因为设计方式的限制,要使化简效果显著需要编写大段大段的程序才能实现——这是我本单元作业的权衡之处,为了设计逻辑简单而付出复杂度和性能分的代价
  • 总结

    • 这三次作业对我而言收获非常大,在编程能力上有了一定程度的提升。同时收到老师和助教的启发,对一些编程思想和一些编程模式有了一定的了解和实践。这是我面向对象编程路上的第一步,以后还需要继续不断总结反思,不断提高。
posted @ 2021-03-28 14:17  罗宇轩  阅读(195)  评论(1)    收藏  举报