BUAA OO unit1 2024 博客

励思媛-22371445 学生 2024-03-21 19:42:08

OO unit1

基于度量的程序结构分析

hw_1

代码量分析
屏幕截图 2024-03-21 100243

第一次作业任务量较小,因此源代码只有410行,主要集中在Simplify、Parser、Poly类,对应输入化简类、字符串解析类、多项式运算输出类这几个主要处理阶段。

UML类图
hw1
  • MainClass:函数主入口,调用Simplify、Parser、Expr、Poly各类
  • Simplify:对输入字符串进行预处理,包括去除前导零、连续正负号、空格,统一化正负号以便后续把字符串解析为Expr(表达树)
  • Lexer:词法分析器,通过next()得到下一个curToken
  • Parser:语法分析器,联合Lexer把字符串解析为Expr(表达树)
  • Expr:有Arraylist,表示多个Term相加,有toPoly()方法。实现Factor接口
  • Term:有Arraylist,表示多个Factor相乘,有toPoly()方法
  • Factor:接口,统一管理Expr、Variable、Number
  • Variable:实现Factor接口,表示x ^ power。有toMono方法
  • Number:实现Factor接口,表示带符号的数字。有toMono方法
  • Poly:多项式,有Arraylist,表示多个Mono相加。有Poly加、乘、合并同类项(化简)、toString方法
  • Mono:单项式,形为coe * x ^ powerX,有Mono乘、toString方法

类设计的优点:Simplify的预处理使我在Parser解析字符串时考虑的情况大幅减少。我采用递归下降的方式对输入字符串进行解析,形成表达式树,层次划分明确。Expr再转为Poly,将加、乘、化简移入Poly类中完成,最后由Poly输出结果字符串。各类各司其职,耦合度较低,基本符合单一职责原理。

类设计的缺点:没有很好利用Factor来统一管理各因子,Expr中是toPoly()方法,但在Variable、Number中是toMono方法,各因子行为不统一。在Term中使用大量if(... instanceof ...),没有完全发挥出接口的优势,也为hw_2的局部重构埋下伏笔。

复杂度分析

类复杂度

屏幕截图 2024-03-21 100626

第一次作业逻辑并不复杂,并且经过重构后我的类层次性划分较好,因此平均操作复杂度(OCavg)、最大操作复杂度(OCmax)、加权方法复杂度(WMC)均较低,反应出第一次作业代码判断逻辑较简单,可维护性和测试性高,代码质量较好。

部分方法复杂度

屏幕截图 2024-03-21 102655

截取了方法复杂度较高的几类方法,Simplify预处理加减号的方法中,我枚举了可能遇到的大量情况,因此认知复杂度(CogC)、设计复杂度(iv(G))、圈复杂度(v(G))均较高。Mono的toString方法复杂度同样较高,因为我在打印输出coe * x ^ power时为追求性能,枚举了大量情况。

经验感悟:大量的if-else或switch-case并不符合面向对象的设计思维,在之后的设计中要尽量规避。

hw_2

代码量分析
屏幕截图 2024-03-21 105859

第二次作业加入了自定义函数、指数函数因子、多层括号嵌套,因此代码量急剧增加。Poly和Mono类负责运算、化简、判断相等、toString等等,代码量较大。Parser也因为输入情况的不断增加,代码量也有显著增长。

UML类图
hw2
  • AllFunc:有Arraylist,存储所有自定义函数,采用单例模式,确保有且只有一个实例,自行实例化并向整个系统提供这个的实例,外界通过getInstance调用。可复制其中的自定义函数并返回该函数
  • Func:自定义函数,包含函数名、函数表达式(Expr)、形参、实参(Factor),能在表达式层面用实参(Factor)代换形参(Variable)
  • Factor:有replaceFormArgu()、toPoly()、deepCopy()抽象方法,统一管理各因子
  • Expo:指数函数,继承Factor接口,形为exp(Factor) ^ power。有toPoly()方法
  • Mono:从coe * x ^ powerX改为coe * x ^ powerX * exp(expoPoly),expoPoly也为Poly类,是指数函数中的Factor转换而来的多项式。为合并同类项,增加equals()方法判断相等
  • Poly:为合并同类项,增加equals()方法判断相等

类设计的优点

  1. AllFunc采用单例模式,外界通过getInstance得到唯一的实例,并向AllFunc添加自定义函数、深拷贝自定义函数。统一管理自定义函数,并深拷贝得到函数副本,以达到解析表达式中函数的目的。
  2. 在函数实参代换形参过程中,在表达式层面进行代换,而非字符串层面。用实参Factor代换函数表达式中的形参Variable因子,有效防止了误处理exp中的‘x’等问题,并在已有的parseExpr()方法上解析函数定义表达式,无需再新写一个递归下降方法来解析f(f(1,1), 1)这种函数嵌套情况的实参提取问题。且可扩展性高,若加入新的形参形式,同样可以用这种方式处理,在新迭代情景上的可延展性好。
  3. 通过局部重构,实现Factor统一管理各类因子,各因子各自实现Factor要求的replaceFormArgu()、toPoly()、deepCopy()函数,去除了Term中冗余的if(... instanceof ...),由Factor统一调用各方法。

类设计的缺点:仍旧没有解决部分类的大量switch-case模式,如Mono的toString()、Parser的parseFactor()、Simplify的预处理方法等等,这些部分很容易出现bug。

复杂度分析

类复杂度

屏幕截图 2024-03-21 105959

此次的增量开发要求较为复杂,Poly类增加了许多方法,且很多方法相互调用,如combineSamePower(合并同类项)方法和equals方法的相互调用,导致Poly类的平均操作复杂度(OCavg)、加权方法复杂度(WMC)较高。Parser由于增添了解析指数函数因子和自定义函数的功能,平均操作复杂度(OCavg)也有上升。

部分方法复杂度

屏幕截图 2024-03-21 105932

此次迭代中,Parser的parseFactor的方法新增了对指数函数因子的判断解析,同时在解析自定义函数时,需要调用AllFunc的newFunc()函数副本拷贝方法、Func的toExpr()实参代换形参方法,因此认知复杂度(CogC)、基本圈复杂度(ev(G))、设计复杂度(iv(G))、圈复杂度(v(G))都有显著上升。Poly的equals()方法涉及与Mono的equals()方法相互调用,基本圈复杂度(ev(G))、圈复杂度(v(G))也较高。

经验感悟:各类相互调用方法会大幅提升方法复杂度,导致各类、各方法相互依赖,给代码测试和维护带来巨大困难。

hw_3

代码量分析
屏幕截图 2024-03-21 160237

第三次迭代相对第二次而言增加的功能较简单,新增了求导因子,并允许自定义函数的嵌套使用。因此新增了Deri类,代码总体增量不多。

UML类图
hw3
  • Deri:求导因子,继承Factor接口,形为dx(deriExpr)表达式求导,由toPoly()方法。该toPoly()方法调用Poly的derivation()方法
  • Poly:新增derivation()方法,对各项Mono进行求导
  • Mono:新增derivation()方法,对形为coe * x ^ powerX * exp(expoPoly)的Mono进行求导

类设计的优点:在Poly层面进行求导,而非在Expr层面进行求导,Poly已经完成了函数展开、括号展开等,因此Poly的各项Mono比Expr的各项Term更简洁,也更容易求导。

类设计的缺点:Poly的derivation()方法和Mono的derivation()方法相互调用,会提升方法复杂度,使程序调试难度上升。且Poly又新增功能,使原本就复杂的Poly方法更多、更复杂,可能造成bug富集在Poly类中。

复杂度分析

类复杂度

屏幕截图 2024-03-21 160314

由于Poly承担了运算、化简、求导、输出等多种任务,Poly的复杂度较高。

部分方法复杂度

屏幕截图 2024-03-21 160304

与hw_2的方法复杂度基本持平。Mono新增的derivation()求导方法,由于Mono需分类讨论coe * x ^ powerXcoe * exp(expoPoly)coe * x ^ powerX * exp(expoPoly)等形式,基本圈复杂度(ev(G))较高

经验感悟:Poly作为主要处理类,应该进行一定的功能外调,而不是把多种功能都集中在Poly中。

架构设计体验

hw_1

很不幸,在hw_1完成过程中我就进行一次大的重构。重构前我试图在Expr层面就进行括号展开,但无法处理形如(1 + 2) * (3 + 4)(x + 1) ^ 2等多个括号连乘的形式。重构后我引入了Mono类和Poly类,在这两类中进行运算、括号展开、合并同类项的优化和输出,奠定了本次作业的基本框架。

hw_1主要架构设计如下:

  1. 解析字符串,并生成表达式树。解析字符串利用递归下降方法,根据指导书中的内容,通过Lexer词法分析器提取不同类型的字符串。再交由Parser语法分析器层次化解析Expr、Term、Factor,最后形成一棵表达式树。把横向一维的字符串拉伸为纵向二维的表达式树,层次分明。
  2. 表达式树转多项式,并输出字符串。通过Expr、Term、Factor各层级的toPoly()方法,把表达式树转为多项式,并在多项式中展开括号,进行加法、乘法运算,合并同类项进行简易的化简,最后输出成字符串。

hw_2

在hw_1较为完善优秀的架构上,hw_2的迭代思路较为明晰,只是对Factor进行了局部重构,使Factor能统一管理各因子,并调用各因子的具体方法。整体架构依旧延续hw_1。

hw_2新增架构如下:

  1. 多层括号处理。由于在Poly层面进行括号展开,hw_1架构天然支持括号嵌套,因此无需修改。
  2. 指数函数因子。在parseFactor()方法中新增对指数函数的判断,新增Expo类继承Factor接口。修改Mono属性以支持指数函数。
  3. 自定义函数。在parseFactor()方法中新增对自定义函数的判断,同时增加对y、z变量的识别,使Parser也能解析函数定义表达式。新增Func类、AllFunc类,处理实参代换形参,把自定义函数解析为表达式,作为原表达式树中的一个表达式因子。
  4. Factor的局部重构。Factor要求实现replaceFormArgu()、toPoly()、deepCopy()函数,各因子重写这些方法。去除了Term中冗余的if(... instanceof ...),由Factor统一调用各因子的具体方法。

hw_3

hw_3新增功能较少,因此本次只是在hw_2架构上新增功能,并未进行重构。

hw_3新增架构如下:

  1. 函数嵌套调用。hw_2架构天然支持函数表达式调用其他已定义的函数,无需修改。
  2. 求导算子。在parseFactor()方法中新增对求导因子的判断。新增Deri类继承Factor接口。Poly和Mono类新增derivation()求导方法。

新的迭代情景

若新增三角函数因子,我会修改parseFactor()方法新增对三角函数因子的判断,新增三角函数类继承Factor因子,修改Mono属性以支持三角函数。同时,Mono和Poly类也要新增三角函数运算方法、化简方法、输出方法。

若新增求积分算子,同样先在parseFactor()方法中新增对积分因子的判断。新增积分类继承Factor接口。可以在Expr层面对表达式求积分,也可以在Poly层面对表达式求积分。

自己程序的bug

hw_1和hw_3的bug数量较少,主要集中在hw_2中。hw_2公测数据中,(((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8这组数据因为括号的深层嵌套,我出现了CPU_TIME_LIMIT_EXCEED问题。这是因为在处理表达式次幂问题中,我采用了每次都暴力解析的方法,导致运行时间过长。修改后,我只解析表达式一次,然后根据次幂把表达式深拷贝数次,解决了该问题。

这个bug出现在Term的toPoly方法中,这个方法圈复杂度较高,因此我们在编写程序过程中,应该尽量降低圈复杂度,可以有效防止一些潜在bug。

我因为充分的本地测试而没有被hack,在本地测试中,我发现的主要bug都由深浅拷贝引起。由于表达式树的复杂嵌套,对表达式进行拷贝时未深度拷贝,各表达式因子相互关联,修改某一表达式引发所有关联表达式的改变,导致错误。

以后应注意深浅拷贝问题,确保不同对象没有任何关联。

hack别人程序bug的策略

我主要采用评测机和手捏数据结合的方式进行hack,在hw_2成功hack到别人。评测机提供随机数据,手动构造提供边界数据、特殊数据。如exp(exp(dx(x ^ 2 + 3))) * dx(exp((3 * x ^ 2)))dx(f(x))等复杂形式的嵌套。

还有同学在预处理字符串中未分类详尽,可能会造成格式上的处理错误,因此编造格式上的特殊字符串也是思路之一。

除此之外,我还浏览了同房间同学的代码。不少同学的架构都与我自己的架构有很大差异,如有些同学没有引入Poly、Mono类,而是在Expr中执行大量操作。因此我针对代码中类复杂度、方法复杂度高的类和方法,编写了一部分数据来hack。

代码优化

为提高程序性能,我主要通过合并同类项、优化toString()方法来缩短输出字符串。合并同类项设计Poly的判断相等,因此为Poly增加了equals()方法。Mono的toString()方法中我详细判断了系数为0,x幂次为1,expoPoly为因子、exp无需两层括号等情况,缩短了输出的字符串长度。

每处代码优化后我都会在本地进行充分测试,以保证优化后代码的正确性,再进行下一处的优化。同时对toString()这种大量分类讨论的方法,我通过写批注来保证代码可读性,通过封装麻烦的判断语句来保证代码简洁性,而不是一昧堆砌在if条件判断中。

心得体会

OO作业任务量很大,迭代更是有可能出现意料不到的错误,虽然过程很痛苦,但完成后还是得到了很多经验。

首先,仔细阅读指导书后,先形成大致的架构设计,会对编写代码有很大帮助。不论是积极与同学讨论想法架构,还是参考往年博客的架构设计,形成一个好的架构和清晰的思路能让写程序事半功倍。

其次,要充分利用java语言的特性,如接口、父类等等,从原本c语言的面向过程编程思维转换到面向对象编程思维。

最后,一定要充分进行本地测试,评测机真的很重要!

未来方向

hw_1是让我觉得最吃力的,当时我对类的划分和架构设计是很迷茫的。如果能在hw_1的指导书中加入架构设计的启发,或许能让我当时有些初步的思路。

...全文
79 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧