前三次OO作业总结
一、作业总结
前三次的任务都是表达式求导。这是我在高中就思考过的问题,但是很久都没有付诸实践,直到学习了“类”这个强大的工具。还有正则表达式,如果能适当使用,则不失为一个字符串格式检查的利器。真觉得有点编译原理的词法分析的赶脚。
从结果来看,不甚满意,尤其是第二次作业,由于一些不可预测的原因而没有做足够的测试,从而在强测阶段爆掉。这是个惨痛教训,争取以后的作业中不要再出现这种问题,把失掉的分弥补回来。
前两次作业,几乎没有面向对象的身影,而主要是锻炼程序的鲁棒性。格式识别中种种要求应接不暇,常常是顾此失彼。第三次作业本身就是递归和 is-a 的关系,这就为设计类、继承提供了逻辑基础。说了这么多还没有进入正题,下面分别对三次作业做个简要分析。
1、多项式求导(第一次作业)
其实这次作业完全是面向过程的。多项式可以表示为二元组的列表,这种形式关于求导这种运算是封闭的,也就是说求导依然产生二元组的表。由于涉及到合并同类项,所以使用映射表比较好,即hashmap
,指数作为键,系数作为值。这样,在查找、添加、合并的时间复杂度都很低,编码也方便。
性能分唯一得分方式就是合并同类项,系数1不输出,指数为1不输出,0项忽略。另外一个小点就是正项先输出。
这次只有一个主类,一Main到底。因为HashMap
已经可以存储所有多项式,就没考虑另定义类。
分模块进行:检查合法性,构造表达式,求导,输出四个主要方法。开始一个大问题是使用长正则匹配整个表达式,所以在字符串长度大于1000的时候会爆掉。后来改用拆项法,每次向后扫描符合条件的项。主要使用的工具是Pattern
,Matcher
。这是我第一次思考正则效率的问题,以前只觉得正则强大,没想到引擎是NPC的。这次记住了一个经验:少用星号,少用加号,少用竖线,多用中括号,多用问号。少用 (),多用 (?😃。
Main类结构:
度量:
Class | OCavg | WMC |
---|---|---|
Main | 5 | 40 |
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Main.createPoly() | 1 | 5 | 7 |
Main.exitError() | 1 | 1 | 1 |
Main.func(int,int) | 1 | 4 | 4 |
Main.judge() | 1 | 7 | 8 |
Main.main(String[]) | 1 | 1 | 2 |
Main.nextStart(int,int,boolean) | 6 | 6 | 8 |
Main.output(HashMap<BigInteger, BigInteger>) | 3 | 8 | 10 |
Main.qiuDao(HashMap<BigInteger, BigInteger>) | 3 | 4 | 5 |
可以看出,在表达式判断合法性、构造和输出方面比较复杂,但我觉得这个比较正常,毕竟格式属于盲搜,而输出需要考虑不同的情况做优化。
互测被测出字符串末尾的加号问题。这是在Matcher扫描过程中出现的错误。这次总体比较满意,美中不足的就是性能分,在一个 -1*x上没有优化成 -x。
2、多项式、三角函数乘积求导(第二次作业)
这次栽了大跟头。由此可知程序测试在工程开发中的重要性。一个小小的replace错误导致强测加互测总共WA了10次。这个错误太致命了,由于split("\\+")
是以加号分割字符串,而预处理阶段的干扰加号没有排干净,失之毫厘谬以千里。
这次有点面向对象的感觉,不过,依然几乎是一Main到底,唯一的一个内部类还是当结构体用的,它用来存储sin,cos,x的指数,作为Hashmap
的键,系数作为值。在HashMap中,自定义类需要重写equals()
和hashcode()
方法,初步使用了方法重写。扫描方法还和上次一样,只不过多了两种sin(x)和cos(x)。
优化仍然是合并同类项,但是还有sin(x)2+cos(x)2=1这种问题,当时没有过多考虑。后来认真研读了大佬们的算法,才知道这个公式有好多文章可以做。原来面向对象也是考数学的啊。
Main类结构:
度量:
Class | OCavg | WMC |
---|---|---|
Main | 6.1 | 61 |
Main.Term | 1.33 | 4 |
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Main.Term.Term(BigInteger,BigInteger,BigInteger) | 1 | 1 | 1 |
Main.Term.equals(Object) | 2 | 3 | 4 |
Main.Term.hashCode() | 1 | 1 | 1 |
Main.createExpr() | 5 | 9 | 11 |
Main.func(int,int) | 1 | 4 | 4 |
Main.gTmD(boolean,String) | 2 | 1 | 2 |
Main.judge() | 1 | 9 | 13 |
Main.main(String[]) | 1 | 3 | 3 |
Main.nextStart(int,int,boolean) | 7 | 8 | 12 |
Main.output(HashMap<Term, BigInteger>) | 1 | 15 | 17 |
Main.printWrongFormat() | 1 | 1 | 1 |
Main.putTerm(HashMap<Term, BigInteger>,Term,BigInteger) | 1 | 2 | 2 |
Main.qiuDao(HashMap<Term, BigInteger>) | 3 | 5 | 6 |
仍然是和判断、构造、输出有关的函数,复杂度比较高。而且由于在增加了三角函数之后仍然一Main到底,平均复杂度提高了。
3、多项式、三角函数复合求导(第三次作业)
这一次吸取了前一次作业的教训,做了百余数据的测试。但还是被hack了,sin(-1)这种会WF,我的妈呀表达式忘了考虑常数为负数的问题。有惊无险,如果强测多几个这种点我就又玩完了。(正所谓bug是难以避免的,只有神仙才能做到完全没有bug,人与人之间差的只是出bug的几率。但是这个几率,能真正决定一个工程的成败)。
不过半天的思考架构也让我受益匪浅,至少能利用面向对象的思想,把一个具体的事物抽象出数据类型和操作了。真正的数据类型不仅仅是存储,而在于性质和操作的捆绑。
这次是真正体现面向对象威力的时刻。表达式本身就是个抽象概念,所以用了抽象类。表达式要参加运算的,所以定义了加、减、乘、复合四种运算作为具体方法。不同种类的表达式都是表达式,而且都有统一的加减乘复合这些运算,所以继承表达式类,这是 is-a 关系。求导,对不同样式的表达式来说规则不尽相同,而且是分层递归的,所以定义成抽象方法,在表达式类的子类中重写,这是多态。子类呢,分别定义基本表达式类(包括常数、幂函数、三角函数的乘方),加减类(两个表达式通过加减运算形成),乘法类(两个表达式通过乘法运算形成),复合类(两个函数复合的结果)。这样,在创建表达式的过程中,就会自动形成表达式树(二叉树),求导起来也特别方便,直接向下递归即可,而表达式树的叶子节点就是基本类。
这样做的好处是,在表达式格式识别、创建的过程中可以略去括号的影响,宏观考虑,递归向下。有点类似于数据结构课程中做的表达式计算器,当有括号时递归进行。
这次把类设计的各个知识点全部回顾了一遍,包括构造方法、方法重写、抽象类、继承、动态联编(多态)这些。我觉得这次作业是比较经典的面向对象练习,对OO思维是一种很好的锻炼。在前两次锻炼了程序的鲁棒性之后,第三次作业加深了对封装、继承的理解。继承并不仅仅是为了代码复用,还有一个重要作用就是可以实现运行时确定类型,这是OO的精髓所在。(顺带的,把C++中的虚函数、纯虚函数这些概念彻底搞懂了。C++,Java这俩语言感觉真是亲兄弟)
最初想用接口,但考虑到复用性,加、减、乘、复合这些方法无需在每个类中重新写一遍,就改用抽象类了。
UML类图:
度量:
类:
Class | OCavg | WMC |
---|---|---|
AddSub | 1.2 | 6 |
Basic | 3.75 | 15 |
Composite | 1 | 4 |
Expr | 3 | 24 |
Main | 5.29 | 74 |
Multiply | 1 | 4 |
方法:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
AddSub.AddSub(boolean,Expr,Expr) | 1 | 1 | 1 |
AddSub.getExpr1() | 1 | 1 | 1 |
AddSub.getExpr2() | 1 | 1 | 1 |
AddSub.isAddOrSub() | 1 | 1 | 1 |
AddSub.qiuDao() | 2 | 2 | 2 |
Basic.Basic(int,BigInteger) | 1 | 2 | 3 |
Basic.getExpClass() | 1 | 1 | 1 |
Basic.getPara() | 1 | 1 | 1 |
Basic.qiuDao() | 10 | 10 | 12 |
Composite.Composite(Expr,Expr) | 1 | 1 | 1 |
Composite.getInner() | 1 | 1 | 1 |
Composite.getOuter() | 1 | 1 | 1 |
Composite.qiuDao() | 1 | 1 | 1 |
Expr.add(Expr) | 4 | 4 | 5 |
Expr.composite(Expr) | 7 | 7 | 7 |
Expr.equalsToOne() | 1 | 2 | 2 |
Expr.equalsToZero() | 1 | 2 | 2 |
Expr.isConstant() | 1 | 2 | 2 |
Expr.isX() | 1 | 3 | 3 |
Expr.multiply(Expr) | 5 | 6 | 7 |
Expr.sub(Expr) | 4 | 4 | 5 |
Main.add(Expr,Expr) | 2 | 2 | 2 |
Main.brackets(String) | 1 | 6 | 7 |
Main.conditionalPrint(Expr,String) | 1 | 2 | 2 |
Main.create(String) | 6 | 8 | 12 |
Main.find(Matcher,int,int,int,int) | 3 | 3 | 5 |
Main.gI(String,int) | 3 | 2 | 6 |
Main.isSin(Expr) | 1 | 2 | 2 |
Main.judge(String) | 2 | 10 | 17 |
Main.main(String[]) | 1 | 3 | 3 |
Main.mul(Expr,Expr) | 2 | 2 | 2 |
Main.nextStart(String,int,int,boolean) | 7 | 8 | 12 |
Main.output(Expr) | 1 | 16 | 16 |
Main.tooBigIndex(BigInteger) | 1 | 3 | 3 |
Main.wrongFormat() | 1 | 1 | 1 |
Multiply.Multiply(Expr,Expr) | 1 | 1 | 1 |
Multiply.getExpr1() | 1 | 1 | 1 |
Multiply.getExpr2() | 1 | 1 | 1 |
Multiply.qiuDao() | 1 | 1 | 1 |
虽然功能比上次复杂,但是OCavg,也就是平均复杂度反而降低了,可见使用类封装、继承的巨大优势。
二、学到了什么
首先就是正则表达式,包括正则的效率问题。然后就是程序的鲁棒性,实际工程中程序对各种不同状况做出的反应都应该符合用户需求。最最重要的就是面向对象的思维。
另外,oo强迫我改掉了不加空格的代码风格。这也是一个巨大的提升,毕竟看那种堆在一起,一个方法几百行,满屏goto等等的代码有多难受,你我都知道。
有痛失分数的懊悔,也有巨大的收获。在这种强大挑战面前,就要有把oo做成软工的思想准备(逃~
三、工具推荐
首先,我推荐用markdown写博客,博客园自带的编辑器超级恶心。可以先用typora
编辑好,然后在博客园中设置:管理博客--设置默认编辑器--修改为markdown。之后,把你编辑好的markdown复制到博客的文本框中。可以保存为草稿预览一下。
1、UML工具
idea自带的,选中所有类,右键--Diagrams--Show Diagram
2、复杂度分析工具:MetricsReloaded
idea菜单栏--文件--设置--Plugins--搜索MetricsReloaded--Install--重启idea
安装成功后,打开工程,选择你需要分析的类,然后右键--分析--Calculate Metrics
3、CSV、markdown、HTML、XLS表格转码
网址:tableconvert
idea的Metrics分析完成后,我们可以选择导出到文件(左下角一栏,特别不显眼的地方)。导出的文件是一个CSV格式,我们把它用Excel打开,然后选择一个表格(注意你需要的表格边界,CSV一般没有sheet),复制到剪贴板。
之后打开上述网站,单击上方import选项,把你剪贴板里的东西粘贴到文本框,确定。下面的导出格式选择markdown。OK,最后把生成的markdown表格复制到你的md文件中。
不过,CSV在typora中是可以直接转码的,typora大法好啊!(天灭TinyMCE,退TinyMCE保平安)
4、图片--base64转码工具
网址:xpcha
这是一个极好的base64和图片相互转码的工具。我们知道,markdown的图片要么用超链接链接到磁盘、网站,要么用内嵌模式即base64码。而链接到磁盘则无法上传,链接到URL则无法链接本地图片,还需要图床等工具,极不方便。所以,我们用这个网站把图片转化为base64之后,在文件末尾设置链接,在行内进行引用。具体可参考CSDN或简书上的教程。