BUAA OO 第一单元总结
一、第一次作业
(1)类结构设计
第一次题目架构比较简单,具体结构设计如下(图1):
- 基本数据类型:
Polynomial
:用Hashmap<int(指数), BigInteger(系数)>
将表达式/项/因子数据内容统一形式- 运算方法:加法
add
、乘法mul
、乘方pow
- 格式化输出:
toString
方法将其内容输出(含正项提前处理),addPowerFunc
负责将每个项转化为字符串形式。
- 运算方法:加法
- 表达式解析:
Lexer
:使用正则表达式(?<num>(?<=[^\\dx)]|^)[+-]?\\d+|x)|(?<opt>\\*\\*|[)(+\\-*])
匹配每个有效字符串,得到形如{"-","-","-2","*","(","x",")"}
的字符串序列parser
:使用递归下降法对得到的字符串序列进行解析,得到表达式/项/因子实例对象。
- 表达式储存
Expr
:表达式- 运算方法:
update
将terms
中的Polynomial
相乘后,在乘power
次幂并记录到res
中;negate
将res
取负。 - 更新方法:
addTerm
增加新的项 - 输出方法:
toString
调用res.toString
- 运算方法:
Term
:项- 运算方法:
update
将factors
中的Polynomial
相加并记录到res
中,negate
将res
取负。 - 更新方法:
addFactor
增加新的因子
- 运算方法:
Factor
(接口):包含Expr
和ValueFactor
- 运算方法:
negate
将res
取负 - 取值方法:
getRes
获得Factor
中数据值 - 接口实现类:
Expr
:表达式ValueFactor
:常数(幂次为0)以及幂函数
- 运算方法:
(2)度量分析
从图中可以看出Polynomial
和Parser
类OCavg较高,高复杂度在于递归调用(Parser
)和双重循环(Polynomial
)。
(3)Hack情况
- 己方bug分析:本次作业个人在强测和弱侧中均未出现bug
- 对方bug分析:
- 解析
- -
时出错
- 解析
二、第二次作业
(1)类结构设计
第二次作业增加了三角函数、自定义函数、求和函数,代码量相较于第一次有明显增长,不过整体架构上没有太大变化。值得一提的是在处理自定义函数/求和函数时,个人的处理方法是先把原表达式以字符串形式储存下来再用类似Parser的方法解析,由于这样Function和Parser的方法有相似之处(也为了后续嵌套表达式处理),因此将Parser和Function进行了合并(由于耦合过高,第三次作业将Function类进行了解耦)
- 基本数据类型:
Polynomial
:用Hashmap<Hashmap<Factor, int(指数)>, BigInteger(系数)>
将表达式/项/因子数据内容统一形式。其中Factor
有且仅有三角函数、幂函数、常数。****- 运算方法:加法
add
、乘法mul
、乘方pow
- 格式化输出:
toString
方法将其内容输出(含正项提前处理),addPowerFunc
负责将每个项转化为字符串形式。 - 判断相等方法:
hashCode
和equal
- 化简方法:
replaceSinX2/replaceCosX2
:负责形如 \(asin(x)^2+bcos(x)^2+c\) 平方和(其中\(a\)、\(b\)、\(c\)、\(x\))均为任意因子/项。replaceSinX4
:负责形如 \(a(sin(x)^4-cos(x)^4)\)的化简。buildSinXnPowers/buildCosXnPowers/buildConstXnPowers
:在化简过程中构造含 \(sin(x)^n\) / \(cos(x)^n\) / 去掉对应三角函数的项
- 运算方法:加法
- 表达式解析:
Lexer
:使用正则表达式((?<=[^\w)]|^)[+-]?\d+)|(\*\*|[)(+\-*])|(sin|cos|sum)|([a-z,])
匹配每个有效字符串,得到形如{"-","-","-2","*","(","x",")"}
的字符串序列(New!)Function
:- 解析方法:
parseXXX
:使用递归下降法对得到的字符串序列进行解析,得到表达式/项/因子实例对象或者对幂次解析。accessSign
:对表达式/项首项符号的解析
- 计算方法:
calculate
:调用后传入参数,得到自定义函数的值(Expr
)。
- 解析方法:
- 表达式储存
Expr
:表达式- 运算方法:
updatePower
将res
乘power
次幂并记录到res
中;update
将terms
中的Polynomial
相乘后记录到res
中;negate
将res
取负。 - 更新方法:
addTerm
增加新的项 - 输出方法:
toString
调用res.toString
- 化简方法:
simplyfy
将调用res.simplyfy
- 判断相同方法:
hashCode
和equal
- 复制方法:
deeplClone
创造一个新的Expr
- 运算方法:
Term
:项- 运算方法:
update
将factors
中的Polynomial
相加并记录到res
中,negate
将res
取负。 - 更新方法:
addFactor
增加新的因子 - 判断相等方法:
hashCode
和equal
- 运算方法:
Factor
(接口):negate
:将res
取负getRes
:获得Factor
中数据值deepClone
:深拷贝setPower
:设置Factor
的幂次- 接口实现类
Expr
:表达式- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 判断相等方法:
Var
:幂函数- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 判断相等方法:
Constant
:常数- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 判断相等方法:
TtrigonometricFactor
:三角函数- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 子类:
Sin
:正弦函数Cos
:余弦函数
- 判断相等方法:
(2)度量分析
从图中可以看出Poly
类的OCavg和WMC较高,Function
类(原Parser
类)的WMC较高,原因同第一次作业。
(3)Hack情况
- 己方bug分析:本次作业个人在强测未出现bug,互测出现两个bug
sin(0)**0
解析出错:原因是如果检查到sin(0)
时直接返回常数0,未考虑幂次- 化简含
cos(x)**2
的表达式时出错:原因是构建时把含cos
的项误写成含sin
的项
- 对方bug分析:
sin(0)**0
解析出错sin(-1)**2
解析出错(底数换成-sin(1)后没有考虑幂次对最终符号的影响)
三、第三次作业
第三次作业允许三角函数/自定义函数的嵌套,相较于第二次作业几乎没有改动,仅仅对Poly类和Function类做了解耦,提取了部分方法,同时改变了一些方法/类名,增加可读性。
(1)类结构设计
- 基本数据类型:
Polynomial
:用Hashmap<Hashmap<Factor, int(指数)>, BigInteger(系数)>
将表达式/项/因子数据内容统一形式。其中Factor
有且仅有三角函数、幂函数、常数。****- 运算方法:加法
add
、乘法mul
、乘方pow
- 格式化输出:
toString
方法将其内容输出(含正项提前处理),addPowerFunc
负责将每个项转化为字符串形式。 - 判断相等方法:
hashCode
和equal
- 判断数据类型方法:
equalX2
、equalConstant
、equalExpr
- 运算方法:加法
- PolySimplyfy:化简方法类
handleSinX2/handleCosX2
:负责形如 \(asin(x)^2+bcos(x)^2+c\) 平方和(其中\(a\)、\(b\)、\(c\)、\(x\))均为任意因子/项。handleSinX4
:负责形如 \(a(sin(x)^4-cos(x)^4)\)的化简。buildSinXnPowers/buildCosXnPowers
:在化简过程中构造含 \(sin(x)^n\) / \(cos(x)^n\) / 去掉对应三角函数的项replaceFactor
:替换项中的Factor
handleSinXCosX
:处理Sin
二倍角化简handleCos2X
:处理Cos
二倍角化简
- 表达式解析:
Lexer
:使用正则表达式((?<=[^\w)]|^)[+-]?\d+)|(\*\*|[)(+\-*])|(sin|cos|sum)|([a-z,])
匹配每个有效字符串,得到形如{"-","-","-2","*","(","x",")"}
的字符串序列(New!)Parser
:- 解析方法:
parseXXX
:使用递归下降法对得到的字符串序列进行解析,得到表达式/项/因子实例对象或者对幂次解析。accessSign
:对表达式/项首项符号的解析
- 解析方法:
Function
:- 计算方法:
calculate
:调用后传入参数,得到自定义函数的值(Expr
)
- 计算方法:
- 表达式储存
Expr
:表达式- 运算方法:
updatePower
将res
乘power
次幂并记录到res
中;update
将terms
中的Polynomial
相乘后记录到res
中;negate
将res
取负。 - 更新方法:
addTerm
增加新的项 - 输出方法:
toString
调用res.toString
- 化简方法:
simplyfy
将调用res.simplyfy
- 判断相同方法:
hashCode
和equal
- 复制方法:
deeplClone
创造一个新的Expr
- 运算方法:
Term
:项- 运算方法:
update
将factors
中的Polynomial
相加并记录到res
中,negate
将res
取负。 - 更新方法:
addFactor
增加新的因子 - 判断相等方法:
hashCode
和equal
- 运算方法:
Factor
(接口):negate
:将res
取负getRes
:获得Factor
中数据值deepClone
:深拷贝setPower
:设置Factor
的幂次- 接口实现类
Expr
:表达式- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 判断相等方法:
Var
:幂函数- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 判断相等方法:
Constant
:常数- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 判断相等方法:
TtrigonometricFactor
:三角函数- 判断相等方法:
hashCode
和equal
- 输出方法:
toString
- 子类:
Sin
:正弦函数normalize
:标准化方法,将Sin中内容转化为统一格式(改变正负)
Cos
:余弦函数normalize
:标准化方法,将Cos中内容转化为统一格式(改变正负)
- 判断相等方法:
(2)度量分析
从图中可以看出Poly
类和PolySimplyfy
的OCavg和WMC较高,Parser的WMC较高,Poly
和Parser
原因同第一次作业,PolySimplyfy
原因为每次化简时需要遍历所有键值对,同时一直化简到无法化简为止。
(3)Hack情况
本次强测结果比较惨痛,根本原因在于课下测试不充分。
- 己方bug分析:本次作业个人在强测和互测共出现3个bug
Poly
中equalX2
方法出错:未考虑x^2
前系数-sin(x)**0
解析出错:未考虑三角函数本身负号getContentPoly
方法复杂度过高,需要多次调用mul
等函数,导致三角函数嵌套过多是TLE
- 对方bug分析:
- 用
int
类型 解析求和函数的起始或结束常数 sin((-x*x))
化简出错sum
中出现i**0
出错
- 用
四、架构设计体验
在第一次作业时,个人采取的方法为传统的用栈解析表达式,结果中测没过(后来发现是正则表达式的问题)。于是又用递归下降方法重新做了一遍,结果三次作业都没有进行大规模重构。现在想想如果采取用栈解析表达式的方法会导致方法冗长,每加入新的运算符都要对多个函数进行改写,不符合面向对象设计思想
在第二次作业时遇到最大的问题就是如何表示数据(多项式),在分析第一次架构的表示数据方法使用了Hashmap<Hashmap<Factor, int>, BigInteger>
对数据进行存储。同时,完成基本代码编写花费了近乎一天半的时间对代码进行小范围重构(例如提取函数、分解函数、提取父类、重命名等等)(这一过程中《重构:改善既有代码设计》一书对我启发颇深)
在第三次作业时由于第二次作业的代码基本上已经能满足需求,所以在简单对部分类进行解耦后,花费了更多时间在提高性能上,但是却疏于对更改后的代码进行测试,导致出现了细微但较为严重的BUG,应当进行深刻反省。
五、感想&小结
Unit1说实话个人做起来还是比较艰难的,主要原因有以下几点:
- 对文法分析的不熟悉,花费了6~7个小时理解并实现了递归下降法
- 对面向对象的思想不熟悉,导致一部分代码在一开始不符合面向对象的思想,花费了较多时间阅读《重构:改善既有代码设计》并进行重构
- 对测试方法不熟悉,导致第三次作业出现了严重BUG
虽然艰难,不过在完成本单元的学习后还是收获满满:)