BUAA_OO第一单元作业总结
单元总述
这个单元分为三次作业,一步一步对函数求导功能进行了完善,内容从最开始的多项式加减到最后多项式、三角函数的嵌套。这是我第一次接触面向对象这种编程方法以及第一次使用Java这种编程语言。现在回头来看,实现的过程中有很多幼稚的地方,也有很多遗憾。随着三次作业难度的递增和对扩展性的欠考虑,我不得不每次都对程序进行整体的重构。但总结而言,在每次作业的过程之中确实收获了很多。那接下来这篇博客就会从三次作业的架构、编写和测试的过程中的感受进行一个总结。
基于度量的分析
一、什么是基于度量的分析
我采用了idea的Metrics来进行分析,关于分析结果的解释借鉴了学长的博客(https://www.cnblogs.com/qianmianyu/p/8698557.html)
Complexity Metrics(复杂度分析)
这部分我们需要使用的主要是方法和类的复杂度分析。
方法的复杂度分析主要基于循环复杂度的计算。循环复杂度是一种表示程序复杂度的软件度量,由程序流程图中的“基础路径”数量得来。
ev(G):即Essentail Complexity,用来表示一个方法的结构化程度,范围在$[1,v(G)]$之间,值越大则程序的结构越“病态”,其计算过程和图的“缩点”有关。
iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在$[1,v(G)]$之间,值越大联系越紧密。
v(G):即循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。
对于类,有OCavg和WMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。
Dependency Metrics(依赖度分析)
依赖度分析度量了类之间的依赖程度。有如下几种项目:
Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。
Dcy和Dcy:计算了该类直接依赖的类的数量,带表示包括了间接依赖的类。
Dpt和Dpt:计算了直接依赖该类的类的数量,带表示包括了间接依赖的类。
二、针对每次作业的度量分析
第一次作业
-
总体结构分析
在第一次作业中,由于是第一次接触面向对象,其实整个程序在本质上还是用的面向过程的思维方式。正如图中所示,一个Term类主要完成的工作是识别输入的表达式并建立一个存储系数与次数的数组,一个exp类存储的是term数组,由此组织起了整个程序的架构。想法十分自然但是扩展性很是一般。
-
复杂度分析
显而易见的是expression类几乎是严重病态。原因是在第一次作业中将表达式正确性的判断与表达式内容的识别放进了一个方法,这就导致了这个方法极为臃肿,并且在后续的debug过程中也因为方法的逻辑太过复杂吃了很多苦头。
除了上面提到的expression类的问题之外,还存在的问题就是由于把优化过程做进到print方法之中,导致print也极其臃肿。在后面的作业中虽然进行了改进但是效果并不明显。现在想来似乎应该单独写一个类来进行各项的合并以及结果的化简。
-
依赖度分析
首先是模块之间不存在相互依赖的关系,因为这个层次是树形的结构。其次在下面,上层的模块会依赖下层模块提供的服务,因此也是合理的。也正是由于这样,导致整体的可扩展性很差,更改下层模块注定要对上层模块中的接口进行更改,导致整体架构的更改。
第二次作业
-
总体结构分析
第二次作业在架构上与第一次作业并无多大的差别,只是在实现上做的更好。同样的,还是靠直觉的形式从输入的表达式到每一项再到每一个因子建立了树形的结构。为了实现实现的简单化,这里对题目给出的形式进行了一个简化。即每一项化简后只能是一个多项式乘一个正弦函数乘一个余弦函数。这样做的好处是在简化上十分方便,但坏处是几乎不可能实现扩展,只是一种针对特定情况做的优化。
-
复杂度分析
由于采用的架构是类似的,所以可以看到出现问题的地方也是类似的。都是既参与运算又参与输入又参与输出的地方十分复杂。但是由于构造方法时的努力,导致每一个方法都控制了很好的复杂度。
-
依赖度分析
和第一作业相同,首先是模块之间不存在相互依赖的关系,因为这个层次是树形的结构。其次在下面,上层的模块会依赖下层模块提供的服务,因此也是合理的。也正是由于这样,导致整体的可扩展性很差,更改下层模块注定要对上层模块中的接口进行更改,导致整体架构的更改。
第三次作业
-
总体结构分析
可以看到第三次作业的整体架构和前两次作业的架构有了很大的分别。本次作业的想法十分朴素,采用了指导书中推荐的做法,首先对每个函数每种结合法则进行了建类,规定了每一种函数中需要存储的信息。其中用类名保存函数的种类,用类中的具体信息保存函数的具体信息,主要包括系数与次数。然后同理,用类名保存法则的类别,类内保存法则的信息,包括左操作数、右操作数。
由于前两次作业的经验积累,很轻松的我们想到把输入单独作为一个类。但是这里为了实现输入与检测在一起进行,做了一个万劫不复的决定:没有采用正则表达式去匹配每一项,而是采用了类似编译中递归下降子程序法来进行对每一项的匹配。这个荒唐的决定直接导致了后面在测试中测试一连串嵌套表达式的超时。我这里深刻地体会到“不要复造轮子”是什么意思。真的是过于托大了。
-
复杂度分析
可以看到较前两次作业而言这一次作业的表现更好。其中抛开自己犯傻用递归下降写In输入类导致复杂度很高不言,Chain类(嵌套类)复杂度高的原因是一开始做设计时没有想到怎么处理嵌套类的输出,导致后来debug时为嵌套类单独写了一份输出的方法,如果在设计时可以考虑到就可以像其他类一样使用递归的方法,可以降低这个复杂度。
-
依赖度分析
这里出现的循环依赖的问题是因为每个类存储的是一棵子树,从而实现了整个表达式的树形存储。循环依赖是可以预见的,但是并不像对数据进行解释的说明那样,一旦出现循环依赖就会让人觉得疑惑。我认为这里的循环依赖是十分容易进行理解的。这里可以给出一条递归链:Com->Principle->Add->Com
总结
可以看到几次作业中还是在不断进步的,可以看到从一开始的面向过程式的思维到逐渐的开始适应面向对象的方法再到最后实现了类的嵌套与复用之后比较完善的面向对象的编程。但就整个过程而言,虽然每一次作业都考虑到了后面如果添加新的需求怎么办,但是终归还是每一次作业只是在做针对当次作业的优化,导致失去了很多扩展性,直到最后一次作业才稍稍有了点扩展性的眉目。这一点值得在以后的编程中注意。
其次就是复造轮子的问题。最后一次作业中出现的巨大问题就是——舍弃正则表达式而使用递归下降子程序法。以后的作业中一定不要被所学的知识所误导,一定要寻找最优的最简的方法而不要说由于问题很近似于自己曾经解决过的问题,就使用繁杂而熟悉的方法而抛弃建议的做法。
程序Bug分析
在这三次作业出现的Bug中,出现Bug的种类是与架构相关的。
-
第一、二次作业
由于前两次作业使用的是相似的用正则表达式去匹配输入,逐层求导逐层返回,统一写输出函数并在输出函数之中进行优化的方法,在debug中三个部分都未能幸免……(手动笑脸)
-
- 输入
在用正则表达式进行匹配的过程中出现的问题是由于没有想到可能出现的输入格式会是什么样的而导致的WF进行处理和正确格式判别成WF的情况。在对输入进行分析后采用了先处理空格问题,再消除空格进行正则判断。问题得到了一定缓解,但是对某些突然出现的WF问题和多个正负号还需要去思考如何处理。
-
- 处理和输出
逐层求导的问题来源于一开始对BigInteger这个packet的不熟悉。在判断BigInteger是大于0还是小于0(在输出中进行优化)时,很天真的将BigInteger转换成int型然后进行比较大小。
-
第三次作业
-
- 输入
由于前两次作业中需要想错误的正则表达式会是什么样的的折磨,这位先生一怒之下使用了递归下降的处理方法。没想到逃出狼穴入虎穴。这才刚刚庆祝一劳永逸解决了WF和正确格式误报的问题,没想到入了CPU_TIME_LIMIT_EXCEED的坑。想到之前还向别人炫耀自己的程序是多么巧妙,真的是……然后翻看强测的记录才发现几个带有多层嵌套的样例的执行时间都岌岌可危。
-
- 处理和输出
言归正传,除了刚刚提到的输入的问题,还有就是输出时的优先级问题。由于在存储中是以树的形式进行存储的,所以并不需要考虑优先级的问题,子树就是优先级高,但是在输出的过程中却不得不考虑这个问题。因此在一次bug之后为每个因子加上了括号。
-
关于Bug的小总结
- 从输入来看,总体而言,真的是哪里都可能出现bug。其中最严重的问题是我的思维一般都是面向正确的格式进行编程,忽略了遇到错误输入时将要如何处理。
- 从处理的过程和输出的过程来看,是要在使用包的时候注意包的说明,不要理所当然。
- 从下次作业开始要尝试使用自动化测试方法。
Applying Creational Pattern
在软件工程中,创建设计模式是处理对象创建机制的设计模式,试图以适合于该情况的方式创建对象。对象创建的基本形式可能导致设计问题或设计的复杂性增加。创建设计模式通过某种方式控制此对象创建来解决此问题。
创作设计模式由两个主导思想组成。一个是封装有关系统使用哪些具体类的知识。另一个是隐藏如何创建和组合这些具体类的实例。
创建设计模式进一步分为对象创建模式和类创建模式,其中对象创建模式处理对象创建,类创建模式处理类实例化。更详细地说,Object-creational模式将其对象创建的一部分推迟到另一个对象,而Class-creational模式将其对象创建推迟到子类。
五个着名的设计模式是创作模式的一部分
- 抽象工厂模式,它提供用于创建相关或从属对象的接口,而无需指定对象的具体类。
- 构建器模式,它将复杂对象的构造与其表示分开,以便相同的构造过程可以创建不同的表示。
- 工厂方法模式,允许类将实例化推迟到子类。
- 原型模式,它指定使用原型实例创建的对象类型,并通过克隆此原型来创建新对象。
- 单例模式,确保类只有一个实例,并提供对它的全局访问点。【1】
这里在第一二次作业中采用的是单例模式,在第三次作业使用的是工厂方法模式。(其实在第三次作业之前还没有接触过工厂模式,程序也不是严格按照工厂模式的要求来进行的,但是写的程序已经有工厂模式的雏形了)
参考文献
【1】创作模式https://en.wikipedia.org/wiki/Creational_pattern
【2】详解设计模式之工厂模式(简单工厂+工厂方法+抽象工厂)https://www.cnblogs.com/toutou/p/4899388.html