2019年北航OO第1单元(表达式求导)总结

1 基于度量的程序结构分析

量化指标及分析

以下是三次作业的量化指标统计:





关于图中指标在这里简要介绍一下:

  • ev(G):基本复杂度,用来衡量程序非结构化程度。基本复杂度高意味着非结构化程度高,难以模块化和维护。
  • Iv(G):模块设计复杂度,用来衡量模块判定结构,即模块和其他模块的调用关系。模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。
  • v(G):模块判定结构复杂度,数量上表现为独立路径的条数。

从上面三张图可以看出,整体上3个复杂度指标基本都是随着作业进度的推进而逐渐降低。另外,异常值超出正常范围的程度也在逐渐降低。第2次作业中由于需求更加复杂,然而程序结构并没有跟上这种变化,使得其中出现了模块设计复杂度和模块判定结构复杂度高达23的方法。到了第3次作业,虽然也存在一些过于复杂的结构,但是和前两次相比能看出明显的优化。这大致上可以说明第1单元的3次程序设计在向着一个更加合理、结构更加优化、耦合度更低的方向前进。

类图及分析

由于第一次作业我仅构造了一个类,故在下面只放了第2次和第3次作业的类图:



从类图的变化可以明显看出,随着作业进度的推进,程序的结构越来越模块化,程序的逻辑越来越清晰明了。这种变化的趋势也和我自身的感觉很接近。第1次用面向对象的语言写程序时,我还待在“舒适区”里,从思考的方式到具体的实现都是沿袭的C语言和面向过程的思路。

在第2次作业中,我开始尝试将最开始的一个大类分裂为功能更加集中的三个类,并尝试了更多的Java提供的数据结构来帮助对数据进行管理。不过这种分裂还不够彻底。从代码量上来看,Poly类依然占据着整个程序绝大部分的比例。Poly类中的方法也依然很多,相互联系也不够紧密。

经过了前两次的训练,到了第3次作业,我才算是真正地构造出了面向对象的程序结构。每个类内部都很紧凑,类之间的连接逻辑又很简单、很“线性”。具体地,Sin、Power等类是代表一种类型的因子,而Factor接口则是这些因子的一个汇总;Term和Expr分别代表项和表达式,其中Expr既是表达式同时也是表达式因子;最后InputOutput类则是专门负责输入字符串的检查、预处理以及求导后输出字符串的化简。

2 程序bug分析

需要提前指出的一点是,由于高工的OO课程并没有互测环节,因此以下的分析都是基于我自己在调试过程中找到的bug以及未通过公测用例的情况。

2.1 bug的特征与位置

如上文所说,第1次作业的需求很单一,因此程序结构也非常简单,bug并不多,而且即便遇到错误也可以很快地定位到相应的代码。我认为出现问题相对较多的部分是语法分析方法parsePoly()。第一次作业我并没有使用正则表达式来提取输入字符串的信息,而是仿照上学期编译器实现中的自顶向下的语法分析器来进行字符串的解析。这种做法导致的一个问题是方法内的复杂度和体积都非常大,而且逻辑交错,处于一种“非线性”的状态。

我自己找到的bug里占主要部分的就是,忽略了某一语法分支的走向而导致程序进入异常状态。举例来说,由于空白字符的存在,在取下一个字符时会自动跳过它们,但是对于wrong format的输入,程序就可能在判断下一个字符的时候突然发现字符串已经读完了,进而报错。下面所示的是加入“结束判断”后的代码:

while (i < polystr.length && Character.isDigit(getchr(polystr))) {
// blah blah blah
}

第2次作业和第3次作业的程序相近,所以我就在这里一起总结了。

最容易出错的地方,正则表达式(regex)绝对算是其中一个。这类bug的特征是:

  • 分散在各个类中,定位有难度;
  • 纠错手段有限,目前我知道的方法只有“定位到具体regex后用肉眼检查”;

除去手抖打错的情况,其实regex问题归根结底是设计时思考得不够全面,有些情况没有考虑到。我能想到的一种解决方法就是尽可能地缩短每一个regex的长度,再用其他策略把regex结合起来。

此外,在一些条件多且复杂的if-else语句块内也比较容易出现bug,这类bug一般都是typo,但是其产生的原因是设计和架构问题。例如,在第二次作业中的求导函数derivativePoly()中我将三种类型的函数(x, sin, cos)放在了一起进行求导,所以需要判断是否有某一种函数指数为0的情况。这样以来就会出现8种情况需要判断。解决这类问题还是要从源头改变,让这种语句块根本就不出现。

更高层面上,有时对于需求的错误分析会导致程序结构出错。在第3次作业中,需要对表达式(expression)进行分裂,得到项(term)。起初我单纯地认为只需以加减号作为标志分裂即可,后来在公测和自测样例未通过后才想到需要加入“括号深度判断”,并且排除“指数内的符号”和“无符号整数内的符号”这些情况。代码如下所示:

if ((exprArr[i] == '+' || exprArr[i] == '-') && depth == 0 && exprArr[i - 1] != '^' && exprArr[i - 1] != '*') {
splitLoc = i; // split point
break;
}

最后一个bug的来源是对于Java语言的不理解或不熟练,有时会错误理解函数的功能和使用方法。这种bug和程序结构、程序内容关系不大,但我认为仍是一个值得注意的问题。

2.2 bug位置与设计结构之间的相关性

从以上对于bug位置和特征的总结来看,大多数bug都是和设计与结构息息相关的。

一般情况下,bug大多产生在程序结构复杂、逻辑混乱、数据类型繁杂、代码执行上下跳动不可预测的地方。这些部分的代码在某种程度上违背了“线性”的设计原则,使得在写代码时容易写错,在检查代码时也不容易发现问题。更严重地,有时程序结构本身就存在问题,bug只是这类问题的表现形式。比如说,程序可能运行到某些分支但是代码却没有对相应分支进行处理,进而导致进入不可预测的异常状态。

一句话来说,结构设计既有可能是bug的催化剂,也有可能是bug的直接原因。

2.3 从分类树角度分析程序在设计上的问题

分类树是指测试内容依据某些特征进行分类形成的树状层次结构,非叶子结点代表分类条件,叶子结点代表一些相似测试用例的集合。事实上一个程序里类的继承关系和接口关系也可以构成一棵树,而这两棵树之间并不是完全独立的。如果能在设计架构时让程序的类树尽可能地贴近测试内容的分类树,那么不仅程序的结构会很清晰,同时在调试的过程中也更加容易发现bug。我觉得,这种设计方法就算作是Test Driven Development(TDD)的一种应用。

3 发现bug所采用的策略

同样的,由于高工的OO课程并没有互测环节,因此只在这里介绍一下我发现自己程序里bug所采用的策略——静态检查和设计测试样例。

3.1 静态检查

静态检查通过肉眼检查和发现代码中的缺陷,依赖程序员对代码的理解和逻辑推理。我认为静态检查可以分为代码风格检查和代码逻辑检查。代码风格检查旨在发现“高危区域”,像是未初始化的变量、过长的条件判断语句等,都是容易出错的地方,对于这些代码要格外注意,多检查几遍。而代码逻辑检查则是按照代码的执行顺序阅读代码,查看是否有”悬空“的分支或者死循环的分支等等。

3.2 设计测试样例

正如上文2.3节提到的,在设计测试样例是需对于分类树的每个分支均有覆盖,同时还要考虑到不同测试样例组合的情况。以带符号整数为例,不仅需要测试各种符号和整数的组合以及在不同位置插入空格的情况,还要考虑带符号整数出现在指数位置、出现在常量因子位置、出现在sin()/cos()函数内部、出现在表达式内部等等情况。

4 Applying Creational Pattern

其实我个人认为,某种程度上在第3次作业中我已经应用了工厂模式的流程来创建和管理类,尤其是各种因子类。在因子的上一层级的“项(Term)”类的构造方法里,我通过正则表达式来判断某一个因子属于哪种类型,随后创建一个该类型的类。代码如下:

// figure out which factor it is
if (factorList.get(i).startsWith("sin(x)")) {
// class Sin
Factor f = new Sin(factorList.get(i));
factors.add(f);
} else if (factorList.get(i).startsWith("cos(x)")) {
// class Cos
Factor f = new Cos(factorList.get(i));
factors.add(f);
} else if (factorList.get(i).startsWith("sin(")) {
// class SinEmbed
Factor f = newSinEmbed(factorList.get(i));
factors.add(f);
} else if(factorList.get(i).startsWith("cos(")) {
// class CosEmbed
Factor f = newCosEmbed(factorList.get(i));
factors.add(f);
} else if (Pattern./matches/("[+-]?\\d+", factorList.get(i))) {
// class Constant
Factor f = newConstant(factorList.get(i));
factors.add(f);
} else if (Pattern./matches/("x(\\^[+-]?\\d+)?", factorList.get(i))) {
// class Power
Factor f = newPower(factorList.get(i));
factors.add(f);
} else if (Pattern./matches/("\\(.+\\)", factorList.get(i))) {
// class Expr
Factor f = newExpr(factorList.get(i).substring(1, factorList.get(i).length() - 1));
factors.add(f);
} else {
System.out.print("WRONG FORMAT!");
System.exit(0);
}

至于涉及到任意一种因子的求导等计算时,我通过Factor接口来实现这些操作。每个因子类都implements该接口,并分别实现求导函数derivative()和输出函数toStr()的具体实现。这些设计可以高效地分离各部分代码,降低各模块之间的耦合度。

5 Coda

第一次撰写博客,难免会有疏漏之处,还烦请大家指出错误啦。谢谢~

2019年北航OO第1单元(表达式求导)总结的更多相关文章

  1. BUAA-OO-第一单元表达式求导作业总结

    figure:first-child { margin-top: -20px; } #write ol, #write ul { position: relative; } img { max-wid ...

  2. 2019年北航OO第四单元(UML任务)及学期总结

    第四单元两次作业总结 第十三次作业 需求分析 本次作业需要完成一个UML类图解析器,所需要解析的只有符合UML标准和能够在Java 8中复现的UML类图.查询指令存在两种:仅与所查对象有关的指令,以及 ...

  3. 2019年北航OO第4单元(UML)总结

    1 架构设计 经过了接近一学期的程序设计训练,在这一单元的第一次作业中我就非常注重架构的设计,竭力避免像之前一样陷入"第一次作业凑合,第二次作业重构"的不健康的迭代模式.整体上来说 ...

  4. OO第一单元表达式求导作业总结

    第一次作业 功能描述: 对输入的表达式进行求导计算和格式正误判断   思路: 一开始的想法是想写一个大正则找到一个通项式,通过这个多项式来判断WRONG FORMAT,结果发现正则写的总是不完善,会漏 ...

  5. 第一次oo博客作业--表达式求导

    (1)说实话我这部分真的不知道写些什么,因为我只有第三次作业写了两个类,前两次都是一个类,一个类的好处可能也就是写起来比较方便(不用抽象什么共性了,直接c语言莽过去),缺点很多,架构不清晰,可读性不高 ...

  6. 2019年北航OO第三单元(JML规格任务)总结

    一.JML简介 1.1 JML与契约式设计 说起JML,就不得不提到契约式设计(Design by Contract).这种设计模式的始祖是1986年的Eiffel语言.它是一种限定了软件中每个元素所 ...

  7. 2019年北航OO第2单元(电梯模拟)总结

    1 三次作业的设计策略 经过了上一单元的训练,我也积累了一些设计策略上的经验.在这一单元的一开始,我便尽可能地把问题中的各个功能实体区分开来,分别封装成类,以便于随后作业中新需求的加入.与此同时,我也 ...

  8. 2019年北航OO第3单元(JML)总结

    1 JML语言的理论基础及应用工具链 1.1 JML语言 Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为.它结合了Eiffel的"契约设计(design by ...

  9. 2019年北航OO第四次博客总结&lt;完结撒花&gt;

    一.UML单元架构设计 1. 类图解析器架构设计 1.1 UML类图 这次作业的目标是要解析一个UML类图,首先为了解耦,我新建了一个类UmTree进行解析工作,而Interaction类仅仅作为实现 ...

随机推荐

  1. 编译CDH Spark源代码

    如何编译CDH Spark源代码 经过漫长的编译过程(我编译了2个半小时),最终成功了,在assembly/target/scala-2.10目录下面有spark-assembly-1.0.0-cdh ...

  2. CentOS 7 时间同步

    在做这个之前需要先搭建yum http://www.cnblogs.com/jw31/p/5955852.html 在做之前我们需要先安装ntp服务 yum install ntp -y vi /et ...

  3. Swift 学习笔记第一天-变量常量,及数据类型

    1.定义变量 用关键字 var 比如 var i=2 2.定义常量用let 如let c=3 可见Swift 定义时不用指定类型.由编译器推断 如果想指定类型 var i:Int32=2 练习 let ...

  4. func_num_args(),func_get_arg(),func_get_args()

    <?php function testFunction1(){ return func_num_args(); } function testFunction2(){ return func_g ...

  5. angularjs $watch demo

    <!doctype html> <html lang="en" ng-app> <head> <meta charset="UT ...

  6. Javascript注意事项一【防止浮点数溢出】

    num = 0.1+0.2; //0.30000000000000004 a = (1+2)/10; //0.3(浮点数中的整数运算时精确的)

  7. EZOJ 网同14(蛋蛋与北大信科-Splay的颜色分离,寻找结点所在子树)

    蛋蛋与北大信科 总时限 10s 内存限制 256MB 出题人 lydrainbowcat 提交情况 1/25 背景 琰琰(孩纸们读作:蛋蛋)是妙峰书苑的一名萌萌哒教师,她的夫君(孩纸们称之为:北大信科 ...

  8. 3. Linux系统磁盘分区介绍

    1. 磁盘分区基本知识 1)磁盘在使用前一般要先分区(相当于建房子要分房间一样). 2)磁盘分区一般有主分区.扩展分区和逻辑分区之分.一块磁盘最多可以有4个主分区,其中一个主分区的位置可以用一个扩展分 ...

  9. 一篇文章搞定mongodb

    一 安装 1 安装目录下新建文件夹data,etc,logs #在bin文件下启动cmd,指定数据存储的路径mongod --dbpath D:\MongoDB\data\db 2 etc文件夹中新建 ...

  10. expprt与环境变量

    一.Windows 环境变量 1.在Windows 系统下,很多软件安装都需要配置环境变量,比如 安装 jdk ,如果不配置环境变量,在非软件安装的目录下运行javac 命令,将会报告找不到文件,类似 ...