BUAA_OO_2022 第一单元总结

BUAA_OO_2022 第一单元总结

第一单元作业主要围绕表达式的解析展开,第一次作业实现简单的多项式展开,第二次作业新增三角函数、自定义函数、求和函数,第三次作业减少了对三角函数、自定义函数的限制。整体来说,实现的大体思路为构建表达式树,选择合适的容器存储各类因子,之后进行去括号、化简等工作。

一、三次作业架构分析

1.1 hw1

1.1.1 hw1 架构

由于对于表达式解析一头雾水,又害怕在ddl前无法完成作业,因此本次作业我采用了预解析模式。

对于最基础的因子划分为两种:

  • Expr :由多个因子Var组成,默认通过正号连接,以HashMap为容器,便于合并同类项,原理比较简单,此处不多赘述。

  • Var : 包括常数和单一的变量x

Expr、Var实现了Factor接口,重写了addOrSub、mult等方法用于计算。

由于使用的是预解析模式,对于输入表达式的计算顺序可以说是已知的,不需要管多余的括号。因此只需要调用Compute类中的getOperate()方法,并记录计算结果。最后取出最后一次计算的计算结果并调用重写后的toString()方法(包含了优化)。

1.1.2 复杂度分析

可以看出Compute、Expr类的复杂度较高。这是由于Compute对表达式计算进行了总体的控制,而Expr中需要实现加法、乘法等计算,并需要进行表达式化简。

可以发现复杂度较高的方法集中在Expr类。

在toString中化简表达式我并没有使用正则表达式,而是使用了大量的if .. else .. ,这导致了复杂度的上升。

在addOrSub中,我则是利用循环来实现了两个HashMap的合并来模拟加减法。

1.1.3 架构优缺点分析

对于本次作业,我其实完成的并不好,可以说可扩展性几乎为0,因此这也导致了hw2的重构(哭)

1.2 hw2

1.2.1 hw2 架构

在hw2中我进行了重构,采取了递归下降来构造表达式树。

对于表达式解析部分,使用Lexer、Parser、PreProcess几种类进行分析:

  • PreProcess :去掉待开表达式、函数表达式中多余正负号(只保留一个)、空格,对于待展开表达式,使用SumFunc、DefineFunc中的方法进行字符串替换来对求和函数、自定义函数展开。

  • Lexer :将待展开表达式按指定意义划分后供Parser类使用。

  • Parser:递归下降构造表达式树。

根据题目形式化表达,我将因子划分为Expr、Constant、Term、Var几种类型:

  • Expr :表达式因子,使用ArraList容器保存Term。

  • Term :由因子以乘号连接构成,带有正负号,用ArrayList容器保存各种因子

  • Constant :常数因子

  • Var :普通变量因子和三角函数因子

对于这四种因子均实现Factor接口,并重写toelement()方法,在解析完毕后,调用表达式树根节点因子的toelement()方法,将表达是转化为形如:

\[\sum{a*x^b\prod{sin(...)cos(...)}} \]

使用以HashMap<String,Integer>为key的HashMap存储,并调用Compute类中的方法进行计算。

1.2.2 复杂度分析

从类复杂度和方法复杂度分析中可以看出,关于函数的解析处理、待展开表达式的处理以及表达式的计算部分其复杂度较高。这主要是由于设计问题,在这些类或方法承担的工作相对较多较复杂,使得其中包含了大量if..else..判断和for循环。

但其实很多是可以优化的,就比如上图为ParserFacotr()方法中的一部分,两个关联的else if 实则是可以合并的。

1.2.3 架构优缺点分析

关于构造表达式树部分,采用了PreProcess类让表达式更加简单,很有效的减少了Parser解析时的难度。

但是对于求和函数、自定义函数采取字符串替换其实并不是明智之举,因为字符串暴力替换毫无疑问会因括号的层数增加而增加表达式树的深度,而对此进行优化,也毫无疑问将面临很多很多细节,无形中增大了bug产生的风险。而为避免这些问题,个人认为将函数表达式作为待展开表达式解析,在调用时进行深克隆,可能会更容易维护、扩展,运行效率也将更高。

并且表达式计算时,将因子转化为单独的element类也是一个值得优化的点,这样做即使在未来因扩展而改变装载element的容器,至少也可以使mult、add等方法的参数类型不必改变,增强了可扩展性。

对于Compute、DefineFunc、SumFunc中使用了大量的静态方法的问题,将在hw3中进行分析。

1.3 hw3

1.3.1 hw3 架构

对于hw3的扩展,相对比较简单。

  • 为实现函数调用的嵌套,在DefineFunc类中新增getParameters(),modifyPara()方法,递归解析参数的形式。

  • 为实现三角函数内部的任意因子嵌套,在Var类中增加processInnerExpr(),将三角函数括号内的部分作为表达式进行解析。

1.3.2 复杂度分析


(此处仅展示复杂度明显增加的部分)

与hw2类复杂度、方法复杂度相比,可以明显发现新增的几个方法的复杂度均比较大。

尤其时对于Var的构造方法,在保留hw2解析内部表达式的同时,增添了对processInnerExpr()的调用,而实际上二者功能相似,这样设计并不符合“高内聚,低耦合”的设计思路。

1.3.3 架构优缺点分析

可以发现在hw2、hw3中我使用了大量的静态方法,起初目的是想要模仿Arrays工具类,减少对象的创建。但实际上,个人认为这样做的益处并不大,静态方法先于对象的创建,因此静态方法无法调用普通成员方法、变量,同时静态方法随类的加载而加载,生命周期长,占用资源多。

二、Hack策略以及bug分析

2.1 hack策略

  1. 写评测机自动化测试

    大致思路借鉴了讨论区的分享(在此也感谢讨论区各位大佬的无私分享)

    数据生成部分:

    利用随机数生成调用函数,递归生成数据

    sumPool = ["sin(i)","i*x**i"....]
    ....
    
    def getWhiteSpace():
        ...
    def getAngle():
        ...
    def getConstant():
        ...
    def getExpr():
        ...
    def getTerm():
        ...
    def getSum():
        ...
        ....
    

    正确性判断部分:

    TestPath = "java -jar D:/TestMachine/hw3/...jar"
    for data in testData:
        thread = subprocess.Popen(TestPath, stdin = subprocess.PIPE, stdout = subprocess.PIPE)
        ....
        stdout = stdExpr.evalf(subs={x: 0})
        myout = myExpr.evalf(subs={x: 0})
        ... #带入特值点进行比较判断
    
  2. 构造边界数据

​ 构造数据来测试边界极端情况,如:sum(i,2147483649,2147483649,i)、sin(0**0)**0、(((((((((((((((((((((x+2)**2)))))))))))))))))))))...

值得一提的是,个人认为好的代码风格确实可以减少我们被hack的几率,因为好的代码风格可以让程序思路更清晰,从而在根本上减少bug的出现。同时优雅的代码也会让其他人赏心悦目,减少其他人想要hack的欲望。比如我在一次作业中发现有些同学用拼音命名类名(Unbengable),让我忍不住想多测一测这位同学的程序。

2.2 bug 分析

本人被hack到的bug:

  • hw2 : 没有考虑到sin(0)**0的情况
  • hw3 :形如sum(i,1,1,sin(i**i))的情况会出错,在三角函数的括号内出现(i**i),处理后变成(1**1),程序会误以为括号内是个常数,而抛出异常。

个人感觉在这几次作业中,尤其是hw2、hw3中,多数人的bug主要问题是出在输出表达式优化的问题上,解析表达式部分出的错相对较少。

三、心得感想

  1. 有时候只停留在思考阶段是不行的,有时候只有亲自动手,写一写,尝试尝试才能发现没有考虑到的问题或者是寻找到更好的思路。

  2. 理性看待“重构”这件事,不要为“重构”而感到崩溃,每一次重构都是积累经验。(在个人能力范围内,追求实现优秀的架构也是必须的)

  3. 独自一人闭门造车不可取,要多与同学、助教、老师交流,在不同思维的碰撞下,才可能有更优异的设计诞生。

  4. 优秀的设计固然重要,但程序的正确性始终离不开测试这个环节。在未来的作业中要实践不同的测试方式、积累测试经验。同时个人感觉程序的测试,不一定要写完全部程序再进行,分层次、分模块进行测试或许更容易找出隐藏的bug,就比如在第二次作业中,我在完成了预处理、表达式解析后都分别进行了测试。

  5. 面向对象是一种很重要的思想,个人认为其思维的核心在于抽象和层次化。而经历三次作业的洗礼,个人感觉只是入了门,或者只是堪堪了解了这种思想,未来还要积累更多的经验、学习并应用其他优秀的设计模式。

posted @ 2022-03-25 22:42  997ddler  阅读(98)  评论(1编辑  收藏  举报