OO:第一单元总结博客
(1)基于度量来分析自己的程序结构
第一次作业分析如下:

类的属性:

类图:

- 主要分了Factor、Item、Expression三个类,分别代表因子、项和表达式,当时的设计初衷也就是对应着三个类三种元素,但是细想的话其实项可以看作合并了因子之后的产物,可以做到更精简的
- 总的来说,在设计上个人还是比较满意的,虽然扩展性做的并不好,后续仍做了重构,不过仅以本次作业的视角来看每个类之间设计的长度、互相之间的关联也比较合理,仅少数方法出现了特别复杂的情况。
- 属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模略显复杂,不过也算是减小了每一个方法的复杂度,各个类之间的耦合情况也中规中矩,并无之特别值得说明的地方
- 优点主要是相比互测屋中看到的一些代码,至少做到了有意识的多分几个类,设计一些耦合的方法等;缺点则主要为可扩展性不佳
- 虽然创建了不止一个类,但是这些类之间并无关系,而且Item和Factor两个类的设计有所重复,属性都是系数与指数,类之间的联系非常孱弱。其实这些属性和方法是可以写在一个类里面的,这就与C语言里的main函数没有什么区别,因此也没有摸到面向对象的门槛
第二次作业分析如下:
方法属性:

类的属性:

类图:

- 在本次作业中,对原先架构进行了大幅重构,设置了最核心的接口Term,并在其下分为Combo与Func两部分,Combo中包含Add、Mult,Func中则为Const、Power、Cos、Sin,前者是一种连接关系,后者则是一个具体的因子或是函数种类。另外的TermFactory用于生成Term类,DiffComputer则是进行核心字符串处理的类。
- 本次作业中最复杂的方法有三个:
- DiffComputer.getExpTree,用于由字符串类型的表达式获取表达式树,是最核心的处理方法;
- DiffComputer. checkSign,用于处理每一项、因子等单元最前面的多个符号问题;
- TermFactory.createTerm,是一个工厂方法,用于生成对应的具体类型的Term实例,因设计复杂的if、else等判断,因此相当复杂
- 不过我并不认为方法复杂说明不够好,事实上对字符串的处理过程中不可能处处合于面向对象的概念,必然存在一定的需要面向过程的繁琐处理,而这三个函数所选择的正是涉足泥淖,将凡尘因果一肩挑之,以此换得其余方法的澄澈清平与浮于表层的“面向对象”式的优良设计,他们才是真正的撑起面向对象式的设计的脊梁。不过或许也有其他更为优秀的设计方法值得借鉴,例如将他们拆成功能更加细分的方法,并进行一定组合,如果有更加优渥的设计,我也会乐于尝试一下。
- 相比于互相之间并无关联的前一次作业,这一次有意识的用到了接口、抽象类以及类之间的继承关系,还有令人欣喜的使用了工厂模式。接口、抽象类等运用更加贴合面向对象的设计思想,也使得可扩展性变得良好,其一个优点就是在之后的第三次作业中我并不需要大幅修改即可实现任务目标;尽管pre2中提及过不少次建议使用工厂模式,不过在此次作业中我才能够体会到工厂模式的优势,并予以运用,取得了简洁精炼的设计成效。
第三次作业分析如下:
方法属性:

类属性:

类图:

- 第三次作业增加了检测格式是否正确的要求,大多数类与第二次几乎相同,仅新增加了一个FormatComputer类,使用了一个递归且巨大的正则表达式匹配原字符串是否符合要求。然而整体上仍然非常臃肿,从类图中可以看到,在DiffComputer和FormatComputer两个类中对一些重复的变量定义了两次,而且对字符串检查且匹配了两次,非常浪费且效率低下,理论上可以将它们合并或是边检查边匹配(分割)。而不是现如今的百足之虫般的设计,程序分析也指出了这两个类的平均循环复杂度OCavg非常之大
- 另一方面,因为一直没太明白许多人宣之于口的递归下降的原理(最后一次研讨课上有同学分享了递归下降的具体做法,不过我觉得并没有体现太大优越性),在对括号的处理上仍然沿用了上一次的外层括号匹配+替换+括号内部递归处理的方法,因此依然存在几个极其庞大滞涩的方法,复杂度非常大,这也是做的不够好的地方之一
- 具体到Term中的每个类中修改非常少,仅仅修缮了Cos、Sin中的一些构造方法和求导方法,这也是上一次打下良好基础的贡献
- 另一处非常失败的设计是TermFactory这个工厂类权力被大幅收束并削减了,上一次作业中TermFactory负责Func中每一个具体因子的创建,包括常数Const、幂Power、正弦Sin和余弦Cos,只需要传入一个字符串,该工厂类会匹配具体的Term类型并返回创建的对象
(2)分析自己程序的bug
- 第一次作业中,或许是程序整体复杂度和架构较简单的原因,强测和互测都没有出现bug。
- 第二次作业中,互测被hack了22次,但其实都是基于一个bug导致的问题。我认为问题其实出现在我对java函数的具体机制不够熟悉了解上。我当时在对括号的处理上采用了外层括号匹配+替换+括号内部递归处理的方法,为此,需要将一个括号中的内容暂时“提取”出来,而原先的括号部分替换为一个临时替代字符,再次调用本函数获取括号内部分的子表达式树,之后再将子表达式树合理地连缀到总表达式树上。在替换时,我用的是String类的replace和replaceAll方法,但这可能会导致替换了错误位置的错误字符串,之后修改为手动的字符串拼接方法得以修复bug。
- 第三次作业仅强测错了一处细微的地方,作业要求指数不能超过50,我当时随手偷懒就用了一个int类型的临时变量存储搜索到的指数,以至于遇到一个巨大指数的情况下检查格式时运行错误。大概算是一种警示吧,在java中BigInteger还是更加安全的选择,这也是一种程序“鲁棒性”的体现。
- 第二次作业出现bug的方法为DiffComputer.getExpTree(),算上注释在内,该方法共有93行,排名所有方法第1;圈复杂度V(G)为14,排名所有方法第2。第三次作业出现bug的方法为FormatComputer.checkExp(),算上注释在内,该方法共有15行,圈复杂度V(G)为3。尽管课程组试图暗示应当得出的结论是方法行数越多、圈复杂度越高,越容易产生bug,这也与统计与概率结果相符。不过综合我的两次bug的产生的方法来看,实在难以得出这样的结论,也许是数据样本太小的原因?总之与bug产生有关联的因素还有待后续的进一步观察
(3)分析自己发现别人bug采取的策略
- 在这三次互测中,或许是因为我寻找bug的策略不够优越,仅仅在第一次找到了别人的bug。我了解到许多人都是采取自己使用python写评测机的方式批量测试互测屋内所有人的程序正确性,大量投喂数据暴力试错。但我其实更倾向于阅读代码的方式,一方面能够更好的了解他人的代码架构与思路,借鉴他人的设计思路与细节的处理;另一方面在大家都能够通过中强测的前提下,普通的测试数据往往难以奏效,产生bug的地方往往是一些个性化的问题,例如一处缺少考虑情况的化简、或是一两处针对特定情况才会发挥效用的隐秘逻辑错误。
- 不过这种debug方式并没有产生想象中的良好效果,能够迅速排查出bug,一是由于在缺少注解或旁批的情况下难以快速理解他人的思路,导致难以理解其代码书写逻辑与每一部分的功用;二是我观察其他人的架构发现部分人的代码从文件名命名、函数命名到变量命名都略显随意,似乎只具有唯一性阅读者,简单来说就是代码风格比较粗滥,不太有阅读的欲望,因此也没有太大耐心读下去。
- 最终我提交的测试数据许多都是临时构造的,不是刻意追求长度与格式上的复杂,而是大多尝试一些特殊的临界条件,例如多个符号相连,或是特殊的指数与因子等。也有很大一部分数据是从我编写程序、提交中测的过程中逐步自用的测试数据,不过也许是因为同屋的dalao思维与设计都比较缜密,其实大多hack不到他人程序中的bug。不过总体来说互测这一环节还是颇具趣味性与挑战性的。
(4)重构经历总结
- 三次作业中,我最大的重构设计是在一、二两次之间,几乎是完全重写了一遍全部内容,之后的第三次则基本没什么改动,仅添加了几个函数。
- 第一次作业我有尝试刻意“反面向过程”也就是“面向对象”一下,不过也没太理解这些理念具体意义以及我具体应该怎么做,于是就多分了几个类,分别是表达式Expression、项Item、因子Factor,尝试在求导、输出为字符串的环节一层层调用,也就是表达式求导就调用项的求导再拼接,输出也分别调用每个项的输出在拼接。我自认为这种设计理念已经浅涉到了面向对象的门槛,唯一的问题就是分割的还不够彻底,之后的第二次作业中才将表达式做到彻底的剖解。
- 第二次作业添加了sin、cos、括号的问题,比第一次的难度与复杂度有了相当大的跃进,对既有类的体系运转造成了急剧冲击。当Item或Factor类沉湎于虚幻的满足时,回头时却骤然发现大厦将倾,归于崩坼。其实在课上老师有过比较明示的暗示,把“对象”看成一个常数、指数、或是一个A+B、一个A乘B,万物皆为对象。对照着前人博客的分类与设计的指引,我最终设计了十个类左右,主类为Term,下辖了Combo与Func两个分类,对应计算类组合体与一种独立的“数”,前者含add与mult两种,后者包括power、cos、sin、const几种,但是在写出这些类的时候,我还并不太清楚“然后该干什么以及怎么办”,只是模糊地觉得应该先创建出来再说。
- 不过第二周是有研讨课的,不少同学分享了对第二次作业的看法与设计,再结合网上博客中对每个类写的函数内容,与助教展示的一个用C程序写的“面向对象”完成四则运算的过程。我尝试性地创构出主体思路,先在提取外层括号的同时构建一个由无数Term运算组合成的Term型表达式树,之后对表达式树求导仍然返回一个Term类型的树状结构,最后将求导后的树转换为一个字符串型的输出并予以优化。由此,我在每个具体的Cos、Sin或是Power等类中写下了构造方法、求导方法、转换为字符串方法,但事实上写完这些方法的时候,我还没有解决一个最关键的问题,也就是这个能够完整且唯一代表这个表达式的树该如何构建,而我此时做的工作只是一座基于这个树的空中楼阁。
- 在此就需要处理括号嵌套、表达式类型的因子等无数问题,针对括号的问题,在讨论区有人指出了递归下降的方法,也有提出“文法分析”还是什么之类的策略,然而问题是我根本看不懂也没理解这些都是个什么操作,
- 第三次作业改动非常小,只是对Sin和Cos类内部新增了inner也就是处理内部内容的部分,新增了一个FormatComputer类也就是处理、判断格式是否合法的类。因为没有涉及太大的重构,就不展开介绍了
(5)心得体会
第一次作业
正则表达式
public void parseItem() { String str = this.origin; String sign = "[+-]?"; String cnst = "[+-]?[0-9]+"; String power = "x(\\^[+-]?[0-9]+)?"; String factor = "((" + cnst + ")|(" + power + "))"; //变量因子 | 常数因子 String item = sign + sign + "(" + factor + "\\*)*" + factor; //[加减 空白项] 因子 | 项 空白项 * 空白项 因子 Pattern p = Pattern.compile(item); Matcher m = p.matcher(str); while (m.find()) { String splitItem = m.group(); Item nitm = new Item(splitItem); itemList.add(nitm); //TODO System.out.printf("Term : %s%n", splitItem); //TODO System.out.printf("Term split : %s%n",nitm.getOrigin()); } }
第二次作业
括号嵌套
第二次作业一大挑战就是对嵌套括号如何处理的问题,我采用的是讨论区同学分享的“简单外层括号匹配”的想法,对每一个检测到的括号进行递归式的表达式树生成,然后将这一结果存入一个Hashmap,并将该括号部分进行替换,之后再将子表达式树替换回来,隐约觉得这就是递归的思想,却又不够确定,依照感觉模模糊糊地往下写。不少同学了解到“递归下降”的处理方式,然而我其实没弄清楚具体是个什么方法,网上的资料也凌乱不堪,由于我的表达式树生成的部分涉及到了递归地处理方式,于是就把目前我写的方法当作疑似的“递归”下降来看了,虽然事实上不是。具体代码如下:
public Term getExpTree(String str) { Stack<Integer> stack = new Stack<>(); ArrayList<Integer> childExpList = new ArrayList<>(); for (int i = 0; i < str.length(); i++) { char current = str.charAt(i); if (current == '(') { stack.push(i); } else if (current == ')') { int popout = stack.pop(); if (stack.empty()) { if (popout - 3 >= 0 && (str.startsWith("sin", popout - 3) || str.startsWith("cos", popout - 3))) { continue; } else { childExpList.add(popout); childExpList.add(i); } } } } //TODO System.out.println("After FindKuoHao:" + str); //TODO System.out.printf("Child Size:%d%n", childExpList.size()); String tihuanStr = ""; String endStr = str; String middleStr; for (int j = 0; j < childExpList.size(); j = j + 2) { String childStr = str.substring(childExpList.get(j) + 1, childExpList.get(j + 1)); //String childStrWithBracket = //str.substring(childExpList.get(j), childExpList.get(j + 1) + 1); Term childRoot = getExpTree(childStr); bracketMap.put(bracketCount, childRoot); //tihuanStr = tihuanStr.replaceFirst(childStrWithBracket, "@" + bracketCount); middleStr = (j == 0) ? str.substring(0, childExpList.get(j)) : str.substring(childExpList.get(j - 1) + 1, childExpList.get(j)); endStr = str.substring(childExpList.get(j + 1) + 1); tihuanStr = tihuanStr.concat(middleStr).concat("@" + bracketCount++); //bracketCount++; //todo System.out.println("@" + (bracketCount-1) + " : " + childRoot.toString()); //todo System.out.println("Child Bra : " + childStrWithBracket); //todo System.out.println("TH : " + tihuanStr); } tihuanStr = tihuanStr.concat(endStr); //TODO System.out.println("TiHuanHou:" + str_tihuan); Pattern p = Pattern.compile(item); Matcher m = p.matcher(tihuanStr); Term fullTerm = new Const("0"); while (m.find()) { String perTerm = m.group(); //todo System.out.printf("Found Term : %s%n", perTerm); String[] splitTerm = perTerm.split("\\*"); Term lastTerm = new Const("1"); for (String eachterm : splitTerm) { //todo System.out.println("Split : " + eachterm); Term newTerm; if (Pattern.matches("[+-]{0,3}@\\d+", eachterm)) { //检查是否括号替换品 boolean fuFlag = checkSign(eachterm); eachterm = eachterm.replaceAll("[+-]{0,3}@", ""); //TODO System.out.println("Term After TH:" + eachterm); int bracketNum = Integer.parseInt(eachterm); newTerm = bracketMap.get(bracketNum); if (fuFlag) { newTerm = new Mult(new Const("-1"), newTerm); } } else { newTerm = TermFactory.createTerm(eachterm); } lastTerm = new Mult(lastTerm, newTerm); //todo System.out.println("NewTerm : " + newTerm.toString()); //todo System.out.println("LastTerm : " + lastTerm.toString()); } fullTerm = new Add(fullTerm, lastTerm); } //todo System.out.println("FullTerm : " + fullTerm.toString()); //todo System.out.println("FullTerm Diff : " + fullTerm.getDiffResult().toString()); return fullTerm; }
该方法简单的可分成三个部分,第一部分使用栈搜索最外层括号,第二部分递归调用本函数获得子表达式树并存入HashMap,同时替换字符串原部分内容,第三部分寻找每一个“项”,遇到替换品则换回子表达式树。
表达式树?
另一个问题是构造怎样的数据存储形式?诸如求导等运算的返回对象类型又是什么?这里采用的就是表达式树的方式,原字符串是一个表达式树,求导后得到的还是一个表达式树,当然这是求导后的。其实抽象来说就是一种在Add、Mult的基础上的一种组合方式,Add和Mult的设计我是从往届的博客中获得的灵感,最妙的一处设计就是Add和Mult类中的两个属性,Term类型的left与Term类型的right,分别代表左子树与右子树,可以观察这个求导函数来体会一下“表达式树”的具体操作流程(前面是Sin求导,后面是Power求导):
public Term getDiffResult() { if (exp.equals(BigInteger.ZERO)) { return new Const("0"); } else if (exp.equals(BigInteger.ONE)) { return new Cos("cos(x)"); } else { Term cosx = new Cos("cos(x)"); Term frontcoeff = new Const(exp.toString()); Term newsin = new Sin("sin(x)^" + exp.subtract(BigInteger.ONE)); return new Mult(new Mult(frontcoeff, newsin), cosx); } }
public Term getDiffResult() { if (exp.equals(BigInteger.ZERO)) { return new Const("0"); } else if (exp.equals(BigInteger.ONE)) { return new Const("1"); } else { Term n = new Const(exp.toString()); Term mult = new Power("x^" + exp.subtract(BigInteger.ONE)); return new Mult(n, mult); } }
第三次作业
格式检查
第三次作业并无太多可说的,主要的区别还是检查格式,这一步我做的并不足够好,甚至重复对同一个字符串部分进行了两次运算与检查。具体到实现方法上,我采用的是与生成表达式树类似的“递归”式检查,先搜索“最外层括号”,之后判断去除外层括号后的字符串是否合法,对每个括号再搜索内部格式是否合法,具体代码如下:
public boolean checkFormat(String str, String type) { Stack<Integer> stack = new Stack<>(); String cuttedStr = ""; String endStr = str; int lastEnd = 0; for (int i = 0; i < str.length(); i++) { char current = str.charAt(i); if (current == '(') { stack.push(i); } else if (current == ')') { int start = stack.pop(); if (stack.empty()) { String substr = str.substring(start + 1, i); cuttedStr = cuttedStr.concat(str.substring(lastEnd, start + 1)).concat(")"); endStr = str.substring(i + 1); lastEnd = i + 1; //todo System.out.println("Cut : " + cuttedStr); //todo System.out.println("Sub : " + substr); if (start - 3 >= 0 && (str.startsWith("sin", start - 3) || str.startsWith("cos", start - 3))) { if (!checkFormat(substr, "Factor")) { return false; } } else { if (!checkFormat(substr, "Expression")) { return false; } } } } } cuttedStr = cuttedStr.concat(endStr); //todo System.out.println("Cut End : " + cuttedStr); //替换完 switch (type) { case "Expression": return Pattern.matches(expression + space, cuttedStr); case "Factor": return Pattern.matches(space + factor + space, cuttedStr); default: return false; } }
另外还有一种思路是递归下降的方式,似乎可以做到边运算处理边检查格式,我到现在也不是很明白,好像大概意思是用一个指针往后扫,目前扫到的种类之后连着的只能是某一些种类,并以此判断。不过我并不喜欢这种设计,感觉写起来颇有枚举的区隔感,反而是我目前采用的这种递归更合简洁明了的方式
关于博客作业的感受
坦白地说,我并不喜欢博客作业。尽管博客可以起到一定的总结与回顾效用,但我更多感受到的却是一种四处碰壁的束缚感。不妨先从作业要求开始说起吧
作业要求
- 其实我理想中的博客作业是面向下一届初涉OO的新人而写的,而不是同侪间的评比与度量。在最初拿到表达式求导这一题目时,由于当时极其迷茫,完全不知从何下手,因此我阅读了网上大部分可以查到的博客,但给我的感受却是越来越困惑与迷茫:不仅看不懂他们究竟在说什么,画的类图到底表现了一个什么含义,更对其完全不点明关键实现细节的行文而恼怒。我希望看到的不是空泛的一句句“表达式树”或“递归下降”,也不希望他长篇累牍地分析自己的代码复杂度等内容,而是有真实的技术细节与实现方式:字符串如何处理?设计怎样的容器存储?需要用到正则表达式等技术吗?总体的实现步骤与环节是怎样的?尽管完全展露实现过程甚至直接贴代码的方式违背了独立思考、百花齐放的作业本义,恰恰相反的是,通过对代码的阅读与体会不仅可以对大致流程框架有一个基本了解,而且在处理同一问题上,不同博客间的对比与张力中能够凸显出最优越的设计与架构。因此我也有意识地详细阐明了几次作业的关键难点与我的实现方法,抛砖引玉,以供咀嚼。
- 作业要求的第一部分是关于度量的,要总结分析度量类的属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模计算经典的OO度量,分析类的内聚和相互间的耦合情况。然而事实却是大家都采用一些插件与工具一键绘制出这些方法与类的表格,之后干瘪地进行一通大同小异的数据分析,然后再自我批评两句,称自己的设计缺少“面向对象式的设计”还有待改进云云。我并不是认为这种制式的分析没有意义,而是认为行文的中心更多的应该放在对自身感受的挖掘上,而非一遍遍地阐释数据,数据分析的意义也应当回归对今后设计上的指导与从中获取的思考与感受。例如回顾或阐明这些类与方法设计时的感受与想法?为什么要如此设计这些方法?结合数据分析出的复杂度来看,在设计时如何平衡或取舍功能性与复杂度?等等,这也应当是博客写作的价值所在
“类图”困境
- 画类图是个有趣的活动。尽管作业要求中重点强调了“不要使用IDEA等工具“无脑”逆向生成类图”,但在大多数情况下类图都是直接逆向生成出来的,我也想寻找到快捷的一键生成的方式。然而令人沮丧的是,我所使用的IDEA的Community社区版中没有一键生成diagram的功能,Ultimate旗舰版则具有这一功能,功能上的缺失简直是一种降维打击,我甚至将这种要求视为一种刁难:只有安装了IDEA之Ultimate版的群体才能够依照要求完美地完成本次作业,否则则需要大费周章。
- 不过最终的解决方式也颇有收获,安装插件并学习了plantUML的语法与UML图的绘制,但是终究还是需要自己一点点敲属性、方法才能绘制出最终的UML图,产生了不少无谓的耗散。自动生成类图并不一定天然地比手动地制作每一张类图卑劣,毕竟代码是自己写出来的,理应对结构于布局相当了然于胸,即便不生成类图,勾勒的类图也应存于想象的虚空中。大概就像数据在图形上的可视化过程,更好地表达代码结构与展现自己的构思,只要达成了目的,不妨一试
其它
- 要求别人写感受是一件“有趣”的事,批阅者喜欢咀嚼赏鉴他人的“真实感受”,这种感受通常千篇一律、合人心意地宣称自己获得了某方面的提升,从中不难汲取到虚幻的满足感与训诫感。
- 这也是我在写第一次作业前阅读大量博客的一种非妄的感受,所有博客都在信誓旦旦地宣称自己通过三次作业,获得了“面向对象式的设计”上的提升,感受到了面向对象优越于面向过程写法的伟大光辉。然而事实上我通过三次作业的感受却截然相反:愈加接触面向对象的设计方法,感受到的却更多是设计改进的永无穷尽与自身的微渺,永远都有更加优越、灵活、简洁、清晰、延展的设计,就像是瞻望浩瀚星空,对采取何种设计愈发敬畏、犹疑或是自我否定。尽管许多类的设计与写法上已经合于主流的架构,但仍有一部分方法摆脱不了芜杂与繁冗的桎梏。不过在设计的坦途上无涯无际也是一件好事,至少时时刻刻仍有优化与改进的方向,皆活泛着新鲜的气息
- 完成全部博客作业带来的疲乏多于满足,甚至沦为对博客长度上的内卷,拼比的则是字数的堆叠,而不是一次纯净的映照内心的观想。事实上这毫无意义,每个人只是充当着一种写字工具,我甚是耗费了多于一次编程作业的时间来撰写这次博客,也许下次就会转于应付,再也许每个人的心血之作只会被视作千篇一律中的一篇,匆匆打分而转眼翻过。希望每一个人都会更加喜欢完成接下来的博客作业,不是为了完成任务、作业或分数而争逐,而是真正地作为抒发内心感受、总结过去展望未来的跳板,成为一幅写字的帷幕。

浙公网安备 33010602011771号