北航面向对象第一单元总结

倪宇轩 22371305 学生 2024-03-23 19:23:55

整体架构分析

1.UML类图

img

总体的架构设计主要是分为五个模块,分别是:

  • 预处理模块
    作用:对输入的字符串进行最基本的预处理,如删除空白字符等,为了之后的解析字符的时候不需要考虑一些没有意义的字符组合打下基础。
  • 解析器模块
    作用:对经过经过简单预处理的字符串进行解析,返回解析出的表达式。
  • 组成模块
    作用:这一模块主要是形式化表达的表示产物,将一个总的表达式经过形式化表达拆解成多个独立的小模块,详见递归下降。
  • 因子
    作用:这一模块主要是组成模块里的因子的一个扩展,因为因子类型比较多,因此有多种因子类实现了因子这一个接口;而因子也作为组成模块里最底层的工件,因此具有很多的具体分支。
  • 储存单元模块
    作用:这一模块主要是解析和输出的联系桥梁,该模块将解析后的产物进行统一化的表达,最后通过一定的规则将其输出成字符串呈现出来。

2.类复杂度

img

其中每个指标的意思是:

  • OCavg(Average opearation complexity):平均操作复杂度
  • OCmax(Maximum operation complexity):最大操作复杂度
  • WMC(Weighted method complexity):加权方法复杂度
    由表中我们可以看出,整体类的复杂度控制的还是比较好的;Monomial类由于是直接联系expr、term、factor类的桥梁,因此在方法方面的复杂度会相对多一点,这也是可以理解的。

3.方法复杂度分析

img

img

img

img

其中每个指标的含义是:

  • CogC:认知复杂度
  • ev(G): 基本复杂度,用来衡量程序非结构化程度
  • iv(G):模块设计复杂度,用来衡量模块和其他模块的调用关系结构
  • v(G):圈复杂度,用来衡量一个模块判定结构的复杂程度

从上图可以看出,方法的复杂度极大多数都是合理的,除去一小部分由于是具备转化两个模块功能的方法外。这也说明本次架构中方法的划分是比较合理的。

4.行数分析

img

行数都控制在250行以下,因此每个类的代码量也是合理的

架构设计体验

第一次作业

第一次作业是整个代码框架的基础,但因为我把java都忘得差不多了(),所以在第一次作业上我花了比较长的时间进行架构。当时主要也是没有接触面向对象的思维,因此对怎么构造类、怎么划分每个类有比较大的困惑,但幸运的是通过学长学姐们的博客,我也逐渐的揭开了面向对象程序设计的大门。

递归下降

第一次作业主要实现的是简易的表达式化简,核心算法是之前OO课程组已经向我们透露出的递归下降算法。这个名字看起来很高大上,其实跟C语言中的递归函数有异曲同工之妙。从形式化表达式来看,一个表达式是由表达式因子所构成,同时因子包含很多种类,甚至包含表达式因子。如果采用面向过程的思维方式,你会发现怎么感觉写着写着又绕回去了,无穷无尽。因此,我们需要将它分成若干个独立的部分进行分析,每个部分实现他们自己的职责,而不用过多的考虑整体的实现过程
因此我们将表达式因子这三项分别作为一个独立的类进行规划,例如表达式是由若干个项组合而成的,我们可以这样考虑:

// 表达式 = 项 + 表达式 || 项
public Expr parseExpr() {
  parseTerm();
  while(isnotEnd()) {
    judge(sign);
    parseTerm();
  }
}

至于parseTerm,就不是我们这个函数需要管的事情了。这样子做,极大地降低了构建的难度,这也是面向对象的一个很重要的优点。
像如上那个样子进行递归下降,我们就能将表达式进行一个从顶层到底层的一个递归下降。
对于解析模块的另一个类Lexer就比较简单了,它的职责就是读取字符串的每一个字符进行分析,并且反馈给Parser。

存储单元

所有的解析之后我们需要一个储存单元来储存最后的结果,并且将这个结果格式化输出成字符串。这里我们采用的是Monomial,存储的只有两个私有属性,一个是系数,一个是x的指数,这也就是第一次作业化简到最后最小单元。然后需要一个存储Momomial的容器来进行多项式的合并,这个就是Polynomial,并且在Polynomial中实现多项式的相加相乘等操作。
注意!
在进行运算的时候,我们难免会碰到需要拷贝一份一毛一样的数据的时候,这时候我们就要考虑怎么拷贝。注意,只要不是想改变原来的东西,一定要使用深拷贝,也就是将所有的东西完全复制一遍,而不能直接两个相等(浅拷贝)。否则会出现一些比较有趣的东西。

第二次作业

第二次作业是难度相对比较大的一次,增加了自定义函数因子和指数函数因子。下面我们分开说。

自定义函数因子

这个主要是采取字符串替代的方式,我们在读到自定义函数的时候,调用FunStorage类,将读到的字符串存储在参数数组和表达式字符串中。等到调用的时候,将调用的实参与形参进行一个Map的对应,输出的时候一个一个匹配对应的字符,碰到形参就替换成对应的实参,并且传入解析字符串里面进行解析,最终返回一个因子。

 private static final HashMap<String,String> funcMap = new HashMap<>(); //函数名+函数定义
 private static final HashMap<String, ArrayList<String>> paraMap = new HashMap<>();//函数名+函数形参列表
 public static void addFunMap(String s)//读到自定义函数的时候使用,添加自定义函数
public static String callFunc(String name,ArrayList<Factor> arrayList) //调用自定义函数时使用,采用```append```函数,进行字符串替代,**提醒一下exp的替代**

指数函数因子

这个是本次的难点,由于指数函数的特殊性,我们需要修改一下存储的基本单元,最小单元将变成:

    private BigInteger coefficient; //系数
    private BigInteger exponent; //幂函数指数
    private Factor factor;//指数函数的因子
    private BigInteger factorExp; //指数函数的指数

那么相对应的,在多项式类中的加减法等都得进行相应的调整,比如加法的合并同类项的时候,在第一次作业中我们只需判断幂函数的指数相等即可,但是在这次作业中,我们还要保证指数函数里的因子相等才行,这里我是写了一个专门判断两个因子是否相等的函数(利用递归的思想)

 if (!Objects.equals(this.exponent, monomial.getExponent())) { // 判定幂函数指数
            return false;
        }else {
            if (s1.equals("0") && s2.equals("0")) {
                return true;
            } // 特判两个exp里面的东西是否为0,防止不限递归
        }
if(num1 != num2)return false; //判断两者因子转化为多项式类时候数量是否一样
//如果一样就一项一项比较就好

第三次作业

第三次作业难度比较小,加入自定义函数定义的时候能够利用之前应定义过的自定义函数以及增加求导因子。
对于自定义函数功能扩展,只要你在第二次作业中使用的是字符串替代之后再解析的形式,就能保证这次作业啥都不用改。

求导因子

多项式求导,也就是单项式求导的和,单项式求导利用链式法则就能求出,因此由两种实现方法:

  • Mono直接求导
    直接再Mono中写一个求导的函数,对形如a*x^b*exp(factor)^c进行求导,这几乎其他类啥都不用改,但是缺点很明显,过于聚集,万一出bug了都不好de
  • 因子求导
    每一个Mono都是一堆因子乘在一块的,因此只要实现了每一个因子的求导,根据链式法则我们就能求出Term的求导,进而求出表达式的求导,这样子我们就不用通过Mono作为中介,直接对一个表达式进行求导即可,最后返回一个多项式就行了。个人推荐这一种。

重构设计

由于我一开始就用的课程组提供的递归下降思路,在整体上并没有进行很大的重构,唯一的一点是由于第一次作业的存储单元比较简单,所以我在Ploynomial里面用的时hashmap(以幂函数的指数作为key),这样当时多项式的化简的时候会简单很多。但是后来由于加入了指数函数因子,导致原本的架构会出现很多相同的key,无法再使用,因此全部改为Arraylist,不过工作量其实并不是很大。

扩展性能

我的架构的扩展性还是比较好的。假如说新增了一个三角函数因子,那么我只需新建一个三角函数的因子类,更改一下Lexer和Parser的部分内容,以及最小单元和多项式的计算层面即可,很类似exp的添加,甚至说是极度相似。这个就不再赘述了。

性能优化策略

本单元的评测指标是输出字符串的长度,一般来说有下面两种优化方向

  • 去除exp的不必要的括号,我采用的是判断其中的因子转化为多项式后是否满足单一因子的要求。
  • exp提取公因式,大部分人主要是提取系数的公因子,但是有一些大佬还考虑到了将exp(x)提取出来emmmm,如
    exp((x+2*x^2+3*x^3+4*x^4+...+100*x^100))如果将exp(x)提取出来是会变短的emmm

Bug & Hack

第一次作业

自己并没有发现bug,但是同一个房间的一个人被hack了好几次,问题出现在表达式因子的指数不为1的时候,进行展开可能系数出现了一点问题。

第二次作业

第二次作业我被找出了个bug,主要原因是我想着优化一下不必要的括号,这里写出问题了。就是在中间的输出过程中,可能会出现形如exp(2*x)这样的非法中间字符串,然后就噶了。
同时我也找到了别人的bug,实在处理自定义函数的地方,如f(-(x^2),exp(x))这样,读取的时候负号被忽略了导致错误。

第三次作业

emmm我被CPUTLE了,数据是dx(exp(exp(exp(exp(exp(exp(exp(exp(x^2)))))))))

总体的感受

评测机评测机评测机!!! 这个真的特别重要,从计组到OO越来越凸显这个的重要性,评测机能帮助你大面积的测试数据,来帮你测到一些中测测不出来的东西。
不过还是要手动构造一些数据,比如0次方输出为0的时候是否会出现输出为空临界biginteger的测试等等。
但是我自己是没有通过去读别人代码获取错误的,因为有的代码真的比较冗杂(),希望大家写的时候可以加上一些注释。
同时,我出现的bug其实都是小地方的疏忽,对于代码行数和圈复杂度上并没有很明显的变化。

心得体会

在OO开始之前,我听闻OO第一单元有一半以上的人要熬一整个通宵,所以刚开始的时候确实挺害怕的。然后经过一个单元的学习,确实感觉到了压力,可能是因为之前接触的比较少吧,但是确实没有像传说中的那么那么可怕,还是能保证睡眠的()。虽然说写代码的时候确实挺折磨的,但是最后跑完之后AC了的话,也确实能松一口气有一种成就感。还有三个单元,希望能满载而归吧,加油!

建议

我觉得需要改变一下理论课的教学内容,现在讲的都是一些比较理论的、抽象的东西,对于作业的实现其实并没有特别大的帮助,如果能在课上分析一下作业类似的题目就好了。

...全文
71 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧