一、程序设计思路及度量分析
第一次作业
思路与实现方案:由于第一次作业要求简单,只需要实现幂函数的求导即可,所以我只设立了两个类,在主类Entry中通过大正则进行匹配,提取每一项的coefficient和index。此外建立Poly类管理表达式和求导,并在其中通过hashmap管理每一项,用幂函数的index作为hashmap的key,易于合并同类项化简。
UML类图:
度量分析:
由于第一次没有经验,分别将输入的匹配和初始化全都放在了主类Entry中,并且将输出放在了Poly类。由于输入匹配和初始化放在一起,输出和优化长度放在一起,导致这两个方法复杂度很高,过于臃肿。
第二次作业
思路与实现方案:其实考虑到第三次嵌套求导的情况,本来是打算在第二次作业就完成因子、项、表达式的归一化求导的,但由于这种方式在第二次作业中难以实现长度优化,遂放弃。仍使用表达式中直接处理每一项的方式,没有抽象出因子这一层。为方便优化,将每一项抽象为 a*x^b*sin(x)^c*cos(x)^d 的模式,将四个参数a、b、c、d存成一个term。Term类中完成同类项的合并工作。Expression类完成正则匹配读出每一项,并通过term的ArrayList存放项,并完成求导。
UML类图:
度量分析:
合并同类项与统一输出的方法复杂度较高。类之间耦合性较好。
第三次作业
思路与实现方案:在该任务中,已经很难看到之前作业那种很明显的对应关系,因为因子和项已经可以嵌套,而且可以变得相当复杂。因此,我构建了从上到下的从 Expression 到 Term 再到 Factor 的抽象层次逻辑关系,并将 Factor 作为工厂类实现。此外,我在 Expression Term和Factor 类及其子类中均加入了 public static final 的 REGEX 作为其属性并对所有类开放,方便进行正则匹配。从而让工厂可以自动地通过逐一判断返回恰当的因子类型。所有的因子、项、表达式类均实现 Derivative 接口,方便在不同层面进行求导,例如在 Expression 类中实现对于加减法的求导法则,在 Term 类中实现对于乘法的求导法则,在含有嵌套的因子类中实现链式求导法则等。
首先是预处理,先对所有的非法空白字符进行特判,之后去掉所有的空白字符。将 SinFactor 和 CosFactor 和 ExprFactor 类中的正则均只匹配到括号,这样处理方便递归匹配正则。使用各个类中的正则匹配组合,在 Expression 类中进行整个的matches方法,错误即输出WF。之后将所有最外围括号外的多个连续的加减符号替换成一个,并据此进行截断,传入到 Term 类中。
在 Term 类中根据最外侧括号外的乘号进行截断,并传入 Factor 类。 Factor 类根据传入的字符串进行正则匹配,通过工厂模式返回所对应正确的因子,并在此因子中进行求导。若遇到存在嵌套的因子Sin、Cos、和Expr,将其括号外的内容读出并在此传输到 Expression 类中进行递归处理。
关于输出,我的每一个求导方法均为 public String derivative (String str); ,即参数和返回值均为字符串,方便进行字符串的连接输出,不需要使用表达式树+toString方法递归输出。优点是易于实现,运算效率高,并且对于嵌套链式法则求导的性能较好。缺点是对于加减求导法则和乘法求导法则的性能很差。
UML类图:
度量分析:
复杂度较低,对比前一次没有明显上升,主要是因为逻辑层次比较清晰。
二、Bugs & Tests
我个人前两次作业在强测和互测中均未出现bug,第三次作业在互测阶段被hack了一个bug。经排查是细节考虑不周:我在进行连续的加减符号替换式是只考虑了连续的两个加减符号,而未考虑连续的三个加减符号(因为我潜意识里连续三个加减符号只能出现在表达是最开头,而最开头我已经特殊处理了)。这一认识上的偏差导致只要数据在表达式内部出现三个连续的加减符号,我的程序就会崩溃。(实在是难以想象这么明显的bug在强测中没查出来,而且在互测中竟然只被hack了一次)
我在前两次作业hack别人时的策略主要是考虑细节。例如在第一次作业,由于逻辑简单,于是我针对空白字符进行了hack。而在第二次,我针对指数的规定范围进行了hack,均有所斩获。
在第三次作业中,考虑到很多大佬的表达式树+toString的优化可能大大影响性能,于是我使用了很多超深层的嵌套函数进行hack,效果理想。
三、应用对象创建模式
第三次作业中的 Factor 类实际上应用了工厂模式,通过实现 public Factor FactorFactory() 方法,我对于每一个传入的项字符串都对各个种类因子分别进行了正则匹配以寻求正确的返回类型:
1 public Factor FactorFactory() {
2 if (factor.matches(SinFactor.REGEX)) {
3 return new SinFactor(factor);
4 } else if (factor.matches(CosFactor.REGEX)) {
5 return new CosFactor(factor);
6 } else if (factor.matches(PowerFactor.REGEX)) {
7 return new PowerFactor(factor);
8 } else if (factor.matches(ConstFactor.REGEX)) {
9 return new ConstFactor(factor);
10 } else if (factor.matches(ExprFactor.REGEX)) {
11 return new ExprFactor(factor);
12 } else {
13 System.out.println("WRONG FORMAT!");
14 System.exit(0);
15 return null;
16 }
17 }
由于本次作业中,各类因子的添加需要在同一个位置进行判断,它们又继承自同一个类,所以在本次作业中非常适合应用工厂模式。在实际应用中,我使用的是一个简单工厂,通过工厂类中提供的函数进行构造。但是, Factor 类作为所有因子的父类,不应该再多实现一个工厂功能,而是应该单独建立工厂类进行操作;同时,这种朴素的实现方法等价于用switch-case对所有子类进行遍历,当子类类型增加时还需要对工厂类进行修改,不符合开闭原则。
四、心得体会
早就听闻了OO的恐怖,通过第一单元终于有了切实的体会。抛开面向对象的思想不谈,从拿到一份几乎无从下手的作业,到慢慢分析需求,到设计功能层次和数据管理层次的抽象,到最后实现功能,整个一套下来还是很神清气爽的。当然对比很多大佬的程序,无论是从功能抽象的角度,还是高聚低耦的角度来看都有很大不足,主要问题有:
- 对于抽象层次与设计模式的运用不够,主要依赖类的组合和条件判断来解决问题;
- 一些类与方法之间的耦合度较高,修改困难;
- 一些方法过于冗长且内聚性不够。
再其次,其实自己在第三次作业时间比较充裕的情况下,偷懒并没有认真的进行优化,应该自我批评。
总而言之,要学的还有很多,道阻且长。