BUAA_OO_Unit1总结

目录

架构设计

我的总体思路为:读入数据,对输入字符串进行预处理,词法解析与语法解析,将解析得到的结果转化为一般形式的字符串,化简字符串并输出。下文依次给出了三次作业的类图,并解释了每个类的设计考虑;然后,对如何将解析结果转化为字符串进行了解释;之后,对架构的优缺点进行了分析,设想了新迭代场景下该架构的可扩展性;最后以三次作业完成后的最终版本为例,进行了代码的复杂度分析。

UML类图
1. 第一次作业的UML类图:

hw1_UML

  • 设计考虑:在确定了上文提到的总体思路后,只需要想办法设计相应的类来具体实现思路即可。以下列出了各个类(及接口)的职能。
    • Main类:读入数据,作为中介联系并调用其他不同的类,输出结果。
    • Filter类:对输入进行预处理,对得到的一般形式的字符串进行再处理,即化简。
    • Lexer类:词法分析,即,将字符串转化为token流,方便后续处理。
    • Parser类:语法分析,即,对token流进行层次化地分析,建立相应的结构。在本单元的情境中,最终得到的结构为表达式(Expr)。
    • Mono类:Mono指单项式,是第一次作业中可处理的基本单元,满足 coef*x^expo 的形式。
    • Poly类:Poly指多项式,由Mono类的对象组成,具体实现了展开括号时可能进行的加、减、乘、乘方、合并同类项等操作。
    • Token类:定义了token的属性,对外提供了访问token各种属性的方法。
    • Expr类:定义了expr(表达式)的属性,对外提供了toString、toPoly等相关方法。
    • Term类:定义了term(项)的属性,对外提供了toPoly、toString等相关方法。
    • Factor接口:规定了factor(因子)要实现的方法,包括toPoly、toString两个方法。
  • 考虑到表达式也是一种因子,且单独的项也可作为表达式,我让Expr类、Term类均实现了Factor接口。同时,由于常数因子、幂函数因子被包含在Mono类中,且单独的Mono也可理解为Poly,我让Mono类、Poly类也实现了Factor接口。在后续写代码时,由于Poly类也实现了Factor接口,有些地方确实变得更方便了。
2. 第二次作业的UML类图:

hw2_UML

  • 设计考虑:由于使用了递归下降的解析方法,自然而然地可以满足第二次作业中新增的支持嵌套括号的要求。此外,第二次作业还新增了指数函数因子和自定义函数因子,对此,我新增了Unit类、Definer类和Func类,对Parser类、Poly类做了一点修改。当然,对枚举类型Type、Lexer等也做了一点小修改,不再多言。此外,由于第一次作业出了一点bug(在bug分析部分展开来谈),我修改了Term类,将term前的正负号作为属性保存,expr中不再记录项之间的加减号,默认为加。
    • Unit类:作为可处理的基本单元,满足 coef*x^expo*exp(factor) 的形式;和Mono一样,也实现了Factor接口。
    • Definer类:内部包含一个静态属性funcMap,数据类型为Hashmap<String, String>,用来记录函数名到函数定义式的映射对;对外提供了两个静态方法,addfunc()用于添加自定义函数,建立函数名到函数定义式的映射,存入funcMap;callfunc()用于调用自定义函数,参数为函数名和实参列表,以字符串形式返回代入了实参的函数表达式。
    • Func类:Func类的对象为自定义函数因子,所以我让Func也实现了Factor接口。内部通过toExpr()方法,将自身转化为解析后的表达式因子,并将该表达式因子作为属性保存。
    • 对Parser类的主要修改:在parserFactor()方法中,新增了处理指数函数因子和自定义函数因子的分支。
    • 对Poly类的主要修改:由于可处理的基本单元由Mono变为了Unit,所以我将Poly的组成单元也从Mono改为了Unit,并修改了Poly之间的加、减、乘、乘方、合并同类项等操作。Poly之间进行合并同类项的操作时,需要判断两个Unit是否为同类项,为此,我在Unit类中实现了tobeMerge()方法。
3. 第三次作业的UML类图:

hw3_UML

  • 设计考虑:由于在第二次作业中Func类的toExpr()方法是可以递归调用的,针对第三次作业新增的自定义函数递归调用的需求,我不用再额外修改。这样,我只需要满足新增的求导需求。对此,我选择了在Expr(表达式)、Term(项)、Factor(因子)这三个层次新增derive()方法,当调用该方法时,以Expr(表达式)的形式返回求导结果。我还在Parser类的parserFactor()方法中新增了处理求导因子的分支。
从解析结果到字符串

我们在Factor接口规定了toPoly()、toString()方法。语法解析后我们得到一个expr(表达式),调用该expr(表达式)的toPoly()方法,会将不同term(项)调用toPoly()方法后得到的Poly相加,最终得到一个与该expr等价的Poly。而调用Term(项)的toPoly()方法,则会将不同factor(因子)调用toPoly()方法后的得到的Poly相乘,最终得到一个与该term等价的Poly。Factor(因子)的toPoly()方法则需要根据因子的种类分别加以实现,不再赘述。事实上,我们以Poly的形式完成了展开括号所需要进行的加、减、乘、乘方等运算。此后,通过Poly的toString()方法,我们最终得到了一个一般形式的字符串。

架构优缺点分析及在其他迭代场景下的扩展
  • 优点:逻辑清晰,结构清晰,有效降低了不同功能模块之间的耦合度,有利于迭代开发。
  • 缺点:过多递归调用,导致很难追踪复杂数据的处理细节,同时,也导致了一些方法耦合度较高,优化难度提高。
  • 其他迭代场景下的可扩展性:以往届出现过的三角函数为例,为了适应新场景,该架构在具体实现上可能需要增改如下内容:1. Unit需要新增属性,以满足 coef*x^expo1*exp(factor1)*sin(factor2)^expo2*cos(factor3)^expo3 的形式。2. 新增sinFactor、cosFactor类,并在parserFactor中增加相应的处理分支。3. 修改Poly类中的加、减、乘、乘方等方法,以适应新的Unit。4. 修改各个类中的toPoly()方法等。

代码复杂度分析
  • 代码量:
    code_statisic

  • 各项指标:
    class_metrics

m1

m2

m3

  • 从各指标数据可以看出,Filter类、Lexer类、Parser类有较高的圈复杂度,说明其中的分支情况较多,有待优化。此外,通过上述分析可知,各个类均有较高的独立性,基本符合“低耦合、高内聚”的原则。

bug分析

我的bug
  • 第一次作业出现了bug:当’(‘后紧接’+'时,Parser在语法解析时无法正确处理。该问题恰恰出现在圈复杂度较高的Parser类中。关于为何会出现此bug,我做了如下反思:1. 原因:完成第一次作业时,将过多的工作放在了预处理中,由于没有考虑全面,导致漏掉了上述情况;2. 如何避免:可以通过列出所有的可能,一例一例来处理,但这很考验思维的全面性,有较高风险,且不利于迭代;还可以减少预处理的工作量,将更多的工作放在解析过程中。我最终采取了后一种方案,增加了Term类的属性,修改了Parser类中的方法,顺利解决了问题,而且代码变得更加简洁明了了。
  • 第二次作业有一个数据点CPU超时。原因:在判断同类项时,我需要判断exp()括号内的因子是否相等,我采取的办法是将因子转化为字符串,通过判断字符串是否相同来解决,但在这个过程中,我冗余调用了Filter的outFilter()方法(多调用了2次),导致超时。为什么会出现这样的问题呢?我觉得是因为我在具体实现代码前考虑了太多情况,在真正实现代码时会发现完全可以排除这些情况的干扰,但我却忘记了修改代码,依然按照原来的考虑去写代码。如何避免这样的问题呢?将理论与实践结合,不能只想不做,也不能只做不想,要在想的同时做,做的同时想。
发现别人bug的策略

在互测中,可以通过看他人代码来借鉴不同的架构,但很难看全细节,这导致了先研究代码再编数据的测试策略有点低效,所以我采用了直接测数据的测试策略,即:测试自己编造的极端数据 + 测试大量随机生成的数据。有一定效果。

优化分析

我通过在toString()方法中增加判断分支、字符串替换等方法只做了很基本的优化,包括去掉数据0、指数为0时只输出系数或输出1、系数为1时只输出变量部分、尽量保证首项为正、化简了部分可以去掉一层括号的指数函数。因此,以性能为代价,代码的正确性和简洁性得到了一定保障。

心得体会

  • 递归下降好优雅!用清晰的逻辑简单地解决了看起来不太简单的问题,而且只要理清逻辑,不犯细节的错误,答案基本就是正确的。这更说明良好的设计理念是十分重要的,可以让工作者事半功倍。
  • 学会编测试数据真的很重要!人力有穷,即便拥有再高明的设计理念,在具体实现时也难免出现差错。测试数据就是用来纠错的利器。“面向数据编程”才能保证程序的正确性。

未来方向

  • 在课堂上重点介绍递归下降的思想;还可以介绍一下和语法解析相关的AST等概念,并给出一些情景示例。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值