究极菜鸟的_OO第一单元作业总结
第一次作业
1、思路及架构

由于第一次作业中不存在嵌套格式的输入(即不包含小括号),只需用正则表达式将输入分开成一个个项,项中则分为常数和幂函数两种分别合并同类项,求导后存入Main中map容器并合并即可。三个类之间无继承关系,Tool类负责输入处理,Term用于存储项的内容和求导。
由main函数读取表达式,传递给Tool类中的initial方法,该方法在进行去除空格、合并加减号的预处理后,用正则表达式将输入拆分为一个个项组成的字符串数组返回给main函数。main函数为每一个项new一个Term类的对象,Term类的构造方法即可完成同类项合并、解析出系数和指数的任务。随后在main中调用Term对象的求导方法,并根据求导结果将Term对象放入TreeMap中并合并同类项。最后main函数遍历TreeMap并调用Term对象的toString方法打印输出。
2、基于度量的分析

由于是第一次作业,我面对对象的思维显然还不完善,尤其是main函数还充斥着面向过程的味道,认知复杂度严重超标。这是因为main函数做的事情太多太杂了,不符合高内聚的要求,且阅读和debug起来较为困难,应该将部分工作拆出来写成方法,也可将一些工作交给其他类解决(比如把输入部分完全交给Tool类,把合并同类项存入map的操作写成单独的方法,也可以单独写一个类来存放map,还可以设置常数因子和幂函数因子两个子类,将Term类、Main类中对两种因子的处理放到子类中解决,实现有效解耦)。
Term类的toString方法的圈复杂度也超标了,但这也是没办法的事,为了实现一些优化比如1*x优化为x、x**1优化为x等等,需要许多特判if。
耦合度高的架构导致我在面临第二次作业的时候完全无从下手修改或复用,只能重写。
3、bug和评测
第一次作业也没啥能出wa的点
注意x**2特判转化为x*x能使结果变得更短,也算是一个优化小坑
还有x*x*x*x...长度为1000的x连乘可能导致出现RE,原因是正则表达式爆栈,对比以下两行正则表达式
final String termPattern = "([+-]?(?:" + cons + "|" + func
+ ")(?:\\*(?:" + cons + "|" + func + "))*;
final String termPattern = "([+-]?(?:" + cons + "|" + func
+ ")(?:\\*(?:" + cons + "|" + func + "))*+;
唯一区别是表达式末尾差了个*,但第一行的正则会出现爆栈,而第二行不会
第二、三次作业
1、前言
因为第二次作业新增了表达式嵌套和三角函数,这导致上次作业中的正则表达式、map容器均不再适用,于是我进行了完全的重写。
由于本人比较笨且喜欢摸鱼,为简化代码、大幅降低出bug的几率,我的第二、三次作业仅以正确性为目标,完全不考虑优化,还采用了经典的错误写法————字符串求导。
2、思路及架构
因为第二、三次作业较为相近,这里只阐述第三次作业的思路,整体思路其实就是指导书最后的提示,即化整为零,将整个表达式拆分为项,再拆分成各个因子,表达式、项、各种因子均有相应的类,这些类由于均需要有求导方法,于是可抽象出求导接口实现归一化。由于不考虑化简,在每个类中无需用容器存储数据信息,只需存储字符串即可(十分的low),所有求导过程也只是字符串的变换与拼接而已。

(1)输入处理
整个程序从main函数进入,首先读取一行字符串,new一个Express对象,并将输入字符串传入Express的构造函数。
要写出超长正则,仍需“化整为零”的思想,即整个项匹配起来很复杂,但只匹配一个因子很简单。如下代码所示,分别写出常数、三角函数、幂函数、表达式因子的正则表达式,将它们拼在一起构成匹配所有“因子”的表达式factor,最终用factor构成"termPattern"即可匹配一整个项。
String cons = "(?:[ \t]*[+-]?(?![ \t])\\d+[ \t]*)";
String trig = "(?:[ \t]*(?:sin|cos)[ \t]*%.*?%[ \t]*(?:\\*\\*" + cons + ")?[ \t]*)";
String power = "(?:[ \t]*x(?:[ \t]*\\*\\*" + cons + ")?[ \t]*)";
String expressFactor = "(?:[ \t]*%(?<=%).*?(?=%)%[ \t]*)";
String factor = "(?:" + expressFactor + "|" + trig + "|" + power + "|" + cons + ")";
String termPattern = "([ \t]*[+-][ \t]*[+-]?" + factor + "(?:\\*" + mix + ")*)";
在这些之前其实有几个预处理。
预处理之一,根据题意,整个表达式的第一个项是不一定有符号的,而表达式中其余的项都是有符号的(因为项之间用加减号连接),这就出现了项的格式的不一致,会对正则表达式匹配项时造成不便。于是有一个特判,若表达式开头没有符号,则会补上一个+。
trig和expressFator中出现了字符%,这又是预处理后的结果。
正则表达式无法处理递归嵌套的输入(比如因子可以是表达式,表达式中又可以包含表达式因子,以此不断嵌套,正则表达式是无法只识别嵌套的最外层括号的),这意味着需要预处理来确保正则能够识别出嵌套。
我采用的预处理是用%替换掉所有的最外层括号,并把由%括起来的部分暂时当做一个整体看待,比如(x**2+(sin(x))*-1)将会被转化为%x**2+(sin(x))*-1%,sin(cos((x+1))),也会被替换为sin%cos((x+1))%,这样正则表达式即可根据%来识别出嵌套并将它们当做一个整体全部识别,至于嵌套里面的内容,后面递归时会将%的内容作为表达式再次进行如上预处理并与正则匹配,由此利用递归将嵌套全部拆开。至于替换的方法,用一个计数器变量当做栈,计数处理括号,但这里由于复杂的逻辑极易出bug。事实证明“替换最外层括号”的思路是一个巨大的错误,原因详见后面基于度量分析部分。
(2)求导思路
整体思路即从上到下,直接将表达式拆成项、再拆成乘法类、再拆成嵌套类、最后拆成各种因子。
在Express的对象中,对于每个被从表达式中分离出的项,各new一个Term对象,在此对象中先去除空格、合并加减号(这里无误是因为在Express中已经用正则判断格式正确与否了,此处直接预处理即可,但不可处理嵌套表达式因子内的空格和加减号,因为这部分在Express中没有经过正则的正确性检验,需要到后面递归处理),然后根据从左往右第一个符号*将项分为两个部分(这个*不能是表达式因子中的),左边是一个因子,右边仍是一个项,并建立一个“乘法类Multiply”将这左右两部分传入。
在Multiply中建立工厂,根据左侧字符串创建对应的因子类或嵌套类(常数因子类、乘方嵌套类、多项式因子类),工厂返回值的类型为接口Derivate,随后立即derivate.derivate()调用因子类的求导方法,并保存toString返回的求导结果。而右侧的部分仍被当做一个项,再new一个Term对象,并调用求导方法,这里就是递归求导了。最终根据乘法求导规则“拼接”字符串。
若左侧因子为多项式因子,求导前会将左右%删除,然后当做一个表达式并new一个Express对象,再次调用Express的求导函数进行递归求导,所以Express不只用于输入的第一次处理,还用于多项式因子的递归求导。
若左侧因子为乘方嵌套因子,在ChengFang类中会具体判断因子类型,并进行再深一层的求导操作(指对sin和cos的求导,再深一层则是对三角函数嵌套的因子进行求导,这个嵌套内容是当做表达式进行递归处理的,因为只有Express类才有判断格式正确的功能)。
Multiply类中就体现了工厂类和接口归一化的好处了,即不知道左侧因子是什么类型,但因为所有因子都实现了Derivate接口,只需将左侧字符串传入工厂,工厂内部自行判断,最后工厂只需要返回Derivate类型即可。在Multiply类中无需关心左侧是什么因子、返回什么类型,只需要对Derivate对象调用求导方法即可,这就是接口所谓的"行为抽象",并且借助“简单工厂模式”实现了“创建对象”操作与求导操作的解耦,距离“低耦合”更近一步。
3、基于度量的分析

- 从开始的uml类图中也可看出,由于完全没有考虑优化等问题,类的总个数、每个类中的方法都非常的少,总代码量也只有500行不到,所有方法均不超过50行,大部分方法的认知复杂度、基础复杂度、设计复杂度、圈复杂度都较低。
- 先抛开复杂性分析,超长正则相比于递归下降法更容易爆栈,当输入过长时可能会发生难以预料的错误,这也就是为啥随着作业的迭代,题目限制的数据长度越来越短。
- 一个没有必要的方法。
Term中的recover原意是用来将三角函数外层%替换会括号,而且表中也可看出认知复杂度不小,较难看懂,但实际完全没有必要,该操作在ChengFang类中解析因子类型的时候处理更为简洁,而ChengFang类中已经有了相关处理了,这就是重复处理了。 - 由于求导过程都是字符串处理,没有将字符串解析为数学数据存入容器,这意味着不可能优化。
ChengFang类中若判断底数不是x,new的应该是TriFunction这个父类,而不是具体的sin或cos,这样可减小ChengFang类的复杂度,同时也可令整个程序的继承结构更加清晰。而且这样就不用在ChengFang类中再建立工厂类了,因为一共就只需判断是x还是三角函数两种情况- 最重要的是,现在来看,只替换外层括号的预处理可能是一个巨大的失误,理由如下:
一、需要时刻注意外层括号内的内容对外部操作的影响(比如去除空格、用*号分割项等),需要注意的地方非常的多,也就极其容易出bug。我第二次作业的两个bug全都与“屏蔽括号内干扰”直接相关。甚至我在写此篇博客时,发现了一个强测没有测出来的bug(对于sin((-++x))我的程序不会报wf),就是因为在合并加减号时把括号内的也处理了,但此时还没有验证括号内加减号的正确性。
二、极其夸张的复杂度,预处理本身就极易出bug。从上面的复杂度分析表即可看出,pretreat函数的认知复杂度离谱(竟然高达45)、圈复杂度也极高(24),且与其他代码耦合性强,难以维护和扩展。而pretreat方法正好就是用来在Express类和三角函数类中替换括号的方法。
预处理可能的解决办法如下:
将整个嵌套的内容全部替换为#,并将被替换的内容保存起来,存入ArrayList,跟着字符串一起递归,并始终保持该List的第一个元素对应字符串的第一个#。例如(x**2+(sin(x))*-1)替换为#,等到需要用到#的内容时再从List中取出,虽然这样也并不简单,每次传输字符串时还要维护一个ArrayList,但这就相当于把嵌套括号因子的处理与其他因子的处理解耦了,而不是混杂在一起。还可以把存储被替换字符串的容器设置为static,当做全局变量使用,就不用每次都传容器了。
当然如此高的出bug几率和我采用的“字符串求导法”有很大关系。不能将字符串中的数据解析出来、放到因子类里并利用容器分类管理,所有数据都堆积在一个字符串里,处理起来能不复杂吗。
但不论再怎么改,如果只用正则,复杂度依然无法明显改善,优化起来十分麻烦,本质原因就是正则无法处理递归输入,必须想办法单独处理嵌套部分,故本次作业的最佳方法仍是递归下降文法分析。正则表达式目前看来只适用于没有嵌套的较短输入。
4、bug和评测
我只在第二次作业中被强测和互测测出过两个bug,全都与“替换外层括号”操作直接相关,且相关代码块的认知复杂度和圈复杂度都较高。
一是在处理项的符号时,-应当被处理为-1*,+同理,但我最初只是保留正负号而没有用数字1和*补成一个因子,而我在每个项求导后都会在外面补上一对括号,这会导致求导*()时会出现*+()的情况
二是在Term类中寻找第一个*以将项分为两个部分时,需要屏蔽开头的表达式因子或三角函数中嵌套的*号的干扰,我的方法是将判断项开头因子是否包含括号嵌套结构,若包含则将第一个因子整体替换为@,再去识别*。但bug出在我误用String类的replace方法,将整个项中所有和第一个多项式因子相同的因子都会被换为@(比如项(x+1)*(x+1),处理后为@*@,但应该为@*(x+1)),这在后面的求导拼接字符串时,会导致@被保留了到了求导结果中。
实际上我程序中大部分的bug都是一边写代码一边hack自己解决的,第三次作业在写代码过程中甚至发现了十几个bug,hack自己的数据也保存了下来,最终这些数据被我用到了互测中。但我并未阅读互测房代码以针对性hack。
心得体会
经过这三次迭代作业,我逐渐掌握了正则表达式的各种细节语法、超长正则的书写技巧、hack自己的技巧,理解了处理复杂表达式的化整为零思想、接口的行为抽象意义、工厂模式的作用、高内聚低耦合的思想,还练习使用了idea的一些代码分析工具(uml图、代码复杂度分析)。

浙公网安备 33010602011771号