(1)第一次做业:
第一次做业的设计,如今看来真是杂乱不堪,彻底是面相过程式编程,只不过是套了一个JAVA的壳子而已,存在不少的问题。
第一次做业的题目是求一个简单多项式的导数。全部的表达式都是形如\(f(x) = \Sigma a*x^c\)这种形式。这个多项式是一个字符串类型,固然咱们首先应该判断其是否合法。个人思路是首先经过正则匹配检查是否存在不合法的空格。java
String pattern1 = ".*[+-]\\s*[+-]\\s+\\d.*"; String pattern2= ".*[\\^]\\s*[+-]\\s+\\d.*"; String pattern3 = ".*\\d+\\s+\\d+.*";
上述的三个表达式分别判断了是否存在表达式加法时的带符号整数之间的非法空格,数字与数字之间的非法空格,以及指数后的带符号整数的非法空格。在判断不存在这样的非法空格以后,为了方便后续的项的分离,咱们将字符串中的全部空格和\t制表符全都删去。
而后咱们对整个字符串进行合法性的检验,检查其每一项是否都属于\(f(x) = a*x^c\)这种形式。这里的正则表达式以下。程序员
pattern1 = "([+-]{0,2}(((\\d+[*])?[x]([\\^]([+-])?\\d+)?)|(\\d+)))";
咱们这里须要注意,由于其是贪婪性匹配,每次都去匹配知足上述模式的最大字符串,所以当字符串巨大时可能会存在爆栈的状况,所以咱们调用Pattern Matcher里的方法将其分红一个个小的GROUP,并获得每一个表达式的系数以及指数,并将其存在多项式类中正则表达式
下面是第一次做业的类图
编程
第一次做业的失败主要是把全部的函数方法都写在了一个类里面,不管是数据的读取,仍是表达式的构造,以及最后的输出,都是在一个Main类里,致使这个类的长度达到了几百行。这是明显违反了OO的设计准则的。正确的设计方法应该是首先构造一个读取类,这个类专门用来读取数据,而且存储咱们的字符串。而后再写一个判断类,来判断咱们的字符串是否合法。再经过一个分离类,将咱们的字符串进行分离,将分离出来的数据存储在咱们的多项式类中,最后再经过输出类来进行数据的输出。这样每一个模块功能明确,而且当往后增长需求的时候,大的模块不须要变更,只需在各个类中添加或者修改方法便可。
(2)第二次做业:第二次做业在第一次做业的基础上增长了\(sin(x)以及cos(x)这两种幂函数\),而且在每一个表达式中容许存在两个因式相乘的形式。因为第一次做业的偷懒设计,致使第二次做业的架构须要从新设计,可是在作完第三次做业以后发现第二次做业的设计依旧是有很大的弊端。小程序
第二次做业的多项式的形式是 \(f(x)=\Sigma a*sin(x)^b*cos(x)^c*x^d\)。项里能够存在乘法,这就须要更改以前的表达式的分离的方法。首先我先更改了多项式的存储结构,运来的多项式里只包含x的指数和系数。如今加入了\(sin(x)和cos(x)\)的指数。最后获得一个项的list,而且根据相应的公式进行求导。求导的公式是
\(a*l*x ^ (a - 1)*cos(x)^c*sin(x)^b + b*l*x^a*cos(x)*cos(x)^c* sin(x)^(b - 1) - c*l*x^a*cos(x)^(c - 1)*sin(x)*sin(x)^b\)安全
此次做业在上一次的基础上更新正则表达式的匹配样例多线程
String patternHead = "[+-]{0,2}(([+-]?\\d+)|(" + "[x]([\\^][+-]?\\d+)?)" + "|([s][i][n][\\(][x][\\)]([\\^][+-]?\\d+)?)" + "|([c][o][s][\\(]" + "[x][\\)]([\\^][+-]?\\d+)?))([*](([+-]?\\d+)|([x]([\\^][+-]" + "?\\d+)?)|" + "([s][i][n][\\(][x][\\)]([\\^][+-]?\\d+)?)|([c][o][s][\\(]" + "[x][\\)]([\\^][+-]?\\d+)?)))*";
经过这个表达式去获得一个个项,而后经过split
函数将\(*\)号分开获得一个个因式,再经过因式的匹配样例架构
Pattern pattern = Pattern.compile("[+-]{0,3}[0-9]+"); Pattern pattern = Pattern.compile("([+-]{0,2})[x]([\\^]" +"?(([+-]?)(\\d+)))?"); Pattern pattern = "([+-]{0,2})sin[\\(]x[\\)]([\\^]([+-]?\\d+))?"; Pattern pattern = "([+-]{0,2})[c][o][s][\\(]x[\\)]([\\^]([+-]?\\d+))?";
分别获得了项的系数,\(x的指数,sin(x)的指数,cos(x)的指数\),而后存入咱们的结构体中。最后经过上述求导公式对每一个项进行求导,而且将相同系数的项合并。
类图以下
框架
能够看到其实第二次设计依旧没有秉持好的设计原则,虽然将不一样的功能写在不一样的方法里,可是没有实现类的分类,这里好的设计应该是sin(x),cos(x),x单独分类,而后进行求导,以及输出。然而这里混在了一块儿,致使main方法十分庞大,修改一个地方会致使不少方法都须要修改。由于代码之间的耦合度很是高,而且几乎全部的操做都是写在Poly里的静态方法,致使第三次做业又须要进行大规模的重构。编程语言
(3)第三次做业
此次做业是我三次里认为还比较满意的一次做业,由于此次的题目比较复杂,所以我以为不能再像前两次做业那样急于编写代码,由于这样会致使代码紊乱,最后难以找bug。所以在动手写代码以前,我仔仔细细地参照面相对象编程的思想,对第三次的题目进行了思考,先把类和接口设计好再进行动手编写代码,果真想清思路以后再下手,写起来快而且最后的bug也少了,代码思路很是清晰。
简单分析一下此次的做业,此次的多项式求导不只延续了以前的项的思路,还添加了嵌套求导的规则。即
表达式 = 项{+项} 项 = 因子*{因子} 因子 = (表达式)|因子
也就是相似\(sin(cos(x))\)这种嵌套的求导。我知道再延续以往的面相过程求导确定是行不通的了。所以此次的设计对每一步进行了细致的类的划分。类图以下。
下面来说一下个人作法,首先我此次划分了不少个类,常数类、x项类、cos项类、sin项类、指数项类、加法类、乘法类这些类,这些类都实现了一个求导的接口,也就是说全部求导的对象都是另外一个可求导的对象,好比说指数类里,指数的底数也是一个实现了求导的类,这样就很好地体现了分类的思想,而且在指数这个类里,我只需管指数函数是如何求导的,而不须要管其底数是什么,由于底数实现了求导接口,底数也会自动去调其覆写的求导方法去对底数进行求导。这样就使咱们的程序显得很简单。
这里的加法和乘法类就是指两个实现了求导接口的对象相乘进行求导,咱们只需关心乘法的求导是怎么样的,而具体对象的求导,放在具体的对象的求导里去完成,这样就真正实现了低耦合的思想。
具体的接口的代码以下:
public interface Derive { public abstract Derive derive();//求导函数 public abstract void print(); }
而后乘法里实现的求导接口的求导方法以下。
public Derive derive() { ArrayList<Derive> addList = new ArrayList<Derive>(); for (int i = 0; i < this.list.size(); i++) { /** 根据几个函数连乘的求导法则进行求导 * result = (ui' * (u1*u2*....un)/ui)的和 */ ArrayList<Derive> multList = new ArrayList<Derive>(); for (int j = 0; j < this.list.size(); j++) { if (i != j) { multList.add(this.list.get(j)); } } multList.add(list.get(i).derive()); Mult mult = new Mult(multList); /** * 这条multList 就是Add链中其中的一条 * */ addList.add(mult); } /** * 至次为止, addList是由好几个Mult类构成 */ return new Add(addList); }
咱们能够看到,对于\(f(x) = h(x)*g(x)\)的求导,只需关心\(f(x)'=f(x)'g(x)+f(x)g(x)'\)便可,而不需关心\(f(x)'和g(x)'\)是什么,由于\(f(x)和g(x)\)都是已经实现了求导方法的对象,在他的类里会调用本身的求导方法进行递归求导。
在明确了类的框架结构之后,咱们再想办法对字符串进行处理,我一开始尝试原来的正则文法匹配的方法,可是发现本身不明白如何去产生上述的表达式里嵌套因子的方式,可是我发现,这个形式和咱们以前学过的编译原理的递归降低分析法相似。因而我采用相同的思想先写了一个简单的词法分析小程序,而后构造了expr(),term(),factor()三个子程序来对字符串进行读取。这样就能实现程序的因子里嵌套表达式的形式了。下面举一个简单的表达式的例子。
/** * 自顶向下的表达式子程序 */ public static Derive expr() { /** * 表达式 = 项 {+项} */ ArrayList<Derive> exprList = new ArrayList<Derive>(); Derive term1 = term(); exprList.add(term1); if (!checkExprTail()) { System.out.println("WRONG FORMAT!"); System.exit(0); } while (sym.equals("Add") || sym.equals("Minus")) { headFlag = 1; term1 = term(); exprList.add(term1); } if (!checkTail()) { err(); } return new Add(exprList); }
这样在后续的调试过程当中我能够单独根据每种形式的求导来找问题,就能很快地发现是哪一个求导过程发生了问题,简明扼要。
各种代码总规模:(statistic)
SourceFile | Total LinesSource Code | Source Code | Comment Lines | Blank Lines |
---|---|---|---|---|
Derive.java | 8 | 4 | 3 | 1 |
X.java | 10 | 8 | 0 | 2 |
Constant.java | 25 | 19 | 0 | 6 |
Sinx.java | 28 | 18 | 4 | 6 |
Cosx.java | 28 | 20 | 3 | 5 |
Mult.java | 52 | 30 | 13 | 9 |
Add.java | 54 | 40 | 3 | 11 |
Degree.java | 61 | 36 | 13 | 12 |
Poly.java | 390 | 345 | 21 | 24 |
Total | 658 | 522 | 60 | 76 |
类的属性方法个数
类 | 属性 | 方法 |
---|---|---|
Derive.java | 0 | 2 |
X.java | 0 | 2 |
Constant.java | 1 | 3 |
Sinx.java | 1 | 3 |
Cosx.java | 1 | 3 |
Mult.java | 1 | 2 |
Add.java | 1 | 2 |
Degree.java | 2 | 5 |
Poly.java | 8 | 14 |
优势:
1:将复杂的多项式求导分红诸多形式的类,每一个类只需注意本身的求导形式,具体的求导规则由各个实现求导接口的类去完成。 2:采用递归降低子程序法,使字符串的处理比较容易理解,而且不容易出错。 3:使用了接口的思想,方即可扩展性。 4:实现了高内聚低耦合的思想,使每一个类和方法尽可能能干的事情少,各自之间互不影响。
缺点:
1:在处理项的连乘的时候可能会出现爆栈的状况。 2:没有作完备的可能发生异常的状况的统计与测试。 3:单元测试不够完备,Main类的设计还过于冗杂。 4:存在大量的if-else语句,不够精炼,存在代码复用比较严重。 5:输出的时候若是嵌套层次太多,会致使大量的()产生,很难进行优化。
第一次
第一次的bug在于没有处理指数或者系数过长可能抛出的异常,没有意识到助教和老师在做业指导书里的提示,这个问题在我理解了BigInteger类以后获得了较好的解决。而且在checkStyle风格检查的时候,没有按照规范要求的行数以及缩进。还存在表达式第一项若是为数字的话前面的符号的个数的问题。
第二次
第三次做业
经过第一个单元的学习,已经基本掌握了各类java类的用法,也理解了面向对象的设计思想。了解了继承与接口的原理。可是在使用上还存在不熟练的时候。但愿在往后进行多线程学习以前,可以把java的基础打扎实,写出漂亮稳定的好程序。