2020春-面向对象设计与构造-第一单元总结-简体版本
2020春-面向对象设计与构造-第一单元总结-简体版本
一、程序结构分析
作业1
作业1代码量不多,主要难点在於将输入的字符串形式的表达式转换为特定数据结构的存储形式。在作业1中我分了三个类:Main、RegExp、Term。其中RegExp类使用静态变量存储了构造的正则表达式字符串;Term类对指数和係数进行了封装,并给出其构造方法和toString方法;主类对匹配输入字符串、处理输入字符串、输出结果的三个过程进行了封装,三个部分均使用了static修饰的方法。
下图为代码行数统计信息
可以看出作业1代码量不多,但注释所占代码的比例比较低。在较为简单的程序中这可能不会有很大影响,但在更复杂的程序中,注释行所占比例应当再大一些,程序的可读性才会有所保障。
下图为UML类图
此次作业的类之间没有继承关係,只有互相调用的关係。可以説本次作业仍旧没有脱离面向过程的“魔爪”,主要处理方法仍旧在主类中以static修饰的方法形式出现。
下图为方法复杂度分析
可以看出主类中的match方法与Term类中的toString方法的复杂度较高。其中Term.toString方法主要是对指数和係数的可缩减表达的情况进行了枚举式的判断,而Main.match方法主要是条件语句、循环语句的层数过多,将匹配正则表达式与构造表达式数据结构的过程全都写到了一个方法中。
下图为类复杂度分析
其中主类与Term类的平均复杂度较高,这与前面的方法复杂度有所对应。
作业2
作业2在作业1的基础上增加了对正餘弦函数求导的要求。由於作业1我是采取了完全的面向过程思想,可以説是没有架构,所以作业2不能叫重构,而是从头开始首构(首次建构)起了一个面向对象的架构。与作业1相比,本次作业有了面向对象的影子;但是缺点在於没有考虑之后作业3可能的拓展要求,所以作业3仍旧不能使用作业2已有的架构,即作业1无构,作业2始构,作业3重构。
作业2中我分了4个类:主类、RegExp类、Term类、Expression类。其中主类仅涉及对输入的读取与处理后的输出;RegExp类以static变量的形式存储了所有的正则表达式;Term类以係数、幂指数、正弦指数、餘弦指数四个BigInteger对单个的项进行了抽象;Expression类以Arraylist<Term>的形式对表达式是由多个项组成这一实际情况进行了抽象。
值得一提的是,我的作业2在第一次提交中测的时候就通过了所有的测试点,这还是令我很高兴的。
下图为代码行数统计信息
没有注释确实不是一件好事,不过可能也是因为我每个方法的名字都尽量不用缩写、能较好地表达每个方法的功能,因此在可读性上我认为还算可以。
下图为UML类图
此次作业我认为最符合面向对象思想的地方就在於Term类与Expression类内部的方法了。Term对象求导会生成一个Expression对象,Expression对象求导也会生成一个Expression对象。Expression类的求导方法会调用Arraylist中每一个Term的求导方法,并将Term对象求导得到的Expression对象通过append方法添加到将返回为Expression类求导结果所生成的的Expression对象中。
下图为方法复杂度分析
Expression.append方法与Expression.symplify方法复杂度较高。我在向表达式中添加每一项的过程中都遍歷了表达式的每一个Term项判断能否对其进行合并简化,又在输出之前又对表达式进行了多轮简化,而在简化过程中出现了许多判断Term不同的拆项方式能否与已有项进行合并的过程,因此我想是这一部分导致了复杂度较高的情况出现。此外Term.Term(String)方法与Term.toString方法复杂度较高,原因是我将分割得到的每一项对应的字符串转化为存储结构的过程全部写到了Term的构造方法中,而toString方法仍旧是枚举式的输出。
下图为类复杂度分析
其中Expression类的复杂度较高,Term类次之,而主类已经不再像作业1一样将所有处理过程都包含进去了,而是仅仅提供一个程序入口。可以看出作业2比作业1更加面向对象。
作业3
作业3在作业2的基础上增加了表达式因子的概念,使得原先作业2“一个项仅有係数、幂函数、sin(x)幂次函数、cos(x)幂次函数构成”的紧密封装的Term类无法继续使用,因此是一次重构。
作业3要管理的类较多,所以我第一次使用了package来对各个类进行了管理。此外我还在作业3中使用了工厂模式,将构造方法交给工厂统一管理,简化了构造新的因子的复杂程度。
下图为目录结构树
下图为代码行数统计信息
注释还是很少,希望之后我能多写点注释吧。。。
下图为UML类图
本次作业应用了接口功能,为不同类型的函数类规范了一个统一的求导接口。本次作业中表达式本质上是一个Combination类型的数据,它的下层是不同Combination类型的组合。值得一提的是,其中Combination的一个具体实现——Single类型,是对Function类型的封装,即实现简单函数的Function类套了个壳,从而其可以按照Combination的方式来使用。在对顶层的求导接口进行调用的时候,会递归调用下层的求导接口,而每层的求导接口都将其自身求导的结果维护好,从而很方便地确保了求导功能的正确性。
下图为方法复杂度分析
上图中复杂的方法主要是由输入字符串向存储结构转化的过程(toComb)、和存储结构向输出字符串的转化过程(toString),作业的难点也正在於此。此外GenaFirst.gf方法是根据项的符号创建第一个是-1或+1的因子,由於项前面的符号可能会有多个,因此涉及了较多的判断。
下图为类复杂度分析
可以看出,类复杂度与方法复杂度是对应的。
二、Bug分析
作业1、作业2
作业1、作业2在中测、强测、互测中均未被测出bug。
作业3
作业3在强测中被测出了一类bug,在互测中被测出了另外一类bug。
# Bug 1 - TLE Bug
测试数据:+cos(sin(cos(cos((-cos(sin(sin(cos(cos(sin(x**2)))))))))))
数据特徵:涉及到了较多层次的嵌套,时间复杂度较高。
错误原因:在toString方法中使用"+"对字符串进行拼接,导致在多层递归调用的时候程序速度较慢。
解决办法:将toString方法中使用"+"进行字符串拼接的代码修改为使用StringBuilder.append()方法,解决了程序可能存在的超时问题。
反映问题:习惯於使用String与"+"进行字符串拼接,没有考虑到字符串拼接可能会造成的性能影响。
# Bug 2 - Remove OuterBrackets Bug
测试数据:+- sin(((5+sin(x)) * (5+sin(x))))
数据特徵:在表达式因子的首位出现了不同的表达式因子。
错误原因:在预处理过程中会根据栈的思想将表达式因子最外层的'('、')'变换为'['、']',而在循环去除表达式因子最外层的括号时,仅判断了第一个字符与最后一个字符是否为'['和']',而没有判断这两个括号是否是成对的括号,造成了会对具有上述数据特徵的数据误判为WrongFormat。
解决办法:在判断首尾是否为'['、']'的条件中添加判断字符串除第一个字符之外的子串中是否存在'['字符的条件,即添加!trim.substring(1).contains("[")
条件。
反映问题:考虑问题不全面,也没有做足测试。
三、互测阶段发现他人bug采用策略
作业1互测时尚未掌握自动对拍程序的编写与使用,因此自行构造了一些边界样例,但是没有hack到其他同学的bug。作业1整个房间的同学都没有hack到其他同学的bug。
作业2互测时使用了自行搭建的基於Python的Xeger包、Sympy包与Windows批处理程序的自动对拍程序,找到了三名同学的不同bug。
作业3的测试数据自动生成较为复杂,因此没有使用自动对拍程序,而是测试了自行构造的一些边界数据,找到了一名同学的bug。
应当检讨的是,我没有结合被测程序的代码设计结构来设计测试用例。大多数情况下我并没有从閲读其他同学的代码中找到明显的bug,经常是先得知了某个测试数据测出了某个同学程序的bug,进而依据该数据找到该同学程序结构上的不足。这恰恰是反过来的逻辑。
四、应用对象创建模式
对象创建模式包括构造函数模式、原型模式、工厂模式、单例模式等。
在作业1和作业2中,我基本上都在使用构造函数模式进行对象构造;在作业3中,我使用了工厂模式和构造函数模式共同进行对象构造。
对於构造函数模式,使用起来很方便,最自然,当需要构造对象时直接new一个即可。构造函数模式也是最基本的对象构造模式。当程序中的类较少的时候,应用构造函数模式较为方便。
原型模式,即对应於C++的拷贝构造、Java的clone方法。原型模式主要在需要对对象进行拷贝操作时使用,使用的机会比较特定。
工厂模式,可以将继承自同一个父类或实现了同一个接口的不同类的构造方式整合到一起。当有较多的类需要管理的时候,可以采用工厂模式,将所有类的构造部分的代码封装到一起,这样的代码较为简洁。
诚然工厂模式有很多好处,但工厂模式也不是万能的。其实很多情况下使用工厂模式并不一定会使代码更加简洁。如当构造两个类的对象所需的参数个数、参数类型不相同时,就难以对这两个类使用同一个工厂(可以使用可变长度参数、或HashSet,但如果两个类差别很大的时候就应当考虑到底是否应该使用同一个工厂)。此外当整个程序没有特别多的类、或者说只有一个类需要用来构造,这种情况下额外为这一个类使用工厂模式可能不如直接new来得简单。
如果要将工厂模式应用到我的作业1与作业2,就需要对整个程序进行重构,依照因子设计各个类而不是依照项。而如果按照现在我的作业1与作业2的情况,只有一个类或者只有两个类,则没有必要使用工厂模式。事实上,虽然我的作业2无法扩展迭代得到作业3,但单独使用四元组处理作业2的问题就很容易,要考虑的内容不多,测试中不容易出现问题,也比较容易进行性能优化。所以我想作业2中没有使用工厂模式也不见得就不好,还是要看具体应用场景。
五、心得体会
- Java很好用
- IDEA也很好用
- 面向对象真的很有意思
- 面向对象很难,有时候压力很大
- 但是当做出来作业的时候又会很开心
- 个人认为面向对象这个译名不如物件导向
- 不过正则表达式听起来要比规则运算式更加舒服
- 没事别瞎写README文档,作业1就不小心把姓名学号写上去了