301
社区成员
发帖
与我相关
我的任务
分享在面向《对象程序与设计课程》课程(OO)学习中的核心即为面向对象的思考分析方法,这种面向对象的思想在诸多问题的解决中带来了不同思路.在第一单元学习与实践过程中,笔者对于对象选择、封装复用以及高内聚低耦合等诸多思想获得了诸多体会。
本文用于总结OO第一单元的学习时间体会,供未来回顾学习,也为更多面向对象学习者留下少许资料以供参考。
第一轮作业较为基础,为实现一个含+,-*以及^运算的含括号表达式的展开化简,评测标准除化简结果正确外,还提出展开效率参数,主要要求化简结果在字符数量层面尽可能缩短.
笔者认为对象的选取应当抓住最小运算单元,向上加以更多运算实现复杂表达式,建立如下图所示的结构体系: 其中word类存储单个带系数x幂次项(常数视为x^0),word之间*连接形成Term,Term之间+,-号连接成为Expr.上述三个类分析具有类似的两套关键属性,一是字符串描述(value),二是实际运算中的数值含义(data),此处value沿用String实现即可,data的实现则是借助Data类实现,运算法则的搭建放入静态类Operator.Data类单个实例对应一个Word实例的实际数值,管理一对系数与x指数.
至此,重要对象的设计已经完成,解决任务的思路为,输入表达式记为Expr对象mianExpr,利用value字符串的切分逐步拆解至Word级别运算元,再由mainExpr向下递归获取运算数据Data,数据的转化(字符串到Data类可运算对象)发生在Word层级.最终获得数值上计算化简完毕的表达式,翻译为字符串输出即可.

对于括号的处理其实思路利用递归思路,括号内当作一个表达式,对外解析时当作一个简单因子与Word同级.

本轮开发任务要求较低,代码规模并不大,主要需要猜测后续开发需求方向留下后路(上学期n轮迭代后的痛苦回忆,我不要吖自己的shi山啦!),主要留下的迭代准备有以下几点:
仅做记录,可能并不是规范/便捷/有价值的,但至少在本次开发中让笔者觉得有一定用处
< 1 > 括号的迭代处理中笔者想要尽可能的让处理中的字符串简化,而括号中完全可以包含+,-,*,^运算符号,在处理中带来干扰,所以静态Factor类由此诞生,设计的考虑为在预处理中直接匹配括号项,并进行"折叠"操作,将括号部分字符串交给Factor管理,并替换为具有标识特征但不会干扰运算的字符串(如笔者采用"$index"的形式),可以极大简化穿处理流程中的括号问题,在后续调试中也可以直接观察Factor类以把握拆分进程.
< 2 > Data类的设计其实相当于设置了一个全新的数据类型,要方便的对其进行操作需要一套完整的运算体系,包括但不限于单个实例/实例列表/实例HashMap的加减乘除(没有除),虽然有点繁琐,但完成起来其实并不是那么复杂,完成之后用起来很方便很爽
< 3 > 为了方便Expr=>Term的分隔,将所有'-'替换为'+-',使'-'仅作为符号,运算仅有'+'
本次在强测中出现一定了问题,但其实本次开发并没有出现对整个设计十分致命的bug,bug原因为最终输出部分的控制第一项为正的遍历循环遗漏了break,所以实际修改仅用一行.
这次bug修复其实给我很大警醒,对象设计等大局问题固然重要,但最基础的细节上决不能出现问题.
回顾提交流程分析可能为checkStyle修改问题时误删了break行,提交前最终又没有细致检查,最终提交版又未做测试,这一bug又巧合的绕过了中测的考察点(天时地利人和了属于,其实一句x+1就能发现问题,中测这没测出来是真的巧)在未来开发中,笔者将更加关注最终最终提交版详细情况,绝不可再犯此类低级错误.
本次迭代开发主要 包括三个方面:
其中需求1在笔者在第一轮已经以简单的迭代思路实现,不作赘述.
需求2在括号迭代已经实现的基础上已经不难实现,仅需以字符串替换的形式,将传入的实参加括号替换函数中对应的形参,放入待处理总表达式迭代一并处理即可.
本次迭代最大的问题来自Exp()函数的调用.exp项无法与普通的x幂次项简单合并,exp之间的+,-操作可行性需要进行更复杂的判定操作.
本次设计仍然遵循上一轮逐步拆分,递归运算的模式,在Data中开设第二种初始化方式,初始化exp项的系数与指数,并在Operator运算基础元中分类对exp进行运算法则设置,整体架构设计基本未改变(还好留了一手).

本轮迭代由于借助现有架构基本能实现,基本为对已有类的拓展开发,本轮基本没有设置新类,仅开设DataList静态类,作用为将一组Data(主要为exp项的系数)映射到固定的负指数,用于Data组的管理.
本轮开发后明显发现Data类中因为集成了两类数据的相关方法,实现的功能更为强大,但也容易发现Data类代码规模逐渐增大,相应的处理类Operator规模也明显偏大,带来了一定的维护难度.主要原因为前一轮将拓展空间设置在Data类的内部,还是低估了实现数据类型的复杂性,理想型应当将Data设计为高级父类,将已有的基本数据作为单个子类实现.



分析可见,不佳的方法设计主要来源于从字符串因子到可运算数据的转换步骤中,由于Data继承功能过多,其实例生成的分支多,造成makeDatas方法复杂度难以降低.
并未将性能优化至最佳,本次迭代中由于exp函数的加入,对于当个项的表示就并不唯一,exp项指数部分可以提出整数因子挂到exp后作为指数,这个操作对当个项长度影响不固定,本次迭代中笔者为计算方便,也是为了防止出现bug,更是为了防止Data类的处理进一步复杂化给后续的迭代带来阻碍,统一设置为exp后部不带指数,全部乘入exp括号中的部分.
这一处理实际上给性能带来了很大损失,当exp指数部分十分复杂时,提出公因数作为外部指数可以极大的缩短输出的字符串,提出一个因子对于exp指数部分每个都与缩短效果,整体的性能优化效果将随着式子的复杂化而提高,这在强测的背景下更有利.
本次强侧中出现了一类常见但棘手的bug,即深拷贝的相关问题.
如代码架构中所述,笔者采用的运算化简策略为将单个因子转为可运算数据元Data类实例,由operator类统一提供运算方式.本次的问题出现在exp相关的一乘多的情况下,如exp(x)*(exp(x)+exp((x^2))),operator中实现exp之间相乘时由于采用的输出不慎使用了浅拷贝,导致第一项exp(x)在乘法运算一轮后受到修改,
修改方案:增设Data类深拷贝方法,运算中调用深拷贝调用数据.
本次的bug处理歧视了规范化方法调用的重要性,在本次实现的运算类方法中应当注意将传入参数设置为不可修改的final类型,防止开发中对其进行误修改.
本次作业的内容主要包括两点
1.非循环函数迭代调用的解决
2.求导因子dx()的实现
在已有的字符串替换的函数处理策略下,任务1已经实现,唯一的修改即为适应dx中x误替换问题,采用exp相同策略,在函数处理字符串替换时将dx替换为其他无干扰字符串.
对于求导因子,本次迭代作业将其归于exp()因子下相同处理,将dx求导内容统一由Expr级别向下递归求导计算,计算求得一组Data数据,代入基本表达式进行计算.

把本次实现中没有出现打破已有功能限制的内容,没有进行新类的增加.但第二次迭代中出现的问题进一步发展,Data与Operator类进一步复杂化,因此这次作出了调整,将部分单个因子以上更高层次的运算分别归类进了Expr,Term,Word三个类中.


经过处理后,Data类的重新规划,其中方法设计基本复杂度下降到了合理水平,但Operator中继承的Data运算方法难以分离,难以继续优化.
经过又一轮迭代,表达式化简结果其实包含的数据种类并未增加,但求导操作带来了合法输入内制造更复杂连续乘法、加法运算,给性能的优化带来了更大的压力,优化方向仍是分析复杂指数系数的exp()相关因子最短输出,需算出如何从exp括号中内容提出适当的内容,在这次迭代中憋着未能鼓起勇气去尝试(我真的好怕出bug啊TAT).
对于这一优化问题,目前已有的但未尝试的主要想法为求出exp括号内Data组的系数公因数,从而得出可提出公因数的基本范围,后续对所有提法进行遍历尝试,更新记录最短输出串并替换常规输出中对应因子的输出内容.
本次出现的bug相比之下更加刁钻,为互测中包含大量exp迭代的数据点造成的性能超时问题,经本地测试,能够运算出正确的结果,因此笔者将目光放在了已有结构的性能优化上.
目前分析超时的原因,主要为目前对于exp的处理思路为在括号迭代中处理,目前采用的优化方式包括对冗余括号的省略,对简单exp结构的跳步运算,但遗憾的是效果有限,再进一步优化则会初级对于Data的基本运算处理思路,基本放弃.若有读者对此有解决良方,欢迎联系笔者讨论.
本次bug主要出现原因为对于极端情况的性能优化没有留下处理余地,可能在最初运算模式设计时性能就难以满足要求,迭代至今受到这类数据便基本没有应对手段,对此类问题笔者目前无力解决,将在未来继续学习,留下未实践的一条处理思路: 即将exp相关因子维持字符串模式,处理中以特殊字符代替,如此在特殊情况性能问题中可以设置特判.
第一单元的三次迭代作业中,笔者的代码都出现了一定的问题,纵观三次bug的内容可以发现bug的产生原因逐步走向深层,最开始可能只是输出中循环的误写,当结构设计逐渐复杂时则渐渐出现了深拷贝与浅拷贝,迭代性能优化等更深层次的问题.
经过这几次迭代开发中的bug分析与修复,笔者体会到,在基本熟练IDEA级别的便捷工具的前提下(不存在拼写等低级问题),bug的出现通常不会仅仅只是某一特殊情况的处理纰漏,其背后往往有着结构层次高层设计上的缺陷,在这一单元中包括对象选取层级不适宜导致部分类过复杂或过简单、接口设置大小不合理反而造成冗余的实现,等等.
迭代开发最大的特点就是需要时刻准备应对未知的需求,一旦出现的需求超出了最初的设计容纳能力且已有设计没有留有充足的拓展空间,在不重构的情况下就只能加入大量的特判与额外处理,成为原设计的一颗越滚越大的肿瘤,催生各类刁钻的bug.
可见,要尽可能减少bug的产生,就需要从问题的源头解决,即主动跳到已有需求的更高层次,时刻保留设计的拓展空间,为自己留下退路.事实上,这一预防bug的思路在本次实验的实践中带来了诸多意外的收益.例如第二轮迭代中的括号嵌套实现,得益于第一轮预先假设好了支持无上限迭代的表达式处理,例如第三轮迭代中的函数递归调用,得益于第二轮发挥字符串替换模式的优势直接完成了迭代替换.正是因为始终保持一个实现功能比已有需求范围更广、更宽泛的状态,笔者才能做到在迭代中减少bug的产生环境.
截止最后一次bug修复,代码的类架构如下图所示:

Main方法接受字符串用于初始化MainExpr,进行启动,Expr类向下逐级拆解为Term与Word,类Word作为最小字符串拆分级别因子,将其转变为可运算数据,需进一步拆解的含括号因子经过函数/表达式处理成为Expr.可运算数据基于Data类相关的HashMap实现,借助Operator类提供统一运算方法,最终计算请求逐级递归,运算完成后回到MainExpr的Data类数据,完成输出.
代码规模如下图,图中进行过一轮结构优化,目前各类整体结构相对比较均匀.

笔者的测试数据生成思路为表达式拆分思路的逆向使用,采用随机生成固定深度的表达式树进行处理.
由于目前实现的运算行为都为因子之间的二元运算,以第三次迭代为例,笔者选择生成最大深度为5的二叉树的方式生成待运算表达式,叶节点设置因子类型数据,分支对应一个随机二元计算.此外额外设置:1.3个最大深度为4的函数树,测试函数调用问题.2. 最大深度为3的子表达式足够多个,顺序用作表达式因子.
此测试方案能够生成固定规模的较复杂输入式,对于基本运算的组合能够达到较强的测试效果.同时此测试方案仍由大量问题,例如对乘方运算的使用规则难以设置,测试不充分,以及复杂的多参数函数难以实现,对于特殊情况缺少专项攻击.
纵观第一单元,在三次迭代中笔者收益良多,主要分为两个方面.
在第一单元的三次迭代中,明显感受到三次工作状态有这巨大差异,第一轮由于需要假设基本结构,代码基础数量较大但实际技术难点不多.第二轮引入exp对代码现有思路造成了较大的冲击,代码工作量不大但需对已有代码进行修改.相比之下,第三轮迭代实际仅为实现了求导因子,借助已有的函数系统较为轻松.整体看来代码难度主要集中在1轮向二轮过度期间,而最后一轮反而基本没有挑战,作业规划存在不合理的部分.
笔者认为可以在第一轮迭代中针对x与exp两类因子实现基本加减乘与括号,(为什么不早说有exp......碎碎念),第二轮可以实现基本的函数,指数的功能,强调括号的高层迭代,第三轮则引入函数多参数,递归调用等.笔者私自认为这样的任务规划能够在前期基本明确开发的大致方向,也基本维持了三次作业的难度均衡.