BUAA-OO-Unit1

OO_UNIT1

一、作业分析及面向对象思想

1.1 第一单元作业分析

第一单元的作业整体要求简单来说就是对于一个复杂的形式化表达式的化简。

其中主要的实际要求就是对于表达式的进行括号的去除并且在保证正确性的基础上尽量实现最终结果的简洁性(即最终化简结果尽量在字符长度上更短)。

本单元的作业对我们的要求,我个人认为主要分为了两部分:

  • 对于输入的形式化表达式的解析

  • 如何建立一个合理的可供加减以及化简的因子存储架构

对于第一部分的要求,我个人很大程度上获得了在第一次作业前的实验的指导,学会了应用语法分析词法分析进行递归下降的解析,也是一定程度上学习到了部分编译的知识,因此在第一部分的要求的实现过程中可以说已经少走了很多弯路,并且实践证明递归下降的解析形式化表达式是具有很强的拓展性的。

因此在本单元作业的完成过程中大部分时间以及大部分精力都花在了对于一个更好的表达式架构的建立上,并且我认为也正是因为如此我能说自己在这一单元中在面向对象思想上有了很多提升。

在架构搭建中,我们首先在指导书的形式化表达下可以很轻松的确定这一单元的几个不同层次的简单的类的创建需求:expression,term,factor。当然这只是非常初步并且没有考虑自己项目架构的中形式泛化的情况下做出的最单纯的考量,当然这几个最重要的类在形式泛化后也仍然可以在各处看到他们的身影。

1.2 面向对象思想成长过程

虽然最初接触到这个题目时并没有特别感受到这个题目的面向对象的特性,但是指导书中的对于表达式形式化表达的详细描述其实也是初步带给了我们一部分面向对象的思想(虽然在第一次作业的时候我并没有意识到,并且时不时的吐槽我们似乎还在面向过程编程,事实说明我当时只是并没有理解面向对象)

接下来我就简单说说我在这一单元作业的引导下对于面向对象思想的简单理解过程

1.2.1 我们要“对象”存什么

对于这几次作业的话,在形式化表达的输入下,我们很自然地能想到我们首先大概需要三个大类:expression, term, factor。并且由于三个类的层次结构,我们要在expression中存储term,在term中存储factor,而factor本身是一个庞大的因子,这个因子由之后每次作业细分的幂函数 常数因子 表达式因子等组成,而我们首先要做的就是将这些在每一个对象中存储下来。

其实这就是我们对于对象“属性”的思考,也是面向对象思想最基础的思想之一,其实在最开始我认为这个对象的所谓“属性”与C语言中结构体的成员差不多,当然他们在一定程度上十分相像,但在更深入的理解面向对象后还是能够发现他们是有差别的。

为了讨论上面的问题是就要引入另外一个概念对象的“状态”,这将会在1.2.2中详细说明自己的理解。

每个对象都会有自己的属性,当然在对于不同对象属性的分析过程中,我们就会发现部分对象会有很多相同的属性,此时面向对象的“继承”思想也就正常的顺应而生了,即我们将部分对象进行一定程度的泛化,得到泛化后的新的对象即父类,这个对象中包含他们共同的属性和方法并且能够以父类的角度来解释下面的子类。

1.2.2 我们要“对象”给我们什么

这是我在分析自己的架构时最常问自己的一个问题,也可能是大家在架构分析中很经常想到的问题。

我们在具体实现作业的化简要求时,我们会注意到我们只在每一个对象中存储他所拥有的其他项或者因子是不能够实现我们的需求的,这些的存储也只是一个包含属性的表征,它并不能给我们更多信息,而我们需要的是一个能够帮助我们进行合并化简东西。

这时候我们就能体会到,如果从expression的角度看向他所拥有的term,它其实并不关注term中包含了哪些因子,而是希望term能够给它一个能够用来化简的“东西”,同时term对于外部其实也是并不希望外部看到它内部有什么,而是给在外部有一定需求时,给外部一个能够满足外部需求的“东西”;termfactor之间的关系其实也是如此。

这个时候我们就能隐约感觉到对象的“属性”与结构体的“成员”之间似乎有些不太一样的东西,这其实就帮助我们能够逐渐理解面向对象中对象的“状态”这一概念。对于这里的理解我认为对于我来说是有重大冲击的,也是得益于老师在理论课上一遍遍强调“属性”和“状态”是不一样的,让我对于这一特性有了更多的思考。

所以简单来说对象的“属性”是对象有的东西,但是这些属性并不是对象希望呈现给外部的东西(否则的话其实同结构体的成员似乎就没有什么区别了),而对象想呈现给外部的是对象的“状态”,并且不在必要的情况下直接暴露自己所拥有的最原始的“属性”。我自己总结的理解是:“状态”一般是对象由自身的“属性”在特定的需求下呈现给外部的自身“属性”的某种特征。

而在这一单元的作业要求中,可以先狭义的将这些对象最重要的“属性”定为他们所要直接存储的子成员,但是这些对象要对外呈现的最重要的“状态”其实是它的这些子成员返回的“状态”在经过加减等操作后得到的此对象的“最简表达式状态”。

同时其实这也是我们在第一单元作业中考虑最多的问题,我们要这些对象以什么形式返回他们的“最简表达式状态”。

1.2.3 我们要“对象”干什么

在分析完对象的“属性”和“状态”后,我们首先就很自然能得到下一个问题,我们如何让这些对象将他们的“属性”转化成外部观察的“状态”。这其实就是我们要对象干什么,也就是对象的“方法”。

在我们的单元作业中对象最重要的“方法”就是对于自身的化简,也就是得到“最简表达式状态”的过程。当然对象的“方法”不局限于将对象的“属性”以“状态”的形式展示给外部,还有更多的可能就是对于对象本身“属性”的改变。这也是我认为的对象的“方法”中主要的两部分。

二、面向作业化简及拓展需求的迭代架构设计

2.1 HW1

2.1.1 需求分析

第一次作业中的需求其实并不是很多,在化简这一大的作业要求下,作业中因子只有最简单的常数,幂函数,表达式因子。并且其中的变量只有x。我们需要的就是存储这些因子以及更顶层的对象,并且找到一个可以存储最简表达式状态的容器。

2.1.2 架构分析

首先对于这一次作业的整体层次架构,我采取的就是很朴素的层次机构,Exp中存储Term,Term里面存储Factor,而factor是一个很大的父类,所有的因子以及项都继承自它。当然我么这样看过去这个层次是一个递归的层次,我们就必须要有递归基,否则将会导致程序不可控,而基于形式化的表达,我们可以得到最终的最基础的因子就是数字以及变量x。在本次作业的中其实我并不只有Factor一个大的父类,我考虑到想处理所有的对象(表达式,项,因子)都直接可以以幂函数的形式出现,我额外添加了一个PowerFactor的大的类,这个对象主要存储两个东西,一个是他的底数(表达式,项,因子都可以),另外一个是他的指数,用于之后再化简方法中进行计算。

而在对于输入的解析中则是借鉴了实验中的分析思路使用Parser Lexer两个类进行语法词法分析,并且这个递归下降的思路一直应用到第三次作业。

对于对象“状态”的考量中,我们最终需要的最简表达式的项可以形式化表达为系数 * x ** 指数,由于这一次作业的需求中由于变量只有一个我的思路就简化为不显式存储变量x,而是直接存储指数以及指数所对应的系数,这就是最终所有对象给外部呈现的“状态”,并且我采取了将这个“状态”以HashMap<BigInteger, BigInteger>的形式存储。

而在对于“方法”的考量中,所有的对象最重要的就是一个得到“最简表达式状态”的方法。对于这个方法,作为递归基的常数和变量x可以直接返回它自身对应的HashMap,而真正的计算就在PowerFactor,Term,Exp中进行。而这一化简计算的方法由于大家都公共就直接放置在了父类Factor中。

2.1.3 作业UML图

2.1.4 度量分析与架构整体回顾

类复杂度分析:

度量参数说明:

OCavg:计算每个类中所有非抽象方法的平均圈复杂度。 继承的方法不计算在内。

OCmax:计算每个类中非抽象方法的最大圈复杂度。 继承的方法不计算在内。

WMC: 计算每个类中方法的总圈复杂度。

可以看到在Parser Lexer Exp中控制复杂度有点高,这是因为在这几个类中,我大量使用了if控制语句并且具体的实现全部在if语句内直接展开,在Parser 与 Lexer中主要是将所有的解析情况基本只通过一个主函数来实现了,而在Exp中则主要是在print的时候为了进行优化叶进行了大量的判断并且都直接在判断语句中展开实现了。

说实话之前并没有过多的关注这一方面,现在看到分析结果细想之后确实,对于情况较多的实现过程,尽量应该把每一个细节的实现作为单独的函数独立出来,这样既有利于单元检测的debug和之后的程序维护也有利于进一步的进行形式泛化可以达到更好的复用性。

方法复杂度分析:

参数说明:

CogC:认知复杂性,随着每个控制结构的使用而增加,而且嵌套控制结构越多,认知复杂性就越高。

ev(G):本质复杂性是一种图论度量方法控制流的结构不良程度的方法。

iv(G):计算方法的设计复杂度。

v(G):计算每个非抽象方法的圈复杂度。 圈复杂度是对通过每个方法的不同执行路径数量的度量。

从这个图的分析中可以看到仍然主要是在Parser Lexer Exp中处理部分的复杂度较高,其余函数中的复杂度以及耦合程度还可以。

虽然在复杂度的分析上我还觉得自己的架构在复杂度上可以接受,但是在实现的具体细节上有很多值得优化的地方以及十分不完美的地方,这也导致了再向后迭代开发时其实对于架构做了很多的改动。(除了语法分析和词法分析基本没动)

  1. 首先是变量,我在第一次作业中默认的变量是x,这就导致我的变量不能够拓展,在之后出现y,z,i时需要重新添加新的对象。
  2. 在对于最简表达式状态的存储上,我也直接简单粗暴的采用了指数与系数的存储形式,这导致在之后出现三角函数时需要做出极大的调整

2.2 HW2&&HW3

2.2.1 需求分析

我直接将两次作业放在一起进行说明,因为两次作业其实差别不是特别大,只要在第二次作业的实现中考虑到了对于递归嵌套的使用,第三次作业的修改量其实几乎没有(只要不做优化)

这两次作业在第一次作业基础上需求有了较大的增量

  1. 在这次作业中首先加入了sin cos函数的加入并且这两个函数的加入意味着在最简表达式中多了一个三角函数的影子。
  2. 除此之外作业中加入了自定义函数f(),g(),h(),并且其中的变量增加了y,z
  3. 增加了sum()函数,并且变量中增加了i

这几个新增的要求其实基本上直接否认了我的第一次对于“最简表达式状态”的存储形式,因为最简的项中可能有三角函数,在最初可能有多种变量,因此我当时一度要放弃自己第一次作业采用HashMap的思维了。但是好在及时同yyh同学交流,并且在研讨课上听取了其他同学的思路,尤其是yyh同学的思路直接将第一次作业的HashMap救了回来。

2.2.2 架构分析

基本思路为:

层次架构上:sin cos var num function sum exp 均是Factor的子类。

在对于“最简表达式状态”的存储上仍然存储为HashMap 不过存储的键值改变,新的存储方式为:HashMap<HashMap<Factor, BigInteger>, BigInteger>具体来说就是仍然使用最外层的HashMap的键作为最简因子的存储,值存储系数,而对于内部的HashMap<Factor, BigInteger>它是用来存最简的项的每一个最简的因子以及它的指数,例如x*cos(x)*sin(x**2)对应的HashMap<Factor, BigInteger>Factor存的就是变量x以及两个三角函数而他们的键对应的值均为系数1

在“方法”上,此次对于Function类并没有给它设置update方法而是在每一次读到自定义函数时直接将自定义函数转换成一个Exp返回,即在实际最终获得的表达式树中不会有Function类存在,在对应的叶结点处实际上是一个Exp。而在方法中的具体细节中有一个十分重要的并且实现的可拓展性极强的地方是在对于Function 以及 Sum的更新操作中采用了直接将对应的形参替换为对应的因子从而形成新的表达式,这样就可以不用再继续考虑这两个因子的update操作,只需要让他们能生成表达式即可,若之后出现更多的变量也是可以直接支持的。

2.2.3 作业UML图

2.2.4 度量分析与架构整体回顾

类复杂度分析:

最终分析结果其实同第一次作业差不多,主要的复杂都集中在Exp Lexer Parser

方法复杂度分析:

分析结果同样像之前一样,主要的复杂度均集中在Exp Lexer Parser中,这也正说明我在之后的代码的编写架构中需要着重注意自己代码的复杂程度以及可维护程度。

三、bug分析

3.1 自己几次作业bug分析

第一次作业没有出现bug,自己本地也测试了快十万个数据没有找到bug,确实是第一次实现十分专一,实现与作业要求耦合极高,拓展性较小,因此出现bug的情况也会少很多。

第二次作业出现的bug其实是clone的重写出现了错误:具体细节是我这几次在实现过程中对于符号的处理是将所有的符号最终当作下一个将要分析的因子的属性。但是我在clone的时候没有复制符号这个“属性”,因此导致了在对于Sum Function等的替换过程中出现了由于符号的问题导致的结果错误。

其实除了这个bug的出现,在第二次作业中折磨我最久的是重写equals 和 Hashcode方法,其实本来直接使用IDEA生成的重写方法并不会出错,不过我错在先在Factor这个父类(且是虚类)中重写了equals 和 Hashcode方法,导致在子类中重写时会在自己的Hashcode方法中再次调用HashCode方法。这样就会导致在比较时陷入了死循环中。

第三次作业的bug其实在很多地方出了错,共两个bug,均发生在自己对于三角函数进行优化时出现的(庆幸强测放我一马),这两个bug中有一个是互测时被别人hack到才发现的,另外一个是在互测时自己打算hack别人是突然测到自己的bug。自己测到的bug是自己在将所有系数为负的三角函数平方项化简时忘记更新最前面的常数项。第二个bug是在优化时对于一个HashMap进行更改后没有及时break导致的。但其实最好的方法还是不要在这种地方冒险而是直接复制一份对于新的这一份进行操作。

对于这几次被测出来的bug以及自己在写代码时出现的bug,自己的总结主要如下:

  1. 对于需要遍历修改的变量一定要先复制一份,这就要求一定要写好clone函数。
  2. 对于equalsHashCode重写时一定要保持两个方法的判断结果相等,最好就直接使用IDEA自己直接生成的重写方法。

3.2 hack他人的bug

在hack他人的bug中最主要的bug其实都是因为指导书没有认真阅读,出现了0**0 = 0; 以及 sum(i,3,-1,x) 不输出0 以及sum爆int的情况。所以完成作业任务时一定要着重注意一些特殊的边界条件或者不确定的情况下的说明。

3.3 测试心得

不论是在对于自己的还是对于别人的程序,在测试时要着重注意边界情况的测试。而对于类似本次作业的优化的情况可以针对优化的程序进行单独的强度测试。

四、收获与体会

在我看来,我最大的收获其实就是初步窥见面向对象思想的冰山一角,从思想上在面向对象这条路上入了门。

当然这一单元也是颇有遗憾,尤其是在第二三次作业中由于自己时间规划十分不合理,导致留给自己在架构上的思考空间少了很多,在架构上基本上是吸收了别人的思想,不像第一次作业中在与他人讨论的基础上自己独立思考最终得到自己满意且写起来十分顺畅的项目。

当然最后还是得感谢这一单元中帮助过我的每一个同学,在作业中因为问题讨论确实占用了他们太多时间,也感谢他们的热心帮助。当然也要感谢在面向对象思想上狠狠启发了我的荣文戈老师。

posted @ 2022-03-26 05:11  SKiToul  阅读(67)  评论(1编辑  收藏  举报