北航2022OO第一单元博客作业

总述

第一单元任务概括起来是处理表达式。表达式是有层次的,总体需要递归地解决,但落实到每一层,终究需要小心翼翼地扫描和处理字符。我在最初设计时,除了最基本的设计以外,还考虑了以下几个问题并作出了初步规划:

1.是否可能出现除法或取模等运算?我认为除法出现概率不大。因为本单元OO作业似乎意在使我们建立起层次化设计的思想,但在目前的预料里,加入函数代值、求导等已足够拉满细节,若再加入除有关运算,可能会带偏重点。此外,评测也可能更为棘手。

2.是否可能出现小数?如果认为不会出现除运算,则不考虑小数。因此小数未纳入考量。

3.从总体角度看,一个表达式的组成可以有各种理解。我选择将其视作“多个由加号连接的单项”的“多项”。而“单项”是最小的单元。由此,符号成为“单项”的属性之一。

4.从局部看,那些最小的因子,是否可能在后续作业里变为复杂的表达式?我认为可能,所以我除了指数以外,其余因子均按“多项”存储。指数看起来不太会用来为难大家,即使指数未来也成长为复杂的表达式,做法也类似。

在三次作业后回顾,有预料之中的挑战,有大费周章的改动,更多的是实实在在积累的经验。

各种问题

字符串处理

在处理字符串方面,相比 C 语言,Java 可谓相当强大了。武器库虽大,但是落到处理每个字符的逻辑上难免出问题。

正则表达式是字符串处理的利器。比如本次作业可以通过正则表达式去除所有多余的符号,像“+-”和“-+”换为“-”等。但难免有同学思考不周到,导致使用正则表达式匹配较长或较复杂式子时翻车。而这若没有课下大量或强力数据的考验,不容易发现。

还存在替换字符串内容不严谨的现象。比如处理求和函数仅简单地将 i 代换为数字,却未考虑 sin 的感受?

这类问题在于考虑不周到。

“分工”不明确

根据不同需求,设计相应的结构和方法,然后进行“分工”,使得处理问题时各个环节干净利落。但是分工不仅是简单地设置几个类便万事大吉。有的类本身任务繁重,是否可以考虑为它分出几个“身份”,使得各行其事?。就我而言,我一度单个类超过500行,思考后将用于判断的方法全部合并在“判断”类中,将频繁使用的常数存在另一类中,它们供全局调用。如此一来,避免出现巨长的类,避免混淆、方便浏览。

这类问题,有可能是因为写代码时草率,也可能是因为对迭代未作出正确预估。

对Java不够熟悉

比较常见的如深拷贝、Hashmap等问题。这类问题是由于经验不足。

忽略了题目未提及的内容

题目并未给出所有的规定,不知是不是有意为之。比如 sum 的上下界使用 int 是否足够、自定义函数参数列表是否一定按 x、y、z 顺序出现等。这类问题只要留意,多数能够想到。

没有仔细研究题目的相关描述

比如哪些括号是必须的、某某表达式是否合法,其实绝大多数答案都在题目描述中。这是读题和审题不周的问题。

不能自行测试

有的同学不会生成层次化的数据,有的同学不会写能判断正误的程序,有的同学有惰性……种种原因导致不少同学只能肉眼debug或者随手造数据进行测试。

自我分析

概览

主要类的几个度量如下。

首先是Lexer,它负责用一个“指针”具体地将表达式每一项进行解析。

可见,getPolybySum方法的复杂度综合最高。关于CogC,它的全名为“Cognitive Complexity”,即认知复杂度,它衡量一个程序的认知流程复杂程度,即它衡量程序是否易于理解。认知复杂度高的程序往往在难以维护。事实上,被测出的两个bug均出自该方法,且都是由于考虑欠周。

接下来是专用于Mono类各种判断的类MnJd。

它的复杂度明显更高,事实上在自己做的测试中这部分bug远远多于其它部分,若不是借助大量随机数据自测和debug,想必互测中大概率会翻车。

接下来是单项类Mono,它的复杂度如下。

复杂度较高的为乘法、用公式化简和两个toString方法。乘法和使用公式的复杂度难以避免,而toString是因为在输出时作了很多优化。上述几处自测时也出现过不少bug。

其它类的复杂度均比较低,偶有一两个用于解析的、面向过程的方法,其复杂度不可避免地高,但实际bug并不多,逻辑也比较清晰。

总结:我的程序特点是“高内聚、高耦合”。高内聚体现在各个类的操作一定仅由该类专门的方法进行,不允许其他类另写代码完成。而高耦合则体现在经常调用其它类的方法和以其它类作为参数。个人认为,在我现有的嵌套结构下,高耦合难以避免。若希望避免,或许需要考虑设计更多且专门的类存储表达式。

接下来是各类之间的关系:

其中用于分层次地存储表达式的有三个类:Poly,Mono,Trig。

Trig 表示三角函数项,它的属性主要有两个:exp(BigInteger) 即指数、factor(Poly) 即三角函数括号里的因子,其类型是多项式。以 sin 为例,一个 Trig 的表示为:sinexp(factor)。

Mono 表示“单项”,即仅用乘号连接的式子。它的主要属性有三个:coe(BigInteger) 即系数、exp(BigInteger) 即指数、sins(LinkedList<Trig>) 即因子不同的 sin 连乘、coss(LinkedList<Trig>) 即因子不同的 cos 连乘。一个 Mono 的表示为: coe * xexp * ∏sinexpi(factori) * ∏cosexp(factori)。

Poly 表示“多项”,它本质是单个Mono或用加号连接的多个Mono。它的主要属性是 monos(LinkedList<Mono>) 。一个 Poly 的表示为:∑monoi 。

MnJd 没有属性,它的所有方法均用于 Mono 有关的判断。这些方法原本是 Mono 类的一部分,但是由于在代码中加入较多化简有关操作,而水平有限,故不得不以大量代码实现;又由于单个类最多 500 行,故单独设置一个类,包含 Mono 所有判断有关的方法。

Funtion 用于存储自定义函数的表达式和其参数。它的主要属性有 exp(String) 即表达式、variables(LinkedList<Character>) 即变量列表。此外,还有一个供全局调用的 functions(HashMap<Character, Function>) ,用于解析表达式时查找对应的函数。

Lexer 和 Parser 类似“提词”作用,仅根据当前指向的字符指示“现在该用什么方法处理”。

SimplifyStr 类最简单,本可以不单独成类。它的唯一作用是将输入的原始表达式中所有多余的符号和空白符去除。

Big 类也非常简单,它定义了几个供全局调用的常用 BigInteger 数字如 0,1,2等,减少 new 的使用。

优缺点分析

按照此结构设计,表达式的层次有三层,比较概括。并且这样的结构对较复杂的嵌套或组合要求实现比较容易,足够支持大多数嵌套、组合要求。举个例子,如果迭代要求指数可以为表达式,则只需要将指数类型由 BigInteger 改为 Poly 即可。概括之,其扩展性比较强

但是这样做的缺点也非常明显,那就是容易造成某个类与其它类耦合度过高,或者内容过于庞杂。显然用于存储的三个层次类,其内容明显多于其它类,与其它类的耦合度也明显更高。如此一来,首先更容易出现 bug ,因为各个类之间互相大量调用;其次,debug 更加艰难,因为一个完整行为的完成,可能需要层层调用来自不同类的十余个方法;最后,由于仅有三个类存储表达式,故每一个类需要充分考虑所有的情况,导致逻辑控制流程更复杂,不论写或改均给阅读造成了困难

我的 bug

我在第一次作业中未出现bug。

我在第二次作业的强测中有一个点未通过,其原因是未考虑 sum 求和的上下界可能为负数,造成 i 的值恒为非负值。

我在第三次作业的互测中由于未考虑 sum 的上下界可能为极大的数而被 hack 。

这两个 bug 均出自 Lexer 类中处理 sum 的方法 getPolybySum ,且都是上下界出错。而回顾初写该方法,确实一心求成,缺乏考虑,导致错误。

但是这两个 bug 也不是偶然。正如上面度量的分析,这个方法的代码量较大,圈复杂度和认知复杂度最高,按理它最易在思考上出错,而事实也如此。从这两个 bug 我认识到,应当尽量降低单个方法的认知复杂度,将问题分而击之,往往不易出错。

此外,这两个问题也出现在了我写的数据生成程序中。由于数据生成没有考虑上述两种情况,导致即使大量测试也没有发现这两个低级错误。

发现他人的 bug

由于我自己写了比较完整和灵活的数据生成程序,同时综合了郭鸿宇同学以及申浩佳同学的评测程序,能够高效地进行自测。所以我对他人代码进行大量测试,每人一般三十万组,以量取胜。

考虑到大家的 bug 基本不在于数据大小而在于思考欠缺(即使是那个作恶多端的 sum 上下界bug,本质也是思考欠缺),故要体现 bug 并不需要多么冗长复杂的数据;事实上绝大多数用于 hack 的数据看起来很简单。此外,复杂的嵌套、组合以及过大的数字会大大降低评测程序的速度。所以我在对他人测试时,数据生成设置为嵌套层次3层及以下、数字在 100 以下、函数出现多、特殊情况出现率高。

当然,数字小的设计也导致我自己的 bug 未能测出......

尽管在数据范围的考虑上存在不足,这样的测试方法依然找出了不少他人的 bug,第一次互测我 hack 了一位同学,后两次均成功 hack 多位以上同学。

架构设计体验

我的架构在第一次作业时已深入地思考过。正如开头所说,我思考了一些较难的迭代要求,比如支持除法一类运算、支持小数分数、支持较少见且复杂的数学式子等等,最终我认为它们不会出现。我现在使用的存储结构,虽然在这三次作业里效果较好,但若出现上述情况则不得不大改一番,算是小赌赢一把。

在假定一些要求不会出现的情况下,为了确保易于扩展、易于操作以支持迭代,我第一次作业代码允许多层嵌套、确立了“多项”和“单项”的层次。如此,任何可能出现加减的式子均用 Poly ,任何不出现加减的式子均用 Mono。如此一来,在预期内的任何式子都可以方便地表示并且能写出方便的化简

即便如此,仍不得不承认三角函数的加入带来了不小的工作量。具体来说,Mono 不再是最小的存储单位,由三角函数类 Trig 替代。 Poly 也不再孤傲地作为最高层 —— 它可能委屈在括号里作为三角函数的因子。同时, Mono 有了新的属性 sins 和 coss —— 由于三角函数因子为 Poly ,光是为它们添加基本的存取、判断相等、合并同类项就足够麻烦。但是大体上,第一次作业的核心没有变,任何一个式子仍是 Poly,它往下一级仍是 Mono;而 Trig 虽然成了最小的,但它的影响更多只是 toString 多了一层而已,在实际处理时并没有抢走 Mono 太多关注 —— 大多数具体的操作仍集中在 Mono 类

至于自定义函数和 sum 式子,并没有对核心思想造成影响。对待它们无非是“出于保险”地多套一层括号,然后把每个括号当作一个 Poly 参与加法运算即可。

如此,在第三次作业时,工作量就几乎没有了。但无论如何,三角函数的化简绝对是值得一想、值得一做的。由于我的水平有限,想不到“一劳永逸”的化简三角函数方法,但是我认真地考虑了使用某些三角函数公式。我发现并非任何公式都能放心用,有的公式在特殊情况下使用反而会使式子变长。所以我选取了一些“稳赚”且实现难度不很大的公式实现。

实现三角函数的公式花费颇多时间,包括设计、局部重构、自测等等,但这是值得的。比如第二次作业我尽管错一个强测点,但由于加入化简,最终分数还是九十多。当然,更重要的是代码能力的训练。

总体感受是,起头务必思考清楚,设计时要多考虑“未来”

心得体会

在 Java 大作业之后,这是我首次写有一定码量的 java 程序。但不同于 Java 大作业,本单元的三次作业需要或多或少的重构,需要考虑“修改”;而 Java 大作业是“一次性”的。与出题人“斗智斗勇”是一种新的体验,而与同学互测是另一种新鲜。我对如何做代码测试有了一定的认知,这是受益终生的。第一单元是我面向对象编程“上道”的影响深远的一步。

posted @ 2022-03-25 21:44  20373715WYJ  阅读(86)  评论(2编辑  收藏  举报