OO第一次博客作业

 

经过了十分充实的三周,初步体会到了被OO支配的恐惧。个人认为这门课程虽然压力很大但是课程设计是科学合理的,只要能沉下心钻研一定会收获满满。希望通过这次博客作业能够更好地总结、掌握第一单元的内容,为后续的学习做好准备。

我首先先对三次作业进行了程序分析、bug分析和优化策略分析。由于第三次作业最为复杂,因此我把程序分析的重点放在了第三次作业上。之后我还简单叙述了创建型模式的使用和评测机的搭建。

一、第一次作业

1. 程序分析

(1)结构分析

第一次作业花了很多时间在熟悉正则表达式和搭建评测机上面,没有仔细思考程序的架构问题,因此保留了之前的面向过程的程序,导致程序的可扩展性较差。

OO第一次博客作业_第1张图片

整体设计思路较为简单,就是在Poly类采取HashMap来存储指数和系数,在添加新的项或是合并原有项的时候都十分方便。求导时将HashMap逐项列举调用求导规则即可。

(2)复杂度分析

 OO第一次博客作业_第2张图片

可以看出Poly.toString的基本复杂度、模块设计复杂度和圈复杂度较高,说明这个方法功能过于复杂,模块化程度低,难以维护,并且与其他模块有较强的耦合,这提醒我在之后的设计中需要将这样的方法进行一定的拆分并将职责下放到其他的方法中。

同时main方法也完成了过多的工作,从输入、预处理到求导、输出全部由main调用别的方法完成,显得有些臃肿。

2. bug分析

这次作业我在强测和互测都没有出现bug。

互测阶段中,同屋选手的bug有:

  1. 输入为单个常数时无法正常输出为空。
  2. 在进行格式化输出时使用deleteCharAt(str.length() - 1)方法,访问到了下标为-1的字符导致程序崩溃。该错误在我的设计阶段也出现过,因此很快就被我发现了。

经过此次互测我发现容易出错的都是一些边缘的极端测试点,这些测试点的通过与否也是检验程序鲁棒性的重要依据。

3. 优化策略

这次作业的优化较为容易,只需要考虑toString()方法的书写。

  1. 对系数为1、0和-1的项进行特殊处理,使之输出简化。
  2. 对指数为1的项进行优化。
  3. 避免系数为负的项出现在第一位。

 

二、第二次作业

1. 程序分析

(1)结构分析

第二次作业中我先后采用了两种架构。 第一种是设置Factor类,将SinFactor、CosFactor、PowFactor和Constant四个类作为Factor类的子类,并设置FactorFactory类来生产Factor类的对象。之后利用Term类来存储Factor类的对象,再用Poly类存储Term类的对象。对Factor、Term和Poly类分别实现Derivative接口进行求导,返回值全部为Poly类对象。乘法求导和复合求导的过程均在Term类的中进行。这种设计比较符合面向对象的思想,但是之后进行优化的时候感觉工作量较大,于是我转而采用第二种方法。以下是第二种架构的UML图。

OO第一次博客作业_第3张图片

 

这种架构不设置Factor类,而是将幂函数的指数、三角函数的指数作为Term类的参数,之后在多项式类Poly用HashMap来表示多项式。这种方法使得合并同类项较为简洁,寻找可进行合并优化的项也十分方便,缺点在于可扩展性十分糟糕,所以第三次作业只能进行重构。

(2)复杂度分析

OO第一次博客作业_第4张图片

OO第一次博客作业_第5张图片

由于设计上的不足,Term类和Poly类都显得有些臃肿。和第一次作业一样,toString()方法没有进行细分复杂度也比较高。由于没有进行继承处理,polyExtract类需要完成常数、幂函数、正弦、余弦函数的解析,因此复杂度也很高,行数险些超过60。

2. bug分析

这次作业我在强测和互测都没有出现bug。

在互测中我仔细阅读了同屋一份层次十分清晰的代码,收获很大,但是没有发现问题。使用评测机检验时也没有发现bug,究其原因还是我的数据产生器太过于随机(评测机搭建见下文)。最后我通过提交边界测试点x**-10000找到了两个人的bug,应该是没有认真阅读指导书所致。

3. 优化策略

在求导结果Poly对象的HashMap中两重循环进行配对,寻找三种配对结果进行化简:

OO第一次博客作业_第6张图片

将化简后的结果与化简前的结果相对比,计算出Poly化为字符串的长度,若长度减小则更新Poly对象并返回True,否则保留原有的Poly。交替使用三种方法进行优化,直至无法再进行化简。为防止三种方法交替使用产生循环,需要设置一个化简的最大次数进行次数熔断。

这种方法基于贪心算法,思路较为简单,不易出错,但是在1/3的数据点上漏掉了最优解。本次作业优化的最佳解法应该是使用深度优先搜索并配合时间熔断,第二次研讨课也有一位同学提出多次随机打乱配合贪心也可以取得很好的效果。

三、第三次作业

1. 程序结构分析

(1)层次结构

这次作业的难度和上次作业相比大了很多,因此在写代码之前经过了仔细的分析。

1.采用了Poly、Item、Base三个层次,其中Base用于存放因子,Item用于存放项,Poly用于存放表达式。Poly、Item和Base类全部实现derivative接口,均可调用求导方法并返回一个Poly类的对象。

OO第一次博客作业_第7张图片

2.抽象类Base5个子类,分别是SinCosPowConstantExpr(内置Poly对象,用于存储表达式因子)。经过考虑我设置了Constant类而不是在Item类中内置系数,这么做比较符合逻辑(即常数因子也属于因子的一种),但是给后期的优化造成了一定的麻烦。

OO第一次博客作业_第8张图片

3.创建变量时使用了工厂方法模式,对应5个Base子类的工厂ConstantFactory、SinFactory、CosFactory、PowFactory、ExprFactory全部实现BaseFactory接口,均可通过getBase方法返回对应的Base对象。具体实现在下文有详细的描述。

OO第一次博客作业_第9张图片

4.解析表达式时仍然使用了正则表达式的方法,在使用之前进行了一定的预处理,将全部最外层的括号都替换成了@符号,之后逐层提取相应的元素。在这一过程中并没有对多层嵌套的情况进行简化,也是导致超时的重要原因。我一开始有考虑过使用自动机来提取表达式,但是害怕类的行数会超过500,只好作罢。之后在研讨课上一位同学通过细致的拆分使用不到200行的自动机就完成了解析过程,十分高效。讨论区也有相当一部分同学使用了递归下降和表达式树的方法。

5.在WRONG FOMAT的判断中,在Main函数中加入try-catch块,如果解析过程中表达式不合法,则抛出异常并由Main函数捕获,打印出错误信息。

(2)复杂度分析

OO第一次博客作业_第10张图片OO第一次博客作业_第11张图片

 

我个人认为这次的设计比前两次作业更能够体现面向对象的思想,运用了接口、继承和多态等方法,各个层次的划分、各个类的职责分配也更加合理。但是仍然存在着部分类和方法过于臃肿的现象,例如提取表达式的polyExtract方法和优化后的multBase方法。

从类的角度来看,专门用于处理字符串的Parse类的内聚程度较低,优化之后Poly类和Item的功能也复杂了许多,圈复杂度和扇出较高,说明模块的复杂度高,需要控制和协调过多的下级模块。

(3)设计缺陷

  1. 循环依赖(cyclic dependency)

    使用DedigniteJava检测出在Poly、Item之间存在着循环依赖的问题,在构造器设计不当的情况下可能引发栈溢出等难以处理的情况。在设计中应该尽量避免循环依赖的出现,若无法避免则需要使用属性注入的方法或者使用接口来避免相互调用。

  2. 方法的圈复杂度(cyclomatic complexity)过高

    Item类的multBase、weakEquals、toString方法,Poly类的polyExtract方法,SinFactory和CosFactory类的getBase方法圈复杂度过高。圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护。

2. bug分析

这次作业强测中未出现bug,互测中由于TLE被hack14次 orz。

超时的主要原因在于优化过程中的舍本逐末,没有去掉多余的括号就开始进行比较合并等操作,导致表达式因子嵌套超过9层就会出现超时。因此在bug修复阶段我重点对多层嵌套的括号进行了判断并去除。这一过程主要在表达式因子类Expr中进行,当字符串两端分别为' ( ' 和 ' ) ' 时进行去括号尝试,若括号去除后表达式仍然有意义则采取去括号操作。

由于互测阶段开始后持续被刀心情很差,而且大部分时间花在了找自己程序的bug上,这次互测我仅发现同屋的一个bug,出现在格式化输出中。可能由于过于谨慎,这位选手在x外层也加上了括号,导致出现(x)**2的格式错误。

3. 优化策略

这次作业我优化的效果比较理想(可能是因为强测的数据比较随机导致高级优化策略难以使用),强测中大多数点都得到了满分。但是在优化过程中处理多层嵌套多项式时出现了超时,互测中被砍14刀。

(1)Item常数部分优化

首先是关于常数项的化简问题。我在Item的构造器中提前内置了一个Constant对象,因此之后有常数添加时仅需要改变该常数的值,而不需要将新的常数对象添加到Item中。同时这种方法也使系数的获取更加便捷。当幂次为0时,SinFactory、CosFactory和PowFactory不会产生对应的Base,而是产生值为1的Constant变量。最后进行格式化输出时还需要对这个常数的取值进行讨论,若该常数为0则整个Item不输出,若为正负1则省略系数。

(2)合并操作

这一部分比较麻烦,写完之后代码量翻了一倍。主要在于重写每一个类的equals和clone方法,之后改变Item类、Poly类中添加新元素的方法。需要仔细甄别一个对象是否是可变的,来决定是否调用clone方法。这部分优化频繁的操作也最终导致了我在互测中超时。研讨课中有同学提出了字典序排序后直接比较toString()来判断等价的方法,比我的方法要简洁不少,同时也避免了超时的产生。

  1. 重写Item的equals方法:由于表达式因子的存在,此处我采用了“愚人比宝”的思想,对于Item1中每个Base对象,穷举Item2,若找到相等的Base对象则在Item2中删去这一Base对象,若最后Item1列举完时Item2也为空,则两个Item对象相等。除此之外我还设置了一个weakEquals方法,该方法比较时忽略了常数项,便于合并同类项时使用。

  2. Item内部的合并:这一过程主要完成对Item内部Base类的合并。需要提前实现Poly类和Base类的equals方法。内容相同的Sin、Cos对象均可以进行合并,只需要完成指数的相加即可。Pow对象的合并较为简单,只需要找到已有的Pow对象进行指数相加即可。

  3. Item之间的合并(即合并同类项):使用weakEquals方法比较各个Item,若出现两个Item仅在常数部分不同,则可以进行合并同类项操作,删除其中一个Item并将其系数添加到另一个Item上。此处使用foreach循环会出错,只能使用普通的for循环。

(3)输出格式的选择

形如 f ( g(x) ) 的函数求导后得到 f' ( g(x) ) * g'(x) ,此时可以选择将g'(x) 展开分别乘上 f' ( g(x) ) 的各项,也可以选择不展开直接乘上一个表达式因子。我在此处做了一个简单的优化,将两个形式进行了比较并选择了较短的一个。

 

四、对象创建模式

第二次实验之后,我意识到工厂模式是个好东西,于是第三次作业中我就使用了工厂方法模式来创建Base类的对象。

  public interface BaseFactory {
      public Base getBase(String s) throws Exception;
  }
  public class ConFactory implements BaseFactory {...}
  public class PowFactory implements BaseFactory {...}
  public class SinFactory implements BaseFactory {...}
  public class CosFactory implements BaseFactory {...}
  public class ExprFactory implements BaseFactory {...}

我还使用了HashMap,这样可以在使用正则表达式的同时进行对象的选择。同时想要添加新的Base类时也会十分方便,只需要添加相应工厂类并在factoryHashMap中添加相应键值对即可,极大地提高了可扩展性。代码如下所示:

  HashMap factoryHashMap = new HashMap<>();
  factoryHashMap.put("con", new ConFactory());
  factoryHashMap.put("pow", new PowFactory());
  factoryHashMap.put("sin", new SinFactory());
  factoryHashMap.put("cos", new CosFactory());
  factoryHashMap.put("expr", new ExprFactory());
  
  ......
      
  Item it = new Item();
  while (termMatch.find()) {
      for (String t : factoryHashMap.keySet()) {
          if (termMatch.group(t) != null) {
              Base b = factoryHashMap.get(t).getBase(termMatch.group(t));
              it.multBase(b);
              break;
          }
      }
  }

 

五、评测机搭建

1. 使用了windows的.bat文件以及python作为主要工具。将Java文件以.jar可执行文件的形式导出,之后在.bat文件中使用重定向的方法,读入poly.txt文件,输出到jar.txt文件。

   call java -jar hw3.jar jar.txt

2. 编写python的三个程序:

generate.py程序引用rstr库/xeger库,用来产生正则表达式,并将结果存入poly.txt中。

  import xeger
  pattern = "..." #对应的正则表达式
  s = xeger.Xeger(limit=10)
  poly = s.xeger(pattern)
  while (len(poly) > 200 | len(poly) == 0):
      poly = s.xeger(pattern)  #限制产生表达式的长度
  f = open('poly.txt', 'w')
  f.write(poly)
  f.close()

diff.py程序引用sympy库,对poly.txt程序中的多项式进行求导,并将结果存入py.txt中。

  import sympy
  x = sympy.symbols('x')
  f1 = open('poly.txt', 'r')
  prim = f1.readline()
  f1.close()
  diff = sympy.diff(prim,x)
  f2 = open('py.txt', 'w')
  f2.write(str(diff))
  f2.close()

comp.py程序读取py.txt和jar.txt中的结果,并带入随机数特值判断两个表达式是否等价,若等价则在文件中写入“=”并继续执行进行验证,若不等价则在wrong.txt文件中输出导致出错的表达式,并使程序进入死循环,达到暂停.bat运行的目的。

  import sympy
  import random

  a = random.uniform(-10, 10)
  x = sympy.symbols('x')
  f1 = open('jar.txt', 'r')
  jar = sympy.sympify(f1.readline())
  f1.close()
  f2 = open('py.txt', 'r')
  py = sympy.sympify(f2.readline())
  f2.close()
  f = open('judge.txt', 'a')
  ans1 = jar.evalf(subs={x: a})
  ans2 = py.evalf(subs={x: a})
  if (abs(ans1 - ans2) > 0.00000001):
      f.write("n")
      f3 = open('poly.txt', 'r')
      f4 = open('wrong.txt', 'a')
      poly = f3.read()
      f4.write(poly)
      f3.close()
      f4.close()
      while (1):
          continue
  else:
      f.write("y")
  f.close()

3. 在批处理程序中依次执行generate.py、diff.py、diff.jar和comp.py程序并不断循环即可。

  @echo off
  :loop
  call python gen.py 
  call java -jar hw3.jar jar.txt
  call python diff.py
  call python comp.py
  goto loop

 

六、对比和心得体会

老师和助教都在不断强调,与OS、计组不一样,OO是一门修炼内功的课程,经过这段时间学习我也感同身受。这些知识虽然不难听懂,但是要真正理解并用于实践之中却十分困难。通过三次作业的对比我也看到了自己在这段时间取得的进步。从一开始大量静态方法组装而成的程序到之后慢慢使用了封装、继承和多态的面向对象方法,提高了模块化程度,降低了耦合。

虽然在家学习状态不如在校,但是感觉到每次理论课、研讨课、作业和实验都是经过精心准备的,觉得自己收获也不少。现在拿到作业的题目可能会优先思考架构的问题而不是一味地追求正确性了。由于前两次作业的设计还是偏向面向过程的,这三次作业我全部都是从零开始进行书写,也给之后的学习一点提醒,就是完成代码不能仅仅考虑当前的功能,还需要进一步考虑到代码的可维护性、可拓展性,这样才能让迭代设计变得轻松。

阅读了课程组下发的优秀代码后我感觉还有很长的路要走,这几天可能会继续学习一些设计模式以增强对OOP的理解,有时间的话也会提前预习一些多线程相关的内容。

这学期也是我第一次使用IDEA,习惯了DevC++后接触这样的高级IDE体验十分良好。超强的联想能力、一键格式化代码以及批量修改提供了极大的便利。希望能使用这个利器更好地面对之后的OO之旅。

你可能感兴趣的:(OO第一次博客作业)