面向对象设计与构造第一单元总结

作业分析

第一次作业

题目简述

实现只包含+,-,*,(),**表达式的化简,括号深度最大为一层。

思路简述

表达式解析部分参考了第一单元训练中的递归下降的结构,先将表达式拆成若干表达式、项和因子,然后根据从属关系合并得到答案。

对于负号,我把负号全部下传,标记到底层的因子上,这样只需要在因子记录正负号即可。

合并的时候使用Poly类来作为结果,Poly类是一个形如\(a_0\times x^{b_0}+a_1\times x^{b_1}+\dots\)的关于\(x\)的多项式,用HashMap存储指数对应的系数。

最后得到整个表达式的Poly作为答案,这样已经是合并同类项的形式。

做了几个优化:\(x**2 \rightarrow x*x\),把正系数放到第一个并且去掉"+"。

程序分析

代码规模分析

Source File Total Lines Source Code Lines
Expr.java 28 24
Factor.java 43 37
Lexer.java 46 42
MainClass.java 12 11
Number.java 23 20
Parser.java 62 55
Poly.java 109 96
Term.java 26 21
Total 349 306

代码量并不太大,最大的代码量在Poly.java,也就是多项式的处理中,因为需要写加、乘、幂等多项式计算的方法。整体来说比较平均

方法复杂度分析

method CogC ev(G) iv(G) v(G)
Expr.addTerm(Term, String) 0 1 1 1
Expr.Expr() 0 1 1 1
Expr.getPoly() 2 1 3 3
Factor.Factor() 0 1 1 1
Factor.getExp() 0 1 1 1
Factor.getOp() 0 1 1 1
Factor.getPoly() 0 1 1 1
Factor.setExp(Lexer) 5 2 4 4
Factor.setOp(String) 1 1 1 2
Lexer.getNow() 0 1 1 1
Lexer.getNumber() 3 2 3 4
Lexer.Lexer(String) 0 1 1 1
Lexer.next() 8 2 6 7
MainClass.main(String[]) 0 1 1 1
Number.getPoly() 2 2 2 2
Number.Number(String) 0 1 1 1
Parser.getExpr() 4 1 5 5
Parser.getFactor() 5 1 4 4
Parser.getTerm() 3 1 4 4
Parser.Parser(Lexer) 0 1 1 1
Poly.add(Poly) 2 1 3 3
Poly.addTerm(Integer, BigInteger) 2 1 2 2
Poly.coefToString(BigInteger, int, Integer) 8 4 2 7
Poly.expToString(Integer) 4 4 1 4
Poly.getCoef() 0 1 1 1
Poly.multiply(Poly) 3 1 3 3
Poly.neg() 1 1 2 2
Poly.output() 4 1 5 5
Poly.Poly() 0 1 1 1
Poly.Poly(BigInteger) 0 1 1 1
Poly.pow(int) 1 1 2 2
Term.addFactor(Factor) 0 1 1 1
Term.getPoly() 1 1 2 2
Term.setOp(String) 0 1 1 1
Term.Term() 0 1 1 1
Total 59 45 71 82
Average 1.69 1.29 2.03 2.34

整体复杂度并不高,有几处ev(G)较高,是使用了多个if来判断输出内容进行简化的地方。

类复杂度分析

class OCavg OCmax WMC
Expr 1.67 3 5
Factor 1.50 3 9
Lexer 2.50 5 10
MainClass 1.00 1 1
Number 1.50 2 3
Parser 2.50 3 10
Poly 2.82 7 31
Term 1.25 2 5
Total 74
Average 2.11 3.25 9.25

只有Poly类复杂度较高,原因和上面两个一样,需要进行一系列运算和输出。

类图

hw1类图

优点:

按表达式、项、因子等分类,结构比较清晰。采用递归下降方法解析,有良好的可扩展性。

缺点:

Number类中记录了String,包含了常数和\(x\),使用if判断。实际上应该将这两种再进行细分,或者是统一成系数和指数的形式。当时图方便就直接这样写了。

Poly类的各种运算是用方法实现的,这样不符合面向对象,而是面向过程了,应该对于每个运算新建类专门处理。

Bug分析

强测中获得了100分,互测中没有被Hack。

Hack了一位同学的1个bug,他在简化输出的时候遇到"-0"会只输出一个"-"。

第二次作业

题目简述

实现只包含+,-,*,(),**,\(sin()\),\(cos()\),\(sum()\)以及自定义函数的表达式的化简,多余括号深度最大为一层,\(sin()\)\(cos()\)只会出现常数和\(x^k\)\(sum()\)和自定义函数不会嵌套。

思路简述

在第一次作业的基础上迭代开发。

合并的时候使用魔改后的Poly类来作为结果,Poly类形如\(a_0\times u_0+a_1\times u_1+\dots\)。其中\(u_i\)为Unit类。用HashMap<Unit,BigInteger>存储系数。

Unit类是由\(x,sin(x^k),cos(x^k)\)若干因子相乘所得项。用一个Integer存\(x\)的指数,两个HashMap<Number,Integer>分别存\(sin()\)\(cos()\)的指数。

对于自定义函数,先把每个自定义函数解析成表达式。建立一个变量到因子的映射,化简最终表达式的时候,若碰到自定义函数,就修改映射,在自定义函数里化简。

例如,\(f(y,x)=y*2+x\),调用\(f(sin(x),x)\),此时映射为\(\{y \rightarrow sin(x),x \rightarrow x\}\)

在自定义函数里碰到变量,就根据映射替换成对应的因子。

\(sum()\)函数也可以通过枚举\(i\),把\(i\)映射成因子(一个常数)来解决。

输出时做了\(sin(0) \rightarrow 0\),\(cos(0) \rightarrow 1\),\(sin(-k) \rightarrow -sin(k)\),\(cos(-k) \rightarrow cos(k)\)的简化。

程序分析

代码规模分析

Source File Total Lines Source Code Lines
CusFunc.java 29 25
CustomFunc.java 27 22
Expr.java 32 27
Factor.java 37 29
Lexer.java 61 54
MainClass.java 22 19
Number.java 78 70
Parser.java 138 120
Poly.java 130 117
SumFunc.java 49 41
Term.java 27 22
TriFunc.java 37 32
Unit.java 163 143
Total 830 721

第二次和第一次相比代码量有了明显提升,多了一倍多,新加的函数实现起来需要还是需要一定的代码量。

主要代码在于Parser.java,Poly.java和Unit.java,整体来说还是比较平均。

方法复杂度分析

method CogC ev(G) iv(G) v(G)
CusFunc.addFactor(Factor) 0 1 1 1
CusFunc.CusFunc(String) 0 1 1 1
CusFunc.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 2 1 3 3
CustomFunc.CustomFunc(String) 0 1 1 1
CustomFunc.getPoly(HashMap<String, CustomFunc>, ArrayList) 1 1 2 2
Expr.addTerm(Term, String) 0 1 1 1
Expr.Expr() 0 1 1 1
Expr.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 3 1 4 4
Factor.Factor() 0 1 1 1
Factor.getExp() 0 1 1 1
Factor.getOp() 0 1 1 1
Factor.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 0 1 1 1
Factor.setExp(int) 0 1 1 1
Factor.setExp(String) 0 1 1 1
Factor.setOp(String) 1 1 1 2
Lexer.getNow() 0 1 1 1
Lexer.getNumber() 2 1 3 3
Lexer.getType() 3 3 4 5
Lexer.Lexer(String) 0 1 1 1
Lexer.next() 8 2 5 6
MainClass.main(String[]) 1 1 2 2
Number.equals(Object) 4 3 4 6
Number.getNum() 0 1 1 1
Number.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 5 1 5 5
Number.hashCode() 0 1 1 1
Number.Number(String) 3 1 3 3
Number.toString() 7 4 4 5
Parser.getCusFunc() 2 1 3 3
Parser.getExpr() 4 1 5 5
Parser.getFactor() 13.0 1 12 12
Parser.getSumFunc() 8 1 5 5
Parser.getTerm() 3 1 4 4
Parser.getTriFunc() 0 1 1 1
Parser.Parser(Lexer) 0 1 1 1
Poly.add(Poly) 2 1 3 3
Poly.addUnit(Unit, BigInteger) 2 1 2 2
Poly.adjust() 4 1 3 3
Poly.coefToString(BigInteger, int, Unit) 9 5 1 8
Poly.getCoef() 0 1 1 1
Poly.getNumber() 4 3 3 3
Poly.multiply(Poly) 3 1 3 3
Poly.neg() 1 1 2 2
Poly.output() 4 1 5 5
Poly.Poly() 0 1 1 1
Poly.Poly(BigInteger) 0 1 1 1
Poly.pow(int) 1 1 2 2
SumFunc.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 4 1 5 5
SumFunc.setEnd(String) 0 1 1 1
SumFunc.setFactor(Expr) 0 1 1 1
SumFunc.setStart(String) 0 1 1 1
SumFunc.setVar(String) 0 1 1 1
SumFunc.SumFunc() 0 1 1 1
Term.addFactor(Factor) 0 1 1 1
Term.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 1 1 2 2
Term.setOp(String) 0 1 1 1
Term.Term() 0 1 1 1
TriFunc.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 4 1 4 4
TriFunc.setFactor(Factor) 0 1 1 1
TriFunc.TriFunc(String) 0 1 1 1
Unit.addCos(Number, int) 3 2 2 3
Unit.addSin(Number, int) 2 1 2 2
Unit.adjust() 15 1 7 8
Unit.checkZero() 0 1 1 1
Unit.equals(Object) 4 3 4 6
Unit.getCoss() 0 1 1 1
Unit.getExp() 0 1 1 1
Unit.getSins() 0 1 1 1
Unit.hashCode() 0 1 1 1
Unit.multiply(Unit) 4 1 5 5
Unit.toString() 10 2 8 9
Unit.Unit(int) 0 1 1 1
Total 147 89 168 186
Average 2.07 1.25 2.36 2.61

整体复杂度不高。Parser.getFactor()的复杂度较高,是因为这里对Factor的种类进行了分类讨论。

类复杂度分析

class OCavg OCmax WMC
CusFunc 1.66 3 5
CustomFunc 1.50 2 3
Expr 2.00 4 6
Factor 1.14 2 8
Lexer 2.40 5 12
MainClass 2.00 2 2
Number 3.00 5 18
Parser 3.28 8 23
Poly 2.83 8 34
SumFunc 1.66 5 10
Term 1.25 2 5
TriFunc 2.00 4 6
Unit 3.00 9 36
Total 168
Average 2.36 4.53 12.92

Parser类的平均复杂度较高,因为有很多分类讨论。

Poly类和Unit类的总复杂度较高,因为方法比较多。

类图

hw2类图

为了简化,类图中所有getPoly(HashMap,HashMap)实际上两个HashMap分别为HashMap<String, CustomFunc>和HashMap<String, Factor>,分别存的是函数的映射和变量的映射。

优点:

使用HashMap映射的方式来代入变量和函数,比较方便、好写。

用adjust方法将Poly和Unit进行整理,合并同类项,输出更加简化。

缺点:

getPoly方法有些冗长,可以新建一个类专门存储两个映射关系,这样会美观很多。

Bug分析

强测中正确性通过了所有测试点,获得了95.3903分(没有处理\(sin^2+cos^2\)),互测中没有被Hack。

Hack了一位同学的1个bug,他把\(sin(0)\)处理成了\(1\)

第三次作业

题目简述

实现只包含+,-,*,(),**,\(sin()\),\(cos()\),\(sum()\)以及自定义函数的表达式的化简,有括号嵌套,\(sin()\)\(cos()\)可以是任意因子,\(sum()\)不会嵌套,自定义函数可能嵌套。

思路简述

在第二次作业的基础上迭代开发。

实际上第二次作业的架构已经支持括号嵌套和自定义函数嵌套。只需要增加三角函数的因子种类。

相当于在三角函数里放了Poly类。把Unit类的HashMap改成<Poly,Integer>即可。

化简仍然是之前的几类,但是相对复杂,需要注意深克隆HashMap。

Unit提取负号的时候,Poly里有可能全是正号,有可能全是负号,有可能两个都有。如果全是负号,那么提负号出来会减少长度。

但是这样也会影响外部的符号,负号可以是奇数个的时候可以把正负号都有的Poly也取反,让外部回正。

程序分析

代码规模分析

Source File Total Lines Source Code Lines
CusFunc.java 29 25
CustomFunc.java 27 22
Expr.java 32 27
Factor.java 37 29
Lexer.java 61 54
MainClass.java 30 26
Number.java 39 35
Parser.java 138 120
Poly.java 166 151
SumFunc.java 50 42
Term.java 27 22
TriFunc.java 37 32
Unit.java 211 190
Total 884 775

代码规模和第二次作业差不多,没有大的改动。

方法复杂度分析

method CogC ev(G) iv(G) v(G)
CusFunc.addFactor(Factor) 0 1 1 1
CusFunc.CusFunc(String) 0 1 1 1
CusFunc.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 2 1 3 3
CustomFunc.CustomFunc(String) 0 1 1 1
CustomFunc.getPoly(HashMap<String, CustomFunc>, ArrayList) 1 1 2 2
Expr.addTerm(Term, String) 0 1 1 1
Expr.Expr() 0 1 1 1
Expr.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 3 1 4 4
Factor.Factor() 0 1 1 1
Factor.getExp() 0 1 1 1
Factor.getOp() 0 1 1 1
Factor.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 0 1 1 1
Factor.setExp(int) 0 1 1 1
Factor.setExp(String) 0 1 1 1
Factor.setOp(String) 1 1 1 2
Lexer.getNow() 0 1 1 1
Lexer.getNumber() 2 1 3 3
Lexer.getType() 3 3 4 5
Lexer.Lexer(String) 0 1 1 1
Lexer.next() 8 2 5 6
MainClass.main(String[]) 3 1 3 3
Number.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 5 1 5 5
Number.Number(String) 3 1 3 3
Parser.getCusFunc() 2 1 3 3
Parser.getExpr() 4 1 5 5
Parser.getFactor() 13 1 12 12
Parser.getSumFunc() 8 1 5 5
Parser.getTerm() 3 1 4 4
Parser.getTriFunc() 0 1 1 1
Parser.Parser(Lexer) 0 1 1 1
Poly.add(Poly) 2 1 3 3
Poly.addUnit(Unit, BigInteger) 2 1 2 2
Poly.adjust() 4 1 3 3
Poly.coefToString(BigInteger, int, Unit, boolean) 9 5 2 8
Poly.equals(Object) 3 3 2 4
Poly.getCoef() 0 1 1 1
Poly.getOp() 6 3 3 6
Poly.hashCode() 0 1 1 1
Poly.multiply(Poly) 3 1 3 3
Poly.neg() 1 1 2 2
Poly.Poly() 0 1 1 1
Poly.Poly(BigInteger) 0 1 1 1
Poly.pow(int) 1 1 2 2
Poly.toString(boolean) 9 3 7 9
SumFunc.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 4 1 5 5
SumFunc.setEnd(String) 0 1 1 1
SumFunc.setFactor(Expr) 0 1 1 1
SumFunc.setStart(String) 0 1 1 1
SumFunc.setVar(String) 0 1 1 1
SumFunc.SumFunc() 0 1 1 1
Term.addFactor(Factor) 0 1 1 1
Term.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 1 1 2 2
Term.setOp(String) 0 1 1 1
Term.Term() 0 1 1 1
TriFunc.getPoly(HashMap<String, CustomFunc>, HashMap<String, Factor>) 4 1 4 4
TriFunc.setFactor(Factor) 0 1 1 1
TriFunc.TriFunc(String) 0 1 1 1
Unit.addCos(Poly, int) 3 2 2 3
Unit.addSin(Poly, int) 2 1 2 2
Unit.adjust() 0 1 1 1
Unit.adjustCos() 10 1 7 7
Unit.adjustSin() 16 1 12 13
Unit.checkZero() 0 1 1 1
Unit.equals(Object) 4 3 4 6
Unit.getCoss() 0 1 1 1
Unit.getExp() 0 1 1 1
Unit.getFactorCnt(BigInteger) 5 2 3 6
Unit.getSins() 0 1 1 1
Unit.hashCode() 0 1 1 1
Unit.multiply(Unit) 4 1 5 5
Unit.toString(boolean) 11 2 8 10
Unit.Unit(int) 0 1 1 1
Total 165 90 181 206
Average 2.29 1.25 2.51 2.86

和第二次作业相比,主要多了一个Unit.adjustSin()这个复杂度高的方法,这里是对\(sin()\)里面的Poly进行调整,提取负号。这里涉及的判断比较多。

类复杂度分析

class OCavg OCmax WMC
CusFunc 1.66 3 5
CustomFunc 1.50 2 3
Expr 2.00 4 6
Factor 1.14 2 8
Lexer 2.40 5 12
MainClass 3.00 3 3
Number 4.00 5 8
Parser 3.28 8 23
Poly 3.14 8 44
SumFunc 1.66 5 10
Term 1.25 2 5
TriFunc 2.00 4 6
Unit 3.20 9 48
Total 181
Average 2.51 4.62 13.92

Number、Parser、Poly、Unit类的平均复杂度比较高,

类图

hw3类图

和第二次作业的类图基本完全一样,只有Poly和Unit进行了少量改动。

Bug分析

强测中正确性通过了所有测试点,获得了95.553分。

互测中被Hack了1个同质Bug,处理\(sum()\)的时候上下界范围溢出int。

Hack了三位同学的共8个bug(五个同质Bug):

1、和自己的Bug一样

2、\(sin((-x))\)没有加括号

3、括号嵌套时碰到指数解析错误

4、没有处理\(sum()\)中是\(i**k\)的情况

5、\(sum()\)中碰到\(i**k\)\(i\)是负数,处理成\(-i**k\)

测试策略

自动化测试

根据形式化描述,很容易写出一个简陋的造数据的程序,使用一些参数对产生的数据进行控制。

可以使用Python的sympy库,看输出和输入表达式是否等价。

Hack策略

实际上每个人的代码量都不小,都认真读一遍是很难做到的。

在Hack的时候,我采用的是自动和手动结合的形式。

自动就是用自动测试程序把每个人程序都跑几千组数据。基本能查出那些比较明显的Bug。

手动是预先构造一些覆盖可能造成Bug的数据,或者根据题目的数据范围构造边界数据,针对这些数据测试。

然后随机挑选几份代码细看,主要查看输入处理和输出化简的部分(中间处理如果有Bug,很容易在自动测试发现),检查是否存在Bug。

这样的策略还是比较有效的,在三次互测中把能找到的Bug都找到了。(但自己的Bug没有及时找出来,是因为没有正确理解题目条件,在互测时才发现)

总结

架构分析

在第一次作业时,如果使用栈或者正则表达式,也可以很好地完成,但是扩展会比较麻烦。相比之下,第一次训练提供的递归下降的方法就有很强的扩展性。

使用多项式来存储结果也是比较明智的选择,避免了合并同类项的麻烦。

第二次作业,加入了三角函数、自定义函数和求和函数,三角函数意味着存储需要改进,而自定义函数和求和函数主要是要解决替换变量的问题。

一种暴力的方法是直接字符串替换,但是这样扩展性也很低,并且很不面向对象。所以我使用变量到因子的映射递归下去替换。

在这样的设计下,第三次作业实际上十分简单了,基础功能都已经实现了,只需要继续改进“多项式”的存储方法即可。

三次作业中我几乎没有重构代码,都是一步步迭代开发的,整体还算比较顺利。

心得体会

本次作业是我在面向对象程序设计方面的第一次尝试,尽管程序的很多地方看起来还是面向过程的,但我学到了很多面向对象的思想和Java语言的特性和技巧。

真正的面向对象,应该要对于每个运算符、每个表达式等都建立一个对象,通过对象的状态得到结果。在我的代码中还是有所欠缺。

要写出这样的大型代码,架构是非常重要的。在前两次作业中,我都花了1到2天进行构思,然后才开始写,实际写代码的时间不长。因此,我对自己的架构还是比较满意的,这也为我第三次作业的快速通关奠定了基础。

作业中题目描述也比较复杂,并且在迭代过程中数据要求有变化,需要先仔细阅读题目细节。我第三次作业中就没有仔细阅读,导致出现了一个小Bug。

在交流中我发现一些同学在化简方面下了很大的功夫,使用搜索等方法找到长度最短的结果,我十分佩服。不过也有同学弄巧成拙,在优化过后正确性出现了问题。

在总结的理论课上,老师提到要建立回归测试集,就是每改一次就要把之前测试过的数据都测一遍。我非常认同这样的做法,这样也可以最大程度避免优化后正确性出错。

posted @ 2022-03-23 20:58  Oshwiciqwq  阅读(121)  评论(1编辑  收藏  举报