结构分析
DesigniteJava 分析如下:
行数 | 类个数 | 方法个数 |
---|---|---|
643 | 9 | 63 |
从类的个数和行数看来,总体的代码较为精炼。
类复杂度
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
Expr | 4 | 0 | 6 | 4 | 69 | 17 | 0 | 0 | 0.0 | 3 | 2 |
Form | 1 | 0 | 3 | 3 | 18 | 4 | 0 | 0 | 0.0 | 2 | 1 |
Function | 3 | 0 | 7 | 6 | 57 | 15 | 0 | 0 | 0.2857142857142857 | 2 | 2 |
MainClass | 0 | 0 | 1 | 1 | 17 | 2 | 0 | 0 | -1.0 | 0 | 3 |
Matcher | 0 | 0 | 1 | 1 | 17 | 5 | 0 | 0 | -1.0 | 3 | 0 |
Momomial | 3 | 0 | 25 | 18 | 254 | 61 | 0 | 0 | 0.0 | 2 | 3 |
Pair | 2 | 0 | 7 | 6 | 50 | 12 | 0 | 0 | 0.2857142857142857 | 2 | 0 |
Polynomial | 0 | 0 | 11 | 11 | 107 | 31 | 0 | 0 | -1.0 | 3 | 1 |
Term | 2 | 0 | 2 | 2 | 54 | 12 | 0 | 0 | 0.0 | 0 | 5 |
从类的数据来看,几个类字段个数都比较小,数据复杂度较低;大部分类的方法个数和行数较少,类的结构比较简单,其中,Momomial 和 Polynomial 两个类的方法个数较多,主要是需要实现大多数的运算方法和输出优化方法,其中 Momomial 的优化调用方法较多,因此代码较为冗长,存在优化空间;所有类都没有继承,项目结构简单,但也类间的关联性也就较差,这主要是个人实现导致的;类的上下级模块调用较少,调用关系简单。
特别的,从内聚缺乏度可以看出这些类的实现都是较为精炼、直接的,并且具有高内聚、低耦合的特性;其中行数和方法数较多的 Momomial 类,在该分析下显示了良好的类内结构。
从类的相关模块数据可以看出代码的复用性和复杂度良好,调用和被调用的方式简单清晰,显示了良好的代码结构。
方法复杂度
Type Name | Method Name | LOC | CC | PC | Line no |
---|---|---|---|---|---|
Expr | Expr | 5 | 1 | 1 | 11 |
Expr | Expr | 5 | 1 | 3 | 17 |
Expr | parseTerm | 25 | 8 | 1 | 23 |
Expr | parse | 12 | 3 | 0 | 44 |
Expr | merge | 13 | 3 | 0 | 55 |
Expr | toString | 3 | 1 | 0 | 68 |
Form | Form | 3 | 1 | 1 | 9 |
Form | size | 3 | 1 | 0 | 13 |
Form | create | 9 | 2 | 1 | 17 |
Function | Function | 9 | 2 | 1 | 10 |
Function | simplify | 9 | 3 | 1 | 20 |
Function | replaceAll | 7 | 2 | 1 | 31 |
Function | getVars | 18 | 5 | 1 | 38 |
Function | preTreat | 3 | 1 | 1 | 54 |
Function | getExpr | 3 | 1 | 0 | 59 |
Function | getName | 3 | 1 | 0 | 63 |
MainClass | main | 15 | 2 | 1 | 6 |
Matcher | bracketMatch | 15 | 5 | 2 | 4 |
Momomial | Momomial | 22 | 4 | 1 | 10 |
Momomial | Momomial | 5 | 1 | 3 | 29 |
Momomial | add | 3 | 1 | 1 | 35 |
Momomial | mul | 5 | 1 | 1 | 39 |
Momomial | mul | 6 | 1 | 2 | 45 |
Momomial | mul | 3 | 1 | 1 | 53 |
Momomial | div | 3 | 1 | 1 | 57 |
Momomial | derivate | 17 | 3 | 0 | 61 |
Momomial | getSharp | 14 | 4 | 1 | 77 |
Momomial | getInfo | 3 | 1 | 0 | 89 |
Momomial | toString | 33 | 8 | 0 | 93 |
Momomial | format | 8 | 2 | 0 | 121 |
Momomial | clone | 3 | 1 | 0 | 129 |
Momomial | negate | 3 | 1 | 0 | 134 |
Momomial | getCoe | 3 | 1 | 0 | 138 |
Momomial | getIndex | 3 | 1 | 0 | 142 |
Momomial | getExp | 3 | 1 | 0 | 146 |
Momomial | convertPowFac | 11 | 3 | 0 | 150 |
Momomial | convertExp | 11 | 3 | 0 | 160 |
Momomial | gcd | 11 | 2 | 2 | 170 |
Momomial | singleMomo | 21 | 5 | 0 | 181 |
Momomial | multiMomo | 22 | 5 | 0 | 201 |
Momomial | cutExp | 9 | 3 | 1 | 221 |
Momomial | availableCount | 16 | 4 | 0 | 230 |
Momomial | convertCoe | 11 | 3 | 0 | 243 |
Pair | Pair | 4 | 1 | 2 | 10 |
Pair | first | 3 | 1 | 0 | 15 |
Pair | second | 3 | 1 | 0 | 19 |
Pair | equals | 17 | 3 | 1 | 23 |
Pair | hasEquals | 9 | 3 | 1 | 37 |
Pair | makePair | 7 | 2 | 1 | 46 |
Pair | toString | 3 | 1 | 0 | 53 |
Polynomial | Polynomial | 4 | 1 | 1 | 9 |
Polynomial | Polynomial | 3 | 1 | 0 | 14 |
Polynomial | add | 10 | 3 | 1 | 18 |
Polynomial | mul | 19 | 5 | 1 | 28 |
Polynomial | derivate | 7 | 2 | 0 | 45 |
Polynomial | getMomomial | 6 | 2 | 0 | 52 |
Polynomial | negate | 5 | 2 | 0 | 58 |
Polynomial | divConst | 5 | 2 | 1 | 64 |
Polynomial | toString | 31 | 9 | 0 | 70 |
Polynomial | format | 8 | 2 | 0 | 97 |
Polynomial | clone | 7 | 2 | 0 | 105 |
Term | Term | 14 | 3 | 2 | 7 |
Term | parse | 36 | 9 | 0 | 19 |
从方法的分析数据来看,这些方法的规模较小(最多不超过 36 行),方法的圈复杂度很低(大部分方法的复杂度在 4 以下),显示这些方法的结构良好,易于理解和维护。
但是几个类中的 parse 方法圈复杂度较高,这主要是递归下降的实现导致的(在过程中判断类型)。
同时在设计异味命令嗅探中,显示了良好的架构实现,没有产生任何的嗅探结果。
架构设计
架构的 UML 类图如下
![](https://i-blog.csdnimg.cn/blog_migrate/590bfb14656a9b72c9d823b4ee10c339.png)
架构解释
整体的运行流程为:在 Form 中存储预处理过的所有函数,函数内部存在方法 simplify 来使用传入的 Form 简化当前函数(字符串替换函数),最后将待化简的表达式作为一个特别的函数对象并传入 Form 进行函数的替换。
此阶段的示意图如下:
![](https://i-blog.csdnimg.cn/blog_migrate/692d3b53d9b1bf61dc4309811a8d544f.png)
处理函数后得到了一个只包含括号、e 指数和微分的表达式,接下来传入表达式处理的程序。
表达式处理的流程是建立一个自上而下的表达式树:初始化,建立一个 Expr 类并将表达式字符串传入,调用 parse 方法将其解析为项字符串;然后生成一个 Term 对象将对应的项字符串传入,调用 parse 方法将其解析为括号、微分与一般因子;对于括号,去括号后作为一个 Expr 处理;对于微分,作为 Expr 处理后调用多项式的求导方法;对于因子,直接生成对象即可。最后在 Term 类中执行多项式乘法,在 Expr 类中执行多项式加法合并。
处理表达式的的示意图如下
![](https://i-blog.csdnimg.cn/blog_migrate/63c4120f640898381521b53c3a26e14f.png)
从类图可以看出,我没有实现各个 Factor 类,从三次迭代来看这是可以接受的,但是在更长期的迭代过程中势必要作出修改。各个 Factor 类被集成在 Monomial 类中,尽管提高了内聚性,但也提高了代码圈复杂度。同时,Monomial 类的方法也过多,这也主要是集成各个 Factor 类的原因。尽管使得外部访问类内元素更为简单直接,但是也牺牲了更多 Factor 的可拓展性。以及在类内包含大量输出优化用的方法,这些方法大多数都是线性调用的,放在类内可能并不妥善,可以考虑在外部设计一个输出类来处理输出的优化,以简化 Polynomial 和 Monomial 两个类。
除去 Monomial 类,其他类的架构设计还是值得肯定的,在迭代需求不涉及表达式或者多项式的总体结构的情况下,这个架构可以应对绝大多数的需求而无需作出修改,或者通过在外部新增方法进行预处理的方式解决。
设计体验
在三次迭代中,我的架构没有经过太大的变化,大部分的架构在第一次的作业中就已经成型。
总体迭代流程如下:
![](https://i-blog.csdnimg.cn/blog_migrate/45a6e18a857234f526515cd000295764.png)
在第一次作业中,我就确定了“表达式 - 项 - 单项式(表达式)”这样的递归下降处理逻辑,并且用多项式作为传递类的基本架构。通过合理的“表达式 - 项”拆分方式,在以后两次作业中,这一环节都可以有效兼容,从而大大减少了代码量。同时,这也导致我在接下来的迭代中选择用预处理字符串的方式解决函数替换的问题。
如何有效从表达式拆分项:
private Integer parseTerm(int beginAt) {
int i = beginAt;
if (expr.charAt(i) == '+' || expr.charAt(i) == '-') {
i++;
} for (; i < expr.length();) {
if (expr.charAt(i) == '+' || expr.charAt(i) == '-') {
i++;
} for (; i < expr.length(); i++) {
char c = expr.charAt(i);
if (c == '(') {
i = Matcher.bracketMatch(expr, i);
} else if (c == '*') {
i++;
break;
} else if (c == '+' || c == '-') {
return i;
}
}
} return i;
}
在第二次作业中,我在 Monomial 类中新增了对 e 指数的识别处理,在外部实现函数的替换。这样的处理方式可以让我把函数替换的问题与化简问题剥离开,降低代码的复杂度。对于 e 指数的识别处理,则可以复用原有的表达式类,即“表达式 - 项 - 单项式(表达式)”的递归下降过程变为“表达式 - 项 - 单项式(表达式) - 表达式”的过程,同样减少了代码量。
![](https://i-blog.csdnimg.cn/blog_migrate/d860a2b7ebc0594b745fe99353da2abe.png)
在第三次作业中,在 Monomial 和 Polynomial 中新增求导方法。通过在多项式中调用单项式求导、单项式中调用多项式求导这样一个递归下降的求导链,解决了求导的需求。
![](https://i-blog.csdnimg.cn/blog_migrate/255e157909d1766ad6b73ef4bc730662.png)
总体的迭代以新增内容为主,以修改原有代码为辅。
假设一个新的迭代需求:函数求偏导
解决方法:预处理函数,可以考虑继承原有的对 x 分析的代码单独进行重写而不是修改原有代码,同时也应该尽可能不使用优化方法。无论如何,不会对原有代码的功能产生影响。这一过程是在预处理阶段完成的。
Bug 分析
自己的 Bug
在三次作业中,我出现了一个 bug,这是由于针对某个特殊数据进行时间优化导致的:为了进行时间优化,我对多项式乘常数的方法进行的特判,在这种情况下,我将不会使用原有的多项式乘法,而是简单地对多项式内每个单项式的系数进行修改。而在原有的多项式加法和乘法的实现中,我的设计会自动排除多项式中系数为 0 的项,而在这种特殊乘法的情况下则忽略了此事,导致如果一个多项式,作为 exp 的指数时,乘 0 之后没有经过更多处理(加法、乘法),会导致程序认为该多项式内仍然还有元素而计算 gcd,最终导致除 0 的错误。这一错误只会发生在 exp 后跟有 0 指数的情况下(因为只有这种情况能够确定多项式一定是乘常数的),并且要求不能够再后续进行更多操作(不然会被加法和乘法过滤掉)。事实上,存在这一严重 bug 的情况下我只挂了 2 个强测数据点,说明原有代码的鲁棒性还是很好的。
一个可能的样例如下:
exp((x+x+x))^0
最终的解决方法是放弃这一时间优化,直接使用原有的多项式乘法,只需要修改一行即可。修改前后,代码的行数和圈复杂度都没有变化。
事实上,这个问题也可以通过更好的方法设计避免,比如再计算 gcd 前先检查多项式是否有效。
互测策略
我的测试策略是评测机与人工简化数据相结合。这种测试方法的效率较高,尽管经常会发现一些同学的程序因为 cost 的限制而无法进行 MLE 和 TLE 的攻击,这种方法发现的 Bug 的有效性还是有保障的。
同时,为了提高测试效率,我设计的评测机具有控制运行时间和异常处理功能:通过记录日志的方法,可以实现代码运行时间、输出长度对比、输出数据有效性判断、输出数据正确性判断等功能,实现全自动无人监管评测,只需要人工进行错误数据筛选即可。
显然,这种评测方式不会针对被测试者的代码构造数据,而是通过海量的数据检测其可能存在的漏洞,也即黑箱测试。从概率上来说,这种方法是最可靠的。 事实上我一份代码也没看过
优化分析
我实现了以下优化:
- 正项提前
- 针对 exp 内表达式为单项式的去括号
- 针对 exp 内表达式为多项式时提取公因数并判断是否更优
对于第一项的实现:我在多项式输出的时候并不直接进行简单拼接,而是先将每个单项式的输出存储,然后寻找里面不包含负号的项并移至第一项输出(与第一项交换),往后的照常输出即可。
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
ArrayList<String> terms = new ArrayList<String>();
for (Momomial momomial : this.values()) {
String term = momomial.toString();
if (term.length() != 0) {
terms.add(term);
}
} for (int i = 0; i < terms.size(); i++) {
if (terms.get(i).charAt(0) != '-') {
if (i != 0) {
Collections.swap(terms, 0, i);
} break;
}
} if (terms.size() != 0) {
sb.append(terms.get(0));
} for (int i = 1; i < terms.size(); i++) {
if (terms.get(i).charAt(0) == '-') {
sb.append(terms.get(i));
} else {
sb.append("+");
sb.append(terms.get(i));
}
} return sb.toString();
}
针对后两项优化,需要具体的特判,具体判断流程如下:
![](https://i-blog.csdnimg.cn/blog_migrate/a0d670883f7867269f56b59bbbc36660.png)
通过分层封装(一层判断用一个方法进行封装)、方法分支(一次判断开一个方法处理)、分支合并(数个并列的判断在同一个方法里集成)和边界条件判断的方法,我大致维持了代码的简洁性。正确性通过两次强测和互测也得到了验证。
分层封装的示意如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/f2075369fa91f790498ffb0fe1f5459c.png)
具体可以从这一判断链中最长的一个方法看出(27 行):
@Override
public String toString() {
if (this.coe.equals(new BigInteger("0"))) {
return "";
}
String cpart = convertCoe();
String xpart = convertPowFac();
String epart = convertExp();
if (xpart.length() == 0 && epart.length() == 0) {
return this.coe.toString();
} else if (xpart.length() == 0) {
if (cpart.equals("-") || cpart.equals("")) {
return cpart + epart;
} else {
return cpart + "*" + epart;
}
} else if (epart.length() == 0) {
if (cpart.equals("-") || cpart.equals("")) {
return cpart + xpart;
} else {
return cpart + "*" + xpart;
}
} if (cpart.equals("-") || cpart.equals("")) {
return cpart + xpart + "*" + epart;
} else {
return cpart + "*" + xpart + "*" + epart;
}
}
通过 else - if 的结构完整实现了判断逻辑链,同时调用下层封装好的方法实现了代码的简洁性。
心得体会
通过第一单元的学习,我实践了递归下降的方法,对其有了更深的理解,同时也对面向对象的“对象”特点有了更深刻的体会。
在与课程组的代码的对比中,我认识到如何设计类与类之间的联系,如何考虑类与类的继承关系。尽管课程组的自下而上的建树方式拥有良好的可拓展性,体现出来了面向对象的设计,但是自上而下的建树方式同样拥有其优点。我认为,自己研究思考的结果也许不一定比课程组精心打磨的成果好,但至少通过实践可以认识到两种方法的差异,这是一味接受课程组指导的同学所体会不到的。与课程组斗其乐无穷
同时,在研讨课上的发言让我体会到团队合作的困难与重要:不同的同学之间的看法、观点与想法都不一样,理解的方式也大相径庭。如何做到在这些不同的发言中求同存异、如何更清晰地表达自己的观点,在这个过程中不断反思、验视自己的代码,并不断改进,这是第一单元带给我最大的收获。
课程未来方向
可以考虑修改一下因子的个数,三四个因子并没有建立类的必要。同时,应该考虑精简指导书,指导书的未尽之处应该引导同学们在讨论区提问,这样一方面可以锻炼同学们交流沟通的能力,另一方面可以让指导书变得更加“表意”。