北航2022面向对象第一单元:表达式解析和化简
北航2022面向对象第一单元:表达式解析和化简
1. 发现的典型问题
1.1 对象深拷贝
在使用对象时,应该尤其注意对象的属性是否在各种操作下都保持不变。特别是那些管理其他对象的对象。如果两个容器类储存了相同的对象引用,其中一个修改时,会把另一个容器中的对象一起修改,从而导致不可知的后果。
就这三次做的作业而言,时间上的要求不高,因此可以把所有管理数据的类全部设置成不可变的。或者获得一个新的对象,必须完全地拷贝原来对象的全部信息,包括容器内部的对象必须是地址不同的。
1.2 字符串提取和解析
比较好的字符串解析方法是递归下降,这样就不用在一个方法内处理所有的问题,一个方法只用处理一种运算即可,逻辑比较简单和清楚。但是这样就必须知道目前顶层是哪一种运算。
可以使用栈来判断顶层的运算。比如要判断该层是否是加法,可以遍历字符串,如果遇到左括号就压栈,遇到右括号就弹栈,只有遇到加号,而且此时栈为空时,才表示这个加号在顶层。
同样的方法可以用来分割函数的参数。函数参数是用逗号分割的,而且参数可以是其他函数的调用。我听到一种做法处理函数参数,就是把每两个逗号作为分隔符,每组分别判断是否是分隔符。这样时间开销相当大,而且分开的字符串形式复杂,不好判断。可以使用栈的方法解析函数参数字符串。如果遇到逗号,而且此时栈为空,就说明这个逗号一定是顶层的,可以作为分隔符。
1.3 加减号和正负号的判断
在第一次作业时,很多同学遇到了问题,不知道怎么区分加减号和正负号。在第一次作业时,我用到的是无脑判断。如果遇到一个 '+' 号,而且此时栈为空,就直接递归解析符号左右的字符串。如果左右都是合法的字符串,就可以认为这是个加号。
这种做法应该是没有逻辑问题的,根据递归分析,错误的字符串会在底层被识别,把错误信息逐层上传回来。但是在第二次作业中,因为这种方法我被hack了两次,然而不是报逻辑错误,而是超时。这是因为输入的字符串非常长,每个项都有一个加号和一个正号。这样我处理每个项,都必须递归分析整个字符串。这种方法的时间复杂度是指数函数,字符串稍微长一点耗时就非常长了。因此我被迫做了特判,就是字符串的结尾不能是 '+' ,'-','*' 号,否则直接返回字符串非法。因为输入的字符串一定是合法的,每次递归下降都是有效操作,这样就保证了每次操作的时间开销都是必要的。
2. 分析自己的程序
2.1 数量度量
指标:
指标名 | 作用对象 | 含义 | 方向 |
---|---|---|---|
LOC | 类/方法 | 代码行数 | 越小越好 |
LCOM | 类 | 类中内聚度的缺乏,值越小说明内聚度好,符合高内聚要求 | 越小越好 |
FANIN | 类 | 扇入表示调用该模块的上级模块的个数,值越大表示复用性好。 | 越大越好 |
CC | 方法 | 圈复杂度,值越大说明程序代码质量低,且难以测试和维护 | 越小越好 |
第一次作业
类:
类名 | 简介 | LOC | LCOM | FANIN |
---|---|---|---|---|
ExpFactory | 用来构造表达式 | 154 | 0.625 | 0 |
Expression | 表达式 | 102 | 0 | 3 |
Term | 表达式中的项 | 62 | 0.286 | 1 |
Main | 主类 | 10 | -1 | 0 |
方法(只记录主要方法):
类名 | 方法名 | 简介 | LOC | CC |
---|---|---|---|---|
ExpFactory | createExp | 创建表达式 | 45 | 11 |
createExpByTerm | 创建项 | 46 | 12 | |
createExpByFactor | 创建因子 | 32 | 7 | |
simplifyStr | 简化字符串 | 3 | 1 | |
Expression | toSimpleStr | 简化字符串 | 28 | 7 |
power | 乘方 | 18 | 4 | |
multiExp | 乘表达式 | 18 | 4 | |
subExp | 减表达式 | 4 | 1 | |
negate | 取负号 | 5 | 2 | |
addExp | 加表达式 | 14 | 3 | |
addTerm | 添加一个项 | 9 | 2 | |
Term | toSimpleStr | 简化字符串 | 40 | 13 |
第二、三次作业
因为第二、三次作业的设计思路基本一样,所以用第三次作业来说明
类:
类名 | 简介 | LOC | LCOM | FANIN |
---|---|---|---|---|
Main | 主类 | 47 | -1 | 0 |
AddSubFunc | 加减函数 | 23 | 0 | 3 |
Func | 函数基类 | 117 | 0 | 10 |
FuncFactory | 用来构造函数 | 227 | 0 | 2 |
FuncLib | 自定义函数库 | 105 | 0 | 2 |
MulFunc | 乘法函数 | 18 | -1 | 2 |
NumFunc | 常元函数 | 19 | 0 | 4 |
OpFunc | 正负函数 | 19 | 0 | 2 |
PowerFunc | 指数函数 | 23 | 0 | 2 |
SumFunc | 求和函数 | 64 | 0.5 | 2 |
TriFunc | 三角函数 | 23 | 0 | 3 |
VarFunc | 变元函数 | 23 | 0 | 6 |
Output | 用来输出的类 | 150 | 0 | 3 |
StrUtil | 字符串工具类 | 37 | -1 | 3 |
Term | 输出类的组成单元 | 157 | 0 | 2 |
方法(只记录主要方法):
类名 | 方法名 | 简介 | LOC | CC |
---|---|---|---|---|
Func | toOutput | 得到输出的对象 | 52 | 12 |
replaceFormal | 将所有变元替换为新函数 | 12 | 4 | |
replace | 将该函数节点替换为新函数 | 10 | 2 | |
duplicate | 复制 | 23 | 4 | |
FuncFactory | createFunc | 创建函数 | 47 | 12 |
createSumFunc | 创建求和函数 | 33 | 6 | |
createTriFunc | 创建三角函数 | 15 | 3 | |
createOpFunc | 创建符号函数 | 13 | 3 | |
createAddSubFunc | 创建加减法函数 | 29 | 7 | |
createPowerFunc | 创建指数函数 | 31 | 7 | |
createMulFUnc | 创建乘法函数 | 33 | 8 | |
createVarFunc | 创建变元 | 10 | 2 | |
createNumFunc | 创建常元 | 10 | 2 | |
FuncLib | addFunction | 添加自定义函数形式 | 5 | 1 |
createCustomFunc | 创建自定义函数调用 | 35 | 7 | |
getRealParamStrings | 解析实参字符串 | 45 | 12 | |
Output | duplicate | 复制 | 7 | 2 |
getString | 获得化简字符串 | 26 | 7 | |
power | 乘方 | 15 | 3 | |
mul | 乘法 | 22 | 6 | |
sub | 减法 | 5 | 1 | |
add | 加法 | 19 | 5 | |
negate | 取负 | 8 | 2 | |
addTri | 添加三角函数 | 10 | 3 | |
addNum | 添加常数 | 4 | 1 | |
addVar | 添加变量 | 4 | 1 | |
isNumFactor | 是否是常量因子 | 7 | 2 | |
isVarFactor | 是否是变量因子 | 17 | 5 | |
Term | duplicate | 复制 | 10 | 3 |
getString | 获得化简字符串 | 53 | 16 | |
mul | 乘法 | 30 | 7 | |
isNumber | 是否是常数 | 9 | 3 | |
isPower | 是否只含有变量指数 | 9 | 3 | |
isSimilar | 是否是同类项 | 6 | 2 | |
StrUtil | noBlank | 去除空白 | 6 | 2 |
noBracket | 去除两端对应的括号 | 29 | 8 |
2.2 发现的bug位置(第二、三次作业)
- 笔误
在 FuncFactory.createPowerFunc() 方法中,使用到了很大的循环,循环中的一个判断条件写错。此处代码31行,是同类方法行数第二长的。 - 超时
在 FuncFactory.createFunc() 中没有特判,导致递归调用超时。该方法47行,是该类方法中最长的,该方法的CC为12,也是该类方法中最大的。这导致这个函数不好调试,而且一旦出错会影响到很多地方。 - 重写错误
在 MulFunc.duplicate() 方法中,重写Func.duplicate(),忘记递归调用乘法两边的复制方法,导致该方法复制不完全,没有达到深拷贝的效果。
2.3 结构图
-
第一次作业
-
第二、三次作业
3. 测试策略
因为时间所限,只有第一次作业做了比较完全的测试。因为整个结构是比较有规律的,二、三次作业根据1.1的易错点进行测试,即可基本保证不出问题。完全测试基本上是根据形式化表述,从底向上,逐层测试。比如先测试各种因子对不对,然后构造项进行测试,最后构造表达式进行测试。
因为时间所限,我没有具体针对代码进行互测,只是随意交了一些数据进行测试,当然也没有查到别人的bug。我认为,最大的问题可能就是对象深拷贝,其次是函数多次调用和循环调用的问题。如果这两个点都是正确的,我相信其他地方也不会有很大问题。
4. 设计体验
4.1 架构迭代
因为第一次作业只有多项式,所以我采用了很简单的方法解析,专门针对多项式。然后根据助教的提醒,后期的作业需要更大范围的抽象,所以只能重构了。第二次作业因为时间不够,加上整个架构全部要重写,所以作业效果并不好。但是架构设计好了以后,为第三次作业带来了很大的方便。第三次作业在第二次的基础上,修改行数估计只有二三十行,修改时间估计一两个小时。而且在强测和互测也没有什么错误。我认为只要把易错点都检查过了,就不会有什么问题。
4.2 心得体会
第一单元是进入OO课程的第一次训练。通过三次作业,我逐步地提高设计的抽象程度,以应对增加的需求。我原先认为,第一次作业和第二、三次的衔接不够,针对第一次作业的设计结构到后面几乎都要重构,否则会变得非常复杂。但是现在我认为这也不是什么问题。因为之后确实有可能会提出各种需求,而这是在之前的作业中不能预计的。所有的远见都是有限的,不可能考虑到所有的情况。我个人认为,程序首先要满足当前的需求和性能,其次才是预留未来的需求。我觉得要抓住主要矛盾和主要方面,优先满足当前需求,该重构时就重构,我听说unix还是linux这么庞大的系统都重写了几次。
关于作业量和难度,我因为之前看过一些Java的教程,所以感觉难度不是很大,只是作业量有点多,需要平衡这个课和其他的课的时间分配。但是我也听说有的同学感觉有些难度,可能有一次作业做得不是特别好。如果我之前没有看过相关知识,可能也会感觉比较吃力。我觉得这实在是没办法的事情,想要用好一门语言,至少要知道语法和常用用法。在pre中有比较好的练习,因为pre不是强制的任务,可能有些同学就没做了。