oo第一单元总结
作者:互联网
为期三周的oo的unit1终于过去了,在这三周中在oo上付出的时间是最多的,同时收获也是最多的。现在来总结一下这个单元。
最终问题描述
-
表达式→ 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
-
项 → [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
-
因子→ 变量因子 | 常数因子 | 表达式因子
-
变量因子→ 幂函数 | 三角函数 | 自定义函数调用 | 求和函数
-
常数因子 → 带符号的整数
-
表达式因子→ '(' 表达式 ')' [空白项 指数]
-
幂函数→ (函数自变量|'i') [空白项 指数]
-
三角函数→ 'sin' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数] | 'cos' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]
-
指数→ '**' 空白项 ['+'] 允许前导零的整数
-
带符号的整数 → [加减] 允许前导零的整数
-
允许前导零的整数→ (0|1|2|…|9){0|1|2|…|9}
-
空白项→ {空白字符}
-
空白字符→
`(空格) |
\t` -
加减 → '+' | '-'
-
自定义函数定义 → 自定义函数名 空白项 '(' 空白项 函数自变量 空白项 [',' 空白项 函数自变量 空白项 [',' 空白项 函数自变量 空白项]] ')' 空白项 '=' 空白项 函数表达式
-
函数自变量→ 'x' | 'y' | 'z'
-
自定义函数调用 → 自定义函数名 空白项 '(' 空白项 因子 空白项 [',' 空白项 因子 空白项 [',' 空白项 因子 空白项]] ')'
-
自定义函数名→ 'f' | 'g' | 'h'
-
求和函数→ 'sum' '(' 空白项 'i' 空白项',' 空白项 常数因子 空白项 ',' 空白项 常数因子 空白项 ',' 空白项 求和表达式 空白项 ')'
-
函数表达式 → 表达式
-
求和表达式 → 因子
基本思路
题目可以简述为输入一个表达式,输出化简结果。因此我们基本的思路可以是:
<div align = "center">
读入预处理存储并解析简化输出</div>
预处理
这一部分首先就是去空白字符、将**转化为~等基本操作便于之后操作。之后做了两件比较重要的操作——化简加减号和表达式的替换。
加减号的问题在第一次作业中十分困扰我,因为它既可以作为一个运算符,又可以作为因子、项、表达式的正负号,这种双重属性较难处理。我所了解的大概有两种方法:一,把重复的++、+-、-+、*+、(+等等全部替换,例如:++变为+、+-变为-、*+变为*;二,在解析的时候识别出到底是表示正负还是加减。我选择了看似简单但坑很多的第一种,因为有的情况考虑不全面,所以替换不完全,直到hw2的时候仍然发现了这方面的问题。
表达式替换可以说是我这次作业中让自己最不满意的地方了,虽然助教老师多次推荐表达式解析,而且表达式解析的实现也并不复杂,但由于我自己比较懒,只想用自己熟悉的方法,所以选择了字符串替换。其实字符串替换在这次作业来说并不容易出错,只要替换的时候注意顺序,多加括号并不会出现方法上的问题,但是从可扩展性和逻辑上来说不够优秀。
存储并解析
这部分是整个题目中最重要的部分,在我的设计中,这两部分是完全分开设计的,存储部分提供容器和方法,解析部分利用容器来存储和运算表达式。
存储
我们读入表达式之后肯定要选择一种存储方式,因为字符串又不能进行加减乘等运算,所以我们的目标在于找到一种可以在其上定义加、减、乘、指数运算的存储方式。
第一次作业中,由于所有能出现的形式都可以表示为多项式,因此一个HashMap<Integer,Integer>就可以很轻松的存储这些表达式,也很好定义计算。
第二次作业中,由于出现了三角函数,之前的存储方式不能满足。由于规定了三角函数内部只能是常数或幂函数,所以我当时是以一种比较傻的方式来存储——先定义一个Tri表示三角函数内部可能出现的情况,然后定义了一个包含一个index(指数)+ 两个HashMap<Tri,Integer>的Storage类,最终定义了一个包含一个HashMap<Storage,Integer>的Container类。这么做有一个极大的问题就是当下一次三角函数里面不确定是什么的时候这种方法就失效了,可扩展性极差。
第三次作业中,我仍然想采用HashMap的存储方法,由于三角函数中的表达式不定(可以多层嵌套),因此第二次的存储方式显然不行。这一次构建了一个内部类(Inner),其中包含一个HashMap<String, Integer>,它的作用是:可以表示任意(String)**(Integer)的形式的式子,由于我们使用了String,因此无论是x、sin(x)、cos(1),还是是多层嵌套,例如:sin(sin(cos((x**2+3))))在Inner看来都是一样的,他们都只是一个字符串而已。然后依旧是定义了一个包含一个HashMap<Inner,BigInteger>的Container类并在其上定义了基本的运算。
解析
解析部分的思路是递归下降。这一部分从第一次作业开始架构基本就没有改变,因为无论如何表达式形式如何,都只需要把当前操作因子提取出来,然后递归下降,下降到底层后再开始一步步返回,已有的运算顺序不变,当加入新的运算操作时只需要把它加到该它运算的那一层。流程如图所示:
简化
在第一次作业中,由于只有多项式,所以只需要合并同类项,这个操作其实在add和sub方法中已经实现了。
在第二次作业中,我并没有简化三角。
在第三次作业中,我针对sin()**2 + cos()**2、2*sin()*cos()、sin(0)、cos(0)进行了化简,当时也没问别人怎么做的,也没看往届博客,于是把sin(-x)和cos(-x)这两种形式忘了,导致有的数据性能分直接为0。
输出
前两次作业的输出都是比较蠢的方法——在主函数中遍历,最后一次作业中,我选择把输出的任务分配到每一个存储的类中,Inner可以输出inner所表示的信息,Container调用Inner的toString输出container所表示的信息,主函数中只需要调用Container的toString即可。
程序结构的分析
由于三次迭代开发过程中,解析部分基本没变,存储部分的区别在上面已经介绍,因此程序结构的部分只针对最后一版代码来分析。
总架构
其中Container和Inner是实现了数据的存储和数据间的运算,在上一部分中已经介绍。
Parser和Lexer实现了递归下降文法解析,Lexer提供当前操作因子,Parser通过调用Lexer获取当前操作因子然后递归下降。在Parser类中还实现了一部分三角化简的功能,即simplifyexpr和simplifyterm,这两个方法分别化简了sin()**2 + cos()**2和2*sin()*cos()两种形式。
Function的功能是分析输入的自定义函数,并且在最终表达式中将自定义函数替换。
规模分析
从这个数据来看,我们可以发现Parser类最冗长,其实导致这个的原因是:Parser内部不仅实现了递归下降,还在递归下降的过程中实现了一部分化简。其次Container类也比较长,这个类从设计上其实是不合理的,因为这个类承担了太多职责,这次作业中的所有计算操作都定义在这个类上。主要体现出来的问题是在设计时没有考虑单一职能原则,导致类的内部有些复杂。
复杂度分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
Function | 9.5 | 15 | 19 |
Lexer | 2.4 | 5 | 12 |
Main | 6 | 12 | 18 |
Parser | 6.14 | 15 | 43 |
storage.Container | 3.38 | 12 | 44 |
storage.Inner | 2.36 | 5 | 33 |
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Function.parse(String) | 5 | 1 | 4 | 4 |
Function.replaceAll(String) | 48 | 7 | 14 | 16 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getFactor() | 3 | 1 | 5 | 5 |
Lexer.getTri() | 4 | 3 | 3 | 4 |
Lexer.next() | 6 | 2 | 6 | 8 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Main.main(String[]) | 4 | 1 | 4 | 4 |
Main.pre(String) | 2 | 1 | 3 | 3 |
Main.replaceAllsum(String) | 36 | 7 | 13 | 15 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 1 | 1 | 2 | 2 |
Parser.parseFactor() | 13 | 5 | 7 | 7 |
Parser.parsePow() | 1 | 1 | 2 | 2 |
Parser.parseTerm() | 1 | 1 | 2 | 2 |
Parser.simplifyexpr(Container) | 66 | 11 | 17 | 20 |
Parser.simplifyterm(Container) | 78 | 11 | 17 | 21 |
storage.Container.Container() | 0 | 1 | 1 | 1 |
storage.Container.Container(Inner, BigInteger) | 0 | 1 | 1 | 1 |
storage.Container.add(Container) | 4 | 1 | 3 | 3 |
storage.Container.addvalue(Inner, BigInteger) | 0 | 1 | 1 | 1 |
storage.Container.containersimplify() | 3 | 1 | 3 | 3 |
storage.Container.cos(Container) | 5 | 1 | 3 | 3 |
storage.Container.getSourceHashmap() | 0 | 1 | 1 | 1 |
storage.Container.isexpr(String) | 17 | 4 | 10 | 14 |
storage.Container.mul(Container) | 7 | 1 | 4 | 4 |
storage.Container.pow(Container) | 3 | 1 | 3 | 3 |
storage.Container.sin(Container) | 4 | 1 | 3 | 3 |
storage.Container.sub(Container) | 0 | 1 | 1 | 1 |
storage.Container.toString() | 35 | 1 | 12 | 12 |
storage.Inner.Inner() | 0 | 1 | 1 | 1 |
storage.Inner.Inner(String, Integer) | 0 | 1 | 1 | 1 |
storage.Inner.abs() | 4 | 1 | 3 | 3 |
storage.Inner.add(Inner) | 5 | 1 | 4 | 4 |
storage.Inner.addvalue(String, Integer) | 0 | 1 | 1 | 1 |
storage.Inner.divide() | 1 | 1 | 2 | 2 |
storage.Inner.equals(Object) | 3 | 3 | 2 | 4 |
storage.Inner.getSourceInner() | 0 | 1 | 1 | 1 |
storage.Inner.hashCode() | 0 | 1 | 1 | 1 |
storage.Inner.innersimplify() | 3 | 1 | 3 | 3 |
storage.Inner.mul(Inner) | 7 | 1 | 5 | 5 |
storage.Inner.numofinner() | 0 | 1 | 1 | 1 |
storage.Inner.sub(Inner) | 5 | 1 | 4 | 4 |
storage.Inner.toString() | 4 | 1 | 3 | 3 |
附:度量名词解析
-
OCavg:类平均圈复杂度,继承类不计入
-
WMC:类的总圈复杂度
-
ev(G):非抽象方法的基本复杂度,基本复杂度是一个图论度量理论,用以衡量一个方法的控制流结构缺陷,范围是1到v(G)
-
iv(G):方法的设计复杂度,用以衡量方法控制流与其他方法之间的耦合程度,范围是1到v(G)
-
v(G):非抽象方法的圈复杂度,用以衡量每个方法中不同执行路径的数量
BUG与HACK
BUG
-
第一次:超大数的计算出现问题,越界。
-
第二次:sin(0)和cos(0)的计算出现问题,由于sin和cos被我定义为一种运算,而在之前的程序中,如果表达式为0就记为空HashMap,因此sin和cos对空HashMap运算后仍然是空,由此产生错误。还有一个BUG是自定义函数空白项忘记删除。
-
第三次:sum中超大数计算出现问题。sum替换i时忘记加括号。一个正则表达式匹配的不合理。
bug出现的主要原因是一些特殊的、边界情况没有考虑,而由于没有写自动评测机,因此自己在造测试数据时不够全面,导致出现了一些特殊的bug。程序的鲁棒性不够高。
同时我们还可以发现出现bug的类代码行数和圈复杂度比较高,因此之后的架构设计中,应该尽量降低类的代码行数和圈复杂度。
HACK
由于并没有写自动评测机,因此hack时只能是采用阅读他人代码来找逻辑上的漏洞,然而事实上这种方法不仅费时,还低效,很难找出某一个错误。
架构设计体验
架构设计从第一版开始就确定了,主要实现:递归下降文法解析+数据存储与计算。递归下降文法解析部分在三次迭代中表现出很强的可扩展性,只需要加入一些情况即可。而数据存储与计算在这三次迭代中虽然主体思路都是找到一个总容器来存储所有数据,但前两次都表现出了很低的可扩展性,每次出现新的要求都需要重构自己的代码。不过在最后一次,找到了一种合理的、可扩展性很强的存储方式。
心得体会
-
在第一单元之前从来没接触过面向对象的设计,因此设计思想还停留在大一的面向过程的思想,但在三次迭代开发的过程中,对面向对象的理解更深入了。虽然最终的架构仍然有不满意,但面向对象的思想已经能逐步体现出来了。
-
这个单元中还学到一些很具体的方法,比如说递归下降,这个虽然在最开始没太明白,但是在一步步写的过程中逐渐理解透彻了。再比如说一些设计原则,单一职能原则,这个在之前的代码中不常应用,但是在面向对象的开发过程中这个原则能使我们的代码变得更加清晰也更加容易debug。
-
这次作业中有两点不足:一是对于代码设计来说,仅仅是以完成任务为目标,没有追求优美的结构设计;二是对于测试部分的工作做的不到位。希望在第二单元中自己能做出一个比较合理的架构并且尝试做出自动数据生成机。
标签:oo,总结,Container,表达式,storage,空白,因子,Inner,单元 来源: https://www.cnblogs.com/pc-hao/p/16053156.html