2022北航面向对象第一次作业分享及总结
2022北航面向对象第一次作业分享及总结
本次作业由三次任务组成,从2.28号开始,历经三周的迭代开发,实现了对表达式结构的建模,并且实现了多层嵌套表达式及函数调用的括号展开与化简;同时,本次作业也是笔者第一次基于面向对象的思想进行工程开发以及层次化设计,本篇将主要围绕设计架构以及代码度量两方面进行总结与分享。
一、题目概述
总体目的为实现去除表达式所含的不必要括号以及表达式化简(可选),整个项目的迭代过程由简入繁,主要针对表达式的形式复杂度进行逐步提升。
1.homework1
一、 因子:
变量因子(包含幂函数)
常数因子 包含一个带符号整数,如:233 。
表达式因子 用一对小括号包裹起来的表达式,可以带指数。
二、项:
由乘法运算符连接若干因子组成。
三、表达式 由加法和减法运算符连接若干项组成。
UML图
2.homework2
因子中添加新类型三角函数(sin(<因子>)或cos(<因子>, 且<因子>在本次作业中只可能是常数因子或者变量因子中的幂函数),自定义函数以及求和函数(函数调用时的因子在本次作业中只可能是常数因子或者变量因子中的幂函数)。
从题目中我们可以看出题目要求限制了括号嵌套的情况,形如f(x,(x*2+3)) + sin(x+sin(x))这样的表达式在这次任务中不会出现。
3.homework3
三角函数、自定义函数、求和函数的调用过程中因此允许为可能的任意形式,即支持括号嵌套。形如f(sum(i, 3, 4, sin((x+2)))) + sin(cos(x**2))这样的表达式可以出现。
UML图
二、架构设计
本次架构围绕计算进行设计,即一切属性与方法都旨在为计算服务。
本次架构基于递归下降语法分析,采用边解析边运算的方法,因此不需要考虑顶层单元对底层单元的存储,而是设计了统一的数据存储方式,整个项目在解析的过程不断的对现有层次的单元进行数据更新,自底向上,最终得到一个包含表达式全部信息的顶层单元。(递归下降语法分析:为每一个语法成分编写一个可递归调用的分析子程序,进行自顶向下分析的语法分析方法;适用于上下文无关的文法)
hw1
hw1比较简单,我采取了training的语法分析模式,设计Lexer类进行文法提取,并协助Parser进行语法分析。
基于题干给出的形式化表达,建立Expression,Term和Factor三类对语法元素进行归类,并且由题干我们可以得知在解析到factor部分时可能会出现分析表达式的情况,因此便有了产生递归的可能性。因此在类设计中,Expreesion类应当继承自Factor类,深究原因,在于Expression类需要实现同Factor类一样的方法,这本身也暗示了两者之间的泛化关系。
本次作业较为简单,起初笔者只设计了一个Formula类,蕴含了上述三类的全部方法。但在初次架构完成之后,不难发现Formula类内聚过低,缺乏方法细化,类的复杂性过高,从而增加在今后迭代过程中出现错误的风险,同时结构也过于模糊,因此在第二次作业前进行了一次重构。
此次由于表达式结构简单,因此采用了HashMap<指数,系数>的数据存储结构,不展开阐述了。
UML图
类复杂度分析
hw2
hw2难度陡增,主要是因为增加了三种因子类型,导致必要的类细化。
由于增加了自定义函数以及求和函数,因此添加了SumParser以及FuncFactory(当然可以将它们存放在一个类中,但为了降低类复杂度,笔者选择将二者分离处理)。其中,针对自定义函数的处理采用了工厂化模式,在于更好的细化FuncFactory和Parser的责任。
我认为hw2的难点更多在于如何设计一个良好的存储结构。一个优良的存储结构即能够简化计算,同时也决定了今后合并同类项时的难度。因为本次任务加入了三角函数,经过思考,我采用了如下的存储形式(将表达式统一看作如下形式):
X ** (指数) * { [sin(constant * x **inIndex) ** outIndex] * [一系列如前者一样的sin()] } * { [ cos(constant * x ** inIndex) ** outIndex] * [一系列如前者一样的cos()] } + ....
PS: 在hw3的完成过程中,采用了一种更加优良的存储结构,将在hw3部分进行阐述。
代码示意:
public class Factor implements Cloneable {
private final HashMap<BigInteger, ArrayList<OverallCoef>> table = new HashMap<>();
}
public class OverallCoef implements Cloneable {
private BigInteger coef;
private ArrayList<Sin> sin = new ArrayList<>();
private ArrayList<Cos> cos = new ArrayList<>();
}
// sin,cos继承自Triangle
public class Triangle implements Cloneable, Comparable<Triangle> {
private BigInteger outIndex;
private BigInteger inIndex;
private BigInteger constant;
}
类关系的再整理
在本题中,类的划分实质上是针对计算方法的划分。加法的运算对象只能是项,项和项之间通过加法形成了一个表达式;乘法的运算对象只能是因子,因子和因子之间通过乘法形成了一个项;乘方的运算对象只能是因子,因子自身乘方形成了一个新的因子,因此因子是递归下降的出口,但由于表达式因子的存在,则存在表达式实现乘方运算的可能性,因此Expression类需要继承自Factor,这也是此处泛化的本质原因。
注:Term此处的泛化其实可以不必实现,但由于本次架构设计类之间的方法重合度较高,为了避免复写,因此实现了此泛化关系。
UML图
度量分析
总体来看,各个方法圈复杂度控制良好,但toString等方法代码复杂度过高,应当进行方法内拆分,实现功能再细化,以降低方法复杂度。
hw3
hw3主要增长的需求在于丰富了三角函数、求和函数以及自定义函数中因子部分,引入了表达式因子,因此hw2采用的存储结构将不再适用(hw2采用的存储结构只能够表达一个简单的幂指形式,而无法存储一个完整的多项式)。于是,我更改了存储结构:
public class Triangle implements Cloneable, Comparable<Triangle> {
private BigInteger outIndex;
///////////修改/////////////
private String innerFactor;
///////////////////////////
}
如代码所示,将原先Triangle类中outIndex, inIndex, constant三种属性换为一个字符串innerFactor,此字符串为形式sin(<因子>)中因子的toString,如此更改好处有如下几点:
- 各种类型的Factor都有对应的toString字符串,因此采用存储对应toString的方式实现了因子存储的统一。
- 在解析运算过程中,并没有实现对三角函数内部存储因子的修改,唯一用到因子的地方就是在乘法运算时根据内部因子是否相同来判断是否要合并三角函数。而判断内部因子是否相同只需要判断 inIndex, constant这两个属性是否一致,而恰恰toString字符串中已经蕴含了上述信息,因此此处用toString代替不仅实现了相关功能,同时也简化了运算过程。
- 协助实现了同类项合并。形如sin(x) + sin(x + 1 - x ** 0)这一表达式,鉴于toString的设计实现了值与字符串的一一对应,在存储时,这两者将会拥有同样的存储单元,这大大简化了我们的化简过程。
三、实现细节
-
(1) 克隆的实现。
在本次项目的运算过程中,我统一采用了返回一个新对象的方式,因此不免需要将另一个对象的元素克隆到新对象中。
克隆分为浅克隆与深克隆,在浅克隆中,被复制对象的所有普通成员变量都具有与原来对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。因此,当你对新对象中的引用对象进行更改时,同时也会更改被复制对象中对应的引用对象,显然这样的情况不是我们希望看到的,因此我们需要实现深克隆。深克隆则是为新对象的成员变量赋了与被复制对象中对应变量相同的值,但摆脱了二者的关联性,实现真正意义上的克隆。
我本次采用了实现Clonable接口、复写clone方法的方式,由于能力不足,没有采用序列化与反序列化的方法。 -
(2)存储形式数据结构的选择。
-
本次设计中,我对包含指数的部分采用了HashMap的数据结构进行存储,好处是java.HashMap内含了许多高效的方法,可以被我们随时调用,缺点则是排序的实现比较复杂,导致每一次运行的输出流可能会出现指数排列顺序的不同。(当然可以自行构造类哈希数据结构,比如构造ArrayList< Object> ,并在object类中包含key与value,并实现Comparable接口,以达到为类哈希排序的目的)
四、测试
本单元的测试我只采取了一种简单的数据生成方式。我使用了Python随机数生成的方法,并结合反向解析表达式的思想生成数据进行测试。不足在于,测试集的覆盖性不够强,同时缺乏边界考虑,导致测试强度过低。
五、Bug与性能
Bug
这三次作业侥幸通过了全部的强测样例,却难逃同学的hack。
原因在与我的toString函数的逻辑错误,以下是我toString函数的复杂度
formula.Factor.toString() 47.0 9.0 12.0 15.0
可见toString函数复杂度过高,数量上表现为独立路径的条数过多,即合理的预防错误所需测试的最少路径条数过大。复杂度大说明程序代码质量低且难于测试和维护,因此需要进一步的功能细化与方法拆分。
同时,我忽略了0**0 = 1(题目要求)这个细节,这也提示我在今后应该更加注重题目的阅读。
性能
由于我过于懈怠,本次作业没有对利用任何三角函数公式进行优化,只达到了除此以外的最简,恕我懒弱。
六、心得体会
本次作业是我第一次接触java以及面向对象编程,刚开始由于对环境以及语法的不熟悉,效率极低,为此恶补了好几天功课。
开学前两周我便深深体会到了OO课程的难度,前两周一共刷了四天夜,追平了上学期总共的刷夜次数。深刻感谢新主楼F121教室以及共同刷夜的同学的陪伴。
本次作业深化了我对工程化思维的认识,并期待在今后的码路上能够做到极简主义。
第一次作业毕,在此appreciate好友刘俊辰、白瑞、邓皓元,感谢三周来的传授与讨论