BUAA OO 第一单元总结

综述

本次作业是面向对象课程的开篇作业,主题是表达式对非必要括号的化简,主要考察了对java基本知识的综合应用和面向对象思想的初步系统实践。在这三次作业的迭代中,第一次作业耗时最长,之后的两次,由于之前打下了比较好的基础架构,耗时逐级递减。这次作业总体上讲达到了我的预期,但遗憾的是最后有一些虎头蛇尾。

一、 基于度量的程序结构分析

1. 通过类图看整体架构

本单元作业最终UML类图如下:  

 

1.1 第一次作业

作业要求

读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。

架构分析

本次作业的主要思路为基于“Expr->Term->Factor(Var, Num, Expr)”模式的递归处理。首先通过MainClass读取一个表达式字符串,然后将其移交给Execute类由travelExpr方法进行字符串的遍历处理,该方法主要原理为边遍历边添加项或因子,读到一个因子字符串后将其进行处理。读到满足分割因子条件的”*”时,将该部分因子字符串处理为因子添加至新建项中;读到满足分割项条件的”+/-“时,添加该因子至新建项,并将该新建项进行整合和化简,添加至表达式中,不断反复。

对”+/-“的处理采用boolean型的symbol属性单独处理,将符号全部在因子层面处理,在读到非指数位置的”-“时,symbol取反。化简项乘法中,异号取反。合并同类项时,同号和异号分别处理。

关于化简主要采用归一化的思想,向上层逐级提取。将单个因子的结构归一化为“系数coe+指数index”的形式,并直接反映在上层的Term类中,通过Term类直接读取因子乘积结果。若在运算乘法时出现含有多项式的项时,一个项会不可避免扩展成多个项,在这里我在Term类中添加了ArrayList<Term>数组,存放可能出现的多个项。随后进行合并同类项,顺次轮询Term或Term类中Term数组的元素,匹配Expr类中将index作为key的Hashmap进行添加元素,从而达到化简目的。

在缩短长度方面,本次作业涉及较少,通过正项提前、”x**2”变为”x*x”等小技巧实现。

1.2 第二次作业

作业要求

读入一系列自定义函数的定义以及一个包含简单幂函数、简单三角函数、简单自定义函数调用以及求和函数的表达式,输出恒等变形展开所有括号后的表达式。添加了sin/cos(Var/Num)、[fgh](因子)、sum等结构。

架构分析

由于sin和cos的出现,使得合并同类项的判断出现重大变化,需要保证x的指数、每个三角函数以及其指数均要对应相等才能合并。因此,在第一次作业的基础上新建Index类,内含x的指数index,存储三角函数的Hashmap<String, Integer>,其中key为经过化简处理的三角函数内元素字符串,value为其指数。因子的归一化结构修改为“系数coe+指数Index”,Expr类的key由int修改为Index。由于Index为可变类,因此重写Index类的equals和hashcode方法确保合并同类项时Hashmap匹配正确。

由于出现函数与三角函数嵌套的情况,因此新增嵌套括号的处理,使用num记录待匹配左括号数,在num>0时,所有读入的字符都应该作为提取因子字符串的一部分。

对自定义函数的处理,新增Function类。在输入函数表达式时,使用setFunction方法通过正则表达式提取函数名、形参与表达式于Function类对象中,并将该对象存储至MainClass的FUNCTIONS数组。由于形参顺序不一定为xyz,所以在Function类中添加两个ArrayList数组,通过加入顺序进行形参与实参的映射。在化简表达式遇到自定义函数时,使用getExpr方法将实参与形参建立映射关系,并通过该映射将函数表达式中的形参替换为加括号的实参,得到替换后的表达式,将函数因子转化为表达式因子继续处理。

求和函数的替换策略更加暴力,对求和表达式中不是sin中的i替换为(num),递增替换,将所有替换后结果用加号相连,转化为表达式因子继续处理。

本次作业可以对三角函数平方和/差以及常数三角函数进行化简,但时间限制未能成型。

1.3 第三次作业

作业要求

读入一系列自定义函数的定义以及一个包含幂函数、三角函数、自定义函数调用以及求和函数的表达式,输出恒等变形展开所有括号后的表达式。添加嵌套括号,取消三角函数和自定义函数内参数的类型限制,取消部分嵌套限制。

架构分析

本次作业的关键在合并同类项时,三角函数内元素如果为表达式,如何判断两三角函数相等这一问题。针对这一问题,在MainClass中设置EQUALS数组,记录第一次出现的表达式字符串。运用getStrEquals方法处理表达式字符串,若之后匹配的表达式字符串与EQUALS已存的与其不同的表达式相减得0,则该字符串替换为EQUALS中与其相等的字符串输出,作为三角函数内元素。对相反表达式的处理,设置ELEMENTS数组和getStr方法,原理类似getStrEquals,只不过将减替换为加,且将项符号全为负的表达式替换为经符号处理的项符号全为正的表达式。

由于第二次实验对嵌套括号的处理已经较为成熟,本次作业并未过多在这方面考虑。

格式规范上,sin和cos内元素若是单个因子则不需要加括号,若为多项式则必须加括号。

在缩短长度方面,对三角函数平方和、差进行处理,在Expr类新增simplifySine方法通过数学公式将满足化简条件的项进行删减或改变系数、指数。

  

1.4   UML类图评价

优点:结构相对清晰,层次较明确。

缺点:类的运用不够灵活,某些类肉眼可见的大规模,MainClass中应该只出现主方法,这可能是由于受到面向过程思想的影响。

2. 代码规模

类的属性个数、方法个数、代码规模等如下表:

    

 

从上表可以看出,大部分类的代码量和属性方法分配比较合理,但核心类Term代码量相当高,直逼规定上限,应该对类中的一些计算方法单独提出作为一类。

3. 经典OO度量(复杂度)

方法和类复杂度度量如下:

 

 

 

从上表可以看出,绝大部分方法与类的复杂度合理,但化简方法simplifyTerm与输出方法termString复杂度有一些高。核心字符串处理方法travelExpr复杂度更是相当恐怖。对应类的复杂度分布与方法基本一致。这些复杂的方法无一例外都是在第一次作业中就基本完善的方法,受到面向过程的影响较深,而巨大复杂度的出现也带来了最终没有测到的bug的出现。

二、 分析自己程序出现的bug

先谈谈直接导致扣分的两个bug。

1. 三角函数内加不加括号的问题,即因子判定问题。

出错测试点:sin((sin(x)*cos(x))) -> sin(sin(x)*cos(x))

出错类与方法:Term类ifFactor方法

出错原因:由于在第二次作业中三角函数只能是常数因子或幂函数,因而在第二次作业中只需要正则表达式sin\(.+\)或cos\(.+\)就能精准判定该因子,导致在第三次作业中在写三角函数加括号因子判定时思想出现懈怠,直接采取该正则表达式匹配,导致诸如”x)*cos(x”也被当成三角函数内元素而把表达式误认为因子,导致格式错误。

2. 字符串遍历问题,即+-*有效性判定问题。

出错测试点:sum(i,1,3,(x**2*i))-(+x)

出错类与方法:Execute类travelExpr方法

出错原因:travelExpr方法中对乘号分割因子的有效性设置有效位,而该有效位也会影响+-的读取,只有满足乘号在括号之外时,有效位才能改变。在改变该有效位,忘记判断无待匹配括号这一条件,致使因子错误的提取为sum(i,1,3,(x**2*i))(+x),导致接下来的操作出现格式不匹配的错误,导致Runtime_Error。

 

总结分析:可以看到,出现bug的方法和类,要么代码量庞大、要么复杂度庞大。第一个bug的出现,虽然ifFactor方法的代码量与复杂度并不大,但由于其所在的Term类代码量极其庞大,出现耐心缺损、影响心态而导致考虑不周到等问题。而第2个bug,由于travelExpr代码量和复杂度都相当大,对这一个很小的缺失不能够敏锐的察觉出,维护性很差。由此可见,一个结构清晰、代码量和复杂度合理的架构,可以有效地提高维护性、降低出现bug的概率

 

针对其他在公测和互测前就已修复的bug,除了sin中的i不可替换确实没有想到,其他bug,如符号处理问题等,大多也和Term类和Execute类有关,再一次印证了上述总结观点。

三、 分析自测和互测的测试策略

由于自己能力的限制以及对随机产生样例的概率把控不能够完全理解,自动化测试的性价比和投入产出比不会太高,所以并没有涉及自动化测试,而是采取手动构造测试样例的方式。

首先构造一些基本样例,检验基本的正确性。再针对作业中基本概念的声明部分,对每一个因子的组合以及符号、括号的穿插进行通过树状结构尽可能梳理清楚,并排列样例,重点关注特殊条件与边界条件。针对互测样例,分析规定的cost限制,寻找其中可以做文章的地方(如sum(i,111111111111,111111111112,i)等),在自测样例的基础上构造互测样例。由于手动构造,因而并不能做到特别高的覆盖性,不可避免地遗漏一些边界条件,被扣分的两个bug就是其印证(虽然更多还是轻敌了)。

四、 架构设计体验

俗话说:“万事开头难。”“从无到有”的第一次作业对我来说最煎熬,也最痛苦。使用什么基本方法、使用什么设计架构、如何进行具体实现、如何能够使架构更利于迭代……一系列的问题都有矛盾的想法不断碰撞。思考了一天多的时间,我才真正确定了我的设计理念和架构,并花了一天多去码代码,遇到了一些问题,不过还好顺利解决了,最后的成绩也很好。

第二次作业,新增了一些函数结构,这不可避免地带来的对架构的添加与改变。不过由于之前的问题考虑比较全面,在迭代过程中没有出现重构现象,几乎只是新增了一些新的类和方法,其他部分几乎没有改动,基于完整架构的嵌套的添加也十分容易,也没有出现bug。

第三次作业,只新增了对嵌套结构的处理,这给了我专心去进行化简工作的理由,本周的迭代开发也相当轻松。然而,可能是被这突如其来的轻松与喜悦冲昏了头脑,导致我直接忽略了之前的架构对于第三次作业正确性的考虑,这也导致了bug的出现。认为的不一定是正确的,针对不同条件要不断实践以验证其正确性,不能有一时松懈与过度自信。

五、 心得体会

  1. 在写这篇博客之前,甚至对复杂度没有什么概念。通过博客的分析,发现复杂度与错误率确实有一定关联。在今后的程序开发中,要更多地关注到复杂度这一层面,必要时进行改写重构。
  2. 心态,每一步都要脚踏实地。
  3. 测试很重要,针对新条件一定要全方面开展新测试,以后希望能接触和学习自动化测试。
  4. 通过本单元的学习,对java基本容器方法和设计理念有了更深入的了解,代码的规范性也有了很大的提升,也体会到了面向对象这一思想的奥妙之处。

总结经验,继续奋进。

posted @ 2022-03-24 22:19  伊尔卡米诺  阅读(55)  评论(0编辑  收藏  举报