BUAA_OO_第一单元总结
文章目录
1 程序结构分析
1.1 代码规模
- 下面是各个类的代码长度
- 代码总行数1604行,源代码1414行,占比88%。可以看出,从头到尾采用后缀表达式去计算的话,代码还是很复杂的,代码长度也较多,但是整体上思路还是比较容易理解的,对于细节的要求比较复杂。
- Operate行数最多,里面主要是针对后缀表达式的处理,包括乘法,加法等等,其中乘法和加法的步骤较为复杂。
- Mainclass行数也较多,里面主要是进行对函数的预处理,以及自定义函数的替换。
1.2 类和方法的相关度量
利用IDEA的Calculate Metrics中的Class Metrics对类的复杂度进行分析:
- OCavg(Average opearation complexity):平均操作复杂度
- OCmax(Maximum operation complexity):最大操作复杂度
- WMC(Weighted method complexity):加权方法复杂度
下面选取两个复杂度较高的类的方法进行复杂度分析:
Mainclass里面方法的复杂度分析:
可以发现,主函数的复杂度还是比较高的。尤其是预处理阶段,需要对字符进行逐一的判断。
还有就是针对自定义函数的处理的复杂度也比较高。
Operate类里面方法的复杂度分析: - Add方法涉及对两个算式进行相加,需要对两个字符串进行分割,然后计算,复杂度较高。
depart(String)
,departpart(String)
,departspace(String)
功能类似,分别是对一个字符串安照+
,*
,
1.3 类图
- 由于整体代码不断在迭代,最后一次代码也能实现第一次的功能,所以只贴出最后一次代码的类图以及之间的关系。
1.4 总结
- 这次作业的类复杂度很高,部分类之间耦合程度高。但是继承较少,多数类的功能互不影响;
- Mainclass类中主要是针对函数表达式的预处理以及自定义函数的处理,涉及到一些字符串的替换以及循环,所以操作复杂度较高;
- 对于Operate类的函数,主要是用于加法和乘法的一些运算以及性能优化部分,所以不仅连续嵌套循环,还要不断地调用其他类中的函数。这就导致在计算和化简时,类之间反复调用,圈复杂度很高,也导致了耦合度大;
- Term,Expr,Number,Var,Ex, Derivation等类与其他类间几乎没有干扰,作为因子完整地实现自身功能,可以说实现了一部分的“高内聚”原则。
- 关于每个类的具体功能,下面会在架构设计体验中仔细介绍。
2 架构设计体验
2.1 Homework1
本次作业的难点主要是对于递归下降算法的理解,第一次作业是起步阶段,是相应的来说最难去突破的一次作业。要从零开始写,一开始我是对递归下降没有任何认知的,需要从零开始去理解,还好有课上的实验课帮助理解,才能够按时的完成。本程序,包括后面的几次作业,都是使用递归下降法将表达式的后缀表达式算出来,然后再进行相应的计算,最后化简并输出。
2.1.1 输入预处理
输入的表达式可以通过一些预处理来简化后面的解析过程。
- 对于空格,全部通过replaceAll方法换掉。
- 将连续的+和-替换为-
- 将连续的+和+简化为一个+
String result000 = result0000.replace(" ", "");
String result00 = result000.replace("\t", "");
if (result.charAt(i) == '+') {
if (result.charAt(i + 1) == '+') {
result = result.substring(0,i + 1) + result.substring(i + 2);
i--;
}
else if (result.charAt(i + 1) == '-') {
result = result.substring(0,i) + result.substring(i + 1);
i--;
}
}
else if (result.charAt(i) == '-') {
if (result.charAt(i + 1) == '+') {
result = result.substring(0,i + 1) + result.substring(i + 2);
i--;
}
else if (result.charAt(i + 1) == '-') {
result = result.substring(0,i + 1) + result.substring(i + 2);
StringBuilder builder = new StringBuilder(result);
builder.setCharAt(i, '+');
result = builder.toString();
i--;
}
}
else if (result.charAt(i) == '*') {
if (result.charAt(i + 1) == '+') {
result = result.substring(0,i + 1) + result.substring(i + 2);
i--;
}
}
2.1.2 解析表达式得到后缀表达式
解析表达式主要利用Lexer和Parser两个类,通过递归下降的算法,得到后缀表达式进行后面的计算。
Lexer lexer = new Lexer(result00);
Parser parser = new Parser(lexer);
Expr expr = parser.parseExpr();
- Lexer类通过lexer.next()来读取下一个数字或者参数,通过lexer.peek()来得到当前读取到的值。
- Parser负责解析表达式,并且生成表达式树。主要有parserExpr()和parserTerm()和parserFactor()三种方法。
2.1.3 通过栈的方法来计算后缀表达式的值
for (int i = 0;i < parts.length;i++) {
if (parts[i].equals("+")) {
String str = Add(stacks[top], stacks[top - 1]);
top--;
stacks[top] = str;
}
else if (parts[i].equals("*")) {
String str = Mul(stacks[top], stacks[top - 1]);
top--;
stacks[top] = str;
}
else {
top++;
stacks[top] = parts[i];
}
}
2.1.4 进行最后的化简
- 将空格删去
- 将连续的加减号化简掉
2.1.5 第一次作业总结
由于本次的代码未能留存,且后续的几次代码是在此代码上进行迭代而来的,所以在此不贴上类图。本次作业在强测和互测中并未出现bug,但是有很少的性能分丢失,但是几乎可以忽略不记。
2.2 Homework2
老师在课上强调本次作业将会是整个oo作业中最难的一次作业,事实也确实是这样,至少是第一单元最难的一部分。这一次的难点主要是对于指数部分的处理,函数替换的部分可以通过字符串替换的方式进行,但是指数部分的处理到周五才刚刚有思路,经历了几天的焦虑以后,终于完成了这一次作业。本次作业仍然是沿用上一次的架构,使用计算后缀表达式的方法,最后对后缀表达式进行计算。本次作业主要分为两个部分,自定义函数和指数部分的处理,同时引入了一个一般形式Normal类,下面将依次介绍。
2.2.1 自定义函数的解决
在下图中:
- s0代表着要计算的式子
- xingcan里面存储的是类似于<‘f’,“xyz”>这种形式,biaodashi里面存储的函数定义式中的
=
后面的部分 - 需要注意的是,再读入函数定义式时,需要将空格全部消除,以及避免可能出现的形参混乱的情况,将表达式里面的xyz全部替换为pqr
public static String removeFxson(String s0, HashMap<Character, String> xingcan,
HashMap<Character, String> biaodashi) {
String s = s0;
for (int i = 0;i < s.length();i++) {
if (s.charAt(i) == 'f') {
String shican = zhao(i, s);
String tihuanqian = "";
tihuanqian = 'f' + shican;
shican = shican.substring(1, shican.length() - 1);
String[] shicans = devide(shican);
String xingcans = xingcan.get('f');
String tihuanhou = biaodashi.get('f');
for (int k = 0;shicans[k] != null;k++) {
String bushican = '(' + shicans[k] + ')';
String buxingcan = String.valueOf(xingcans.charAt(k));
tihuanhou = tihuanhou.replace(buxingcan, bushican);
}
tihuanhou = '(' + tihuanhou + ')';
s = s.replace(tihuanqian, tihuanhou);
i = -1;
}
//后续是针对g和h函数的处理,在此省略
}
}
2.2.2 指数部分的处理
针对于指数部分,在借鉴了学长代码的思路下,终于有了解决办法:
- 避免代码中对于x的处理出现麻烦,此处首先将exp全部替换为x,在输出时再将e全部替换为exp
- 再读到
e
这个字符时,进入到读因子的模块(此处只是说大致思路,期间还有需要处理的细节,对lever.peek()的控制等等),然后将这个因子作为一个指数因子的一个参数。
public class Ex implements Factor {
private Factor inex;
private int cishu = 1;
//下面有一些方法的实现,在此略过
}
- 在输出时:需要将Factor的后缀表达式重新调用一遍计算后缀表达式的过程
public String toString() {
Operate son = new Operate("aaa");
String out = son.operator(this.inex.toString());
out = "(" + out + ")";
if (this.cishu == 1) {
return "e(" + out + ")";
}
else {
return "e(" + out + ")^" + String.valueOf(this.cishu);
}
}
2.2.3 有关Normal类的介绍
public class Normal {
//a*x^b*hashMap
private BigInteger aa;
private BigInteger bb;
private HashMap<String, BigInteger> hashMap = new HashMap<>();
//下面省略一些方法
}
一个Normal类的一般形式是:
- 在Operate函数里面,计算乘法时,后首先将字符串分割成不含加法的形式,然后将两个字符串化成Normal类,再进行计算。
2.2.4 第二次作业总结
本次作业首先是纠结于到底要不要进行重构,发现不重构也能做,然后是不会处理指数部分,最后事不会建立一般形式,最后才发觉一般形式里面可以装一个HashMap,本次作业强测和互测都被发现了bug,是没有进行充分的本地测试,没有利用好测评机资源的原因,也在第三次作业中使用测评机发现了许多bug
2.3 Homework3
本次作业主要是增加了求导这一个功能,以及函数在定义时可以调用其他定义的函数,但是这一个地方如果上一次作业是采用字符串替换的方式进行,就不会产生错误。下面主要介绍的方法。
2.3.1 求导过程的实现
- 首先是预处理阶段,为避免可能出现的不必要的麻烦,首先将所有的
dx
替换为d
- 读因子时,如果遇到d,就进入读入一个表达式的过程,读入的这个表达式算作这个求导因子的参数
- 然后每一个因子类都增加一个方法todxString(),用作求导方法,下面是新增加的Derivation类,是求导因子。
public class Derivation implements Factor {
private final Expr expr;
public Derivation(Expr expr) {
this.expr = expr;
}
public String toString() {
return this.expr.todxString();
}
public String todxString() {
Operate grandaughter1 = new Operate("grandaughter1");
String ooo = grandaughter1.operator(this.toString());
ooo = ooo.replace(" ", "");
ooo = "d(" + ooo + ")";
Operate grandaughter2 = new Operate(ooo);
String out = grandaughter2.op();
String outout = chuli(out);
return outout;
}
//chuli()是将结果化成后缀表达式的形式
}
2.3.2 第三次作业总结
本次作业是较为简单的一次,很多同学在题目放出来当晚就已经完成了作业,但是我用后缀表达式去做的话,可能需要更注重细节上的实现,多一个空格可能都会导致结果错误,此次作业也学会了使用评测机进行找bug,还是能找出来很多bug的。
2.4 自定义新迭代情景说明可扩展性
- 比如在下一次迭代过程中,增加了三角函数的因子类。在这种情况下,其实和指数函数因子类的处理方法大致相同。新增加一个Sin类和Cos类,在其中实现求导和输出的方法。作业完成过程中没有经历过大的变动,也说明程序的可扩展性良好。
3 Bug分析
- 在第一次作业的互测和强测中没有发现bug。
- 在第二次作业中,发现了几个bug。
当跑程序0 (((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8
时,程序直接报错。这儿我意识到了进行表达式toString
方法的实现的时候,需要进行适当的判断。
还有就是类似于exp(-1)
这种情况,没有处理好exp()里面是有负号的这种情况。
- 在第三次作业中,好在利用了测评机,没有在强测和互测中发现bug,但是在自己找bug的时候遇到了一些bug,我在预处理的过程中会将()的零次方替换成1,但是如果同时出现
(x+1)^0
和(x+1)^000
时,替换时会出现错误。
4 优化策略
- 还是最经典的那一点,连续的加减号化简成只有一个加减号的形式。
- 还有就是关于exp()*exp()的形式,由于我的思路中采用的是不进行合并,所以在最后我也并没有针对这一个地方进行优化,但是这儿是会导致严重的性能失分。
5 心得体会
- 想好思路再动手:OO第一单元主要要求将一个表达式展开、化简,同时要求化简结果正确而且不能带有不必要的括号。这次作业是我第一次接触这么复杂的Java程序,因此在完成作业时较为吃力,而且初次完成设计时有很多不健全的架构设计,在后面的每次迭代中都需要进行一定的改动。这也提醒我,在接下来的OO作业中要充分考虑好后面可能需要的迭代功能,避免重构带来的巨大压力。还有就是充分利用好同学的测评机资源,尽量避免一些小错误的发生。
- 多关注讨论区:讨论区可能有一些需要注意的点,比如讨论区提到,为了避免一些可能发生的错误,可以在读入函数的阶段,将xyz替换为pqr,避免可能发生的错误
- 总的来说,本单元一共又三次作业,三次作业层层递进,这对代码的可拓展性有一定要求,而多种因子类型又要求实现多态,这让我体会到面向对象思维的重要性,加深我了对封装,继承与多态等性质的理解,增强了自己的代码能力。
6 未来方向
- 作为从头到尾都采用后缀表达式来做的方法,可能对面向过程的要求更高,需要频繁处理字符串,以及考虑字符串输出的格式,可以更好的考虑代码架构,代码长度和复杂度也可以相应的优化。