从C、DS、计组一路折磨过来, 几乎都在采用过程化、函数式的编程思想。初接触面向对象的项目开发,通过了三周的对多项式求导问题的迭代开发,经历了设计、coding、测评环节,算是对面向对象有了必定的认识,这个过程总结了一些经验,在这里但愿和你们一块儿share,欢迎你们给我提意见。java
1、关于代码架构python
一、第一次做业正则表达式
主要设置了3个classshell
PolyComputer做为主类,进行I/O操做,正则表达式匹配,项的提取,合并同类型,排序这些操做express
PolyTerm表示每一项,包含项的基本特征系数和指数,constructor(处理每一个String项,提取系数和幂),以及获取和修改系数和指数,转化为String这些Methods.编程
PolyDerivation继承PolyTerm,修改构造器,继承PolyTerm的Methods.架构
总体思路分析:框架
一、在PolyComputer类中,使用正则表达式匹配每一项,并提取项之间的符号。(一项一项匹配能够有效防止爆栈问题,固然还可使用独占模式进行匹配来避免,我会在后面的做业dom
使用到,这里推荐一个学习连接https://blog.csdn.net/weixin_42516949/article/details/80858913)模块化
二、在PolyComputer类中,把提取出的符号和项进行合并,因为这里存在正负性的问题,我是单独设置了一个Method去合并,其实在提取过程当中边提取边合并也不失为一种好方法
(我的甚至更推崇)。
三、提取出的每个String类的terms传给PolyDerivation(继承PolyTerm)的生成器,在super(term)中采用正则表达式提取系数和指数,而后根据求导法则,进行求导。
四、合并同类项、排序(无关紧要,能够考虑在排序过程当中增长一种量化方法即尽可能选取正数项为开头,提高性能)、输出
2、第二次做业
第二次做业的总体架构跟第一次比没有太大的变化,主要就是修改了提取各个项的方法(引入了由乘号链接因子的项的状况,须要修改正则表达式,而在这里为了保险起见,避免爆栈
我采用了独占模式的正则表达式匹配),对于每一个项,因为均可以抽象为(x_coeff,x_deg,sin_deg,cos_deg)的形式,所以在PolyTerm类中增长了对sin,cos的指数的提取,修改
PolyDerivation的生成器,在继承PolyTerm的基础上转化为更复杂的链式求导法则。
反思:
一、应该增设factor类,求导方法类,使架构更OO,也便于后续开发。
二、优化只停留在合并同类项和对sin(x)^2+cos(x)^2=1的简单优化(二次项优化),目前打算修改成启发式化简(在讨论课上已经详细介绍过,也是python的sympy库的优化方法)的方法,提高性能。
三、第三次做业
从类图能够看出来,这很不OO,这是一次比较难受的迭代开发,我主要增设了求导方法和求导(递归)两个类,主类的方法也进行了大量的修改(主要体如今开头就进行了
非法字符的识别,去空白符等操做,把原先留给后面的提取和求导的复杂度提早,为相对此次比较复杂的递归求导开辟一条相对平坦的路,提升求导过程的开发体验)
本次的架构还有大量须要改进的地方,这里并不推荐,将主要阐述我所采用的递归求导方法。
我没有采用主流的递归降低的词法分析方法。
而是采用了层层拆解的方法
一、拆项(split)
全部的expression均可以以+/-为分隔拆解成为term,运用加法求导法则
term = split(str,1); if (term.size() > 1) { return method.plusRules(term); }
全部的term均可以以*为分隔拆解为factor,运用乘法求导法则
term = split(str,2);
因而,我创造了
public String plusRules(ArrayList<String> term) {} public String multRules(ArrayList<String> term, int i,String strIn) {} public ArrayList<String> split(String str, int mode) {}
这里须要用到栈(java自带栈,教程请移步https://www.jb51.net/article/130374.htm)其用于括号匹配,保证split的正确性,是个很是好用的手段。
二、对于不可拆项的预处理
去括号
因为多层嵌套以及表达式因子的存在,为了后续求导的便捷我单独写了一个去括号的Method,作一个化简
public String strip(String strIn) { Stack<Character> stack = new Stack<>(); .... }
三、对于不可拆项的求导
主要分红三类
(1)知足第二次做业可求导的部分
public String matchBasic(String str) {} public String multForBasic(ArrayList<String> term, int i) {}
(2)带幂次方的部分
public String matchPower(String str) {} public String multForPowerRules(ArrayList<String> term, int i) {}
(3)带sin、cos的嵌套因子
public int matchTrig(String str,int mode) {} public String multForSin(ArrayList<String> term, int i) {} public String multForCos(ArrayList<String> term, int i) {}
因而,在类DerivationMethod 和 Derivation中部署好相应的Method以后,递归的框架也造成了
//...... while (!str.equals(strip(str))) { str = strip(str); } //...... term = split(str,1); if (term.size() > 1) { return method.plusRules(term); } //...... term = split(str,2); //...... for (i = 0; i < term.size(); i++) { if (!matchPower(term.get(i)).equals("")) { returnString = returnString + method.multForPowerRules(term,i); } else if (matchTrig(term.get(i),1) != -1) { returnString = returnString + method.multForSin(term,i); } else if (matchTrig(term.get(i),2) != -1) { returnString = returnString + method.multForCos(term,i); } else if (!matchBasic(term.get(i)).equals("") || term.size() > 1) { returnString = returnString + method.multForBasic(term,i); } else { //WF } } return returnString;
这就是我搭建的递归框架,不得不感叹函数式编程深刻人心,也但愿你们能对个人方法提出宝贵的意见
2、自我BUG总结
三次测评,公测中共被hack一次,互测共被发现两个bug
第一次公测中被发现的bug出如今类PoltTerm的toString()法中,是典型的手抖党bug。
else if (this.coeff.equals(new BigInteger("-1"))) { output = '-' + "*x^" + String.valueOf(this.deg);
看了这段代码不由惊叹-*x是什么鬼????这是什么神仙bug,显然这个bug很是之好改,删除*就完事了
第一次互测中除了被人发现了上述的bug,万万没想到还有一个惊天的bug
某匿名朋友向我扔了一个1+,而后我死了
后来我发现,这个方法来自于个人提取项和符号的过程,一项一项匹配的方法有一个致命的须要判断的地方,就是提取出的项数和符号数万万不能相等,因而我作出了这样的修改
if (terms.size() == ops.size()) { System.out.println("WRONG FORMAT!"); System.exit(0); }
第一次做业以被hack的面目全非了结了
第二次,
我总结了第一次做业的教训,好比尝试构造覆盖性样例,没有自动化的疯狂测试,没有作单元模块测试
因而,装备上了自动化评测机、翻看了无数遍指导书、作了分块测试后的第二次公测、互测我终于无缺完好得活着了
第三次,事情又有那么一些不简单
这一次的错误来源于我对指导书的理解的误差
指导书中有这么一句话",例如1926^0817
是不合法的,由于幂函数的自变量只能为x
。",因而我天真的觉得相似于
sin(sin(sin((-+3+-8*-1))^9))^47的样例是绝对不合法的,所以他们没有自变量x,因而我在互测中果真被hack了。
可是其实这个幂是在三角函数上的。指导书理解误差的惨痛经历。
反思
从我本身课下debug的过程以及测试中被发现的bug来看,设计结构跟debug的难易程度以及bug的数量是息息相关的。
***第一次的bug除了测试方法的锅,还有一个重要缘由就是Method的长度,设计中设计了比较长的Method(如今看来30行左右的体验是最好的)容易致使逻辑错误,思惟混乱,而后就手抖了
***本身测试过程(尤为是第三次),因为一些重要的类,例如Dervation类过长,递归过程当中的异常状况常常出现,而且难以定位。并且Derivation中的split方法也比较长,较多的if-else
严重影响了本身的体验。
***类的拆分很重要,前两次我都直接定义了Term类,而忽略了Factor类,致使了Term类复杂度太高(尤为是正则表达式)出错的状况不在少数。
***对WF作单独检查真的过重要!!!!!!为了有效防范过程当中重复不断的WF,写一个Method或者甚至Class来检查格式会让设计过程当中集中精力在功能,体验更佳。
3、测试方法分析
这三次多项式求导是第一次对WF有了这么深的认识,也是第一次进行如此高强度的测试(尤为是互测和强测环节)。从第一次在强测环节遭到hack后的思考总结,以及讨论区的各个帖子,逐渐摸索出一些比较有效的测试方法。
先从傻瓜测试讲起:第一次测试,没有进行模块化的功能测试,本身构造的样例测试不全面,想到什么样例测什么,一切都有点糟糕。(让我第一次公测崩掉一个case的罪魁祸首!!
后来,事情终于逐渐有了起色。
Method1: 模块化测试
对设计的每一个class,每一个method,在设计过程当中,完成每一部分后进行测试,尤为是分支语句各个部分 都要有样例覆盖(第一次在这里吃大亏了!!)。
Method2: out of bounds检查
众所周知,设计过程当中不免使用到charAt,substring等语句,极容易碰到OUT OF BOUNDS的报错(尤为 是一不当心字符串被化简成空串的状况!!),为了有效规避这类常见错误,我的采起了设计以后搜索charAt ,sustring等关键字,逐条检查。
Method3: 无比快乐的自动化测试
这里采用我比较熟悉的python。
一、随机生成测试case
Xeger
Library to generate random strings from regular expressions
在写自动化测试的case的时候,采用Xger库根据正则表达式随机生成测试数据,测试效率将会大大提升
(java也可使用Xeger,进行随机生成,具体见buaa_oo讨论区大佬的分享)
to install,type: pip install xeger to use,type:
from xeger import Xeger x = Xeger(limit=const) #const can be changed x.xeger("your regular expression")
注:
*前两次做业能够直接复用java中的正则表达式,设置好limit(能够采用random库生成),直接生成case
def creat_factor(): def creat_term(): #creat_factor() * creat_factor() def creat_expr(): #creat_term() +creat_term()
*第三次做业加入嵌套因子和表达式因子以后,在creat_factor()中作微调引入creat_expr() 以及增长random函数设置嵌套层数。
*关于WF的测试:random函数对生成的结果作随机删除
二、打包程序,I/O操做
os库
os 模块提供了很是丰富的方法用来处理文件和目录。这里咱们借助os指导python进行文件路径的访问。
subprocess
subprocess包中定义有数个建立子进程的函数,这些函数分别以不一样的方式建立子进程,因此咱们能够根据须要来从中选取一个使用。另外subprocess还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通讯。在这里咱们借助subprocess讲信息输入给java,获取java输出。
to use,type: import os import subprocess def get_output(path,java_main,input): os.chdir(path) #改变工做目录到java文件的目录 #class Popen(args, bufsize=0, executable=None, stdin=None, #stdout=None, stderr=None, preexec_fn=None, #close_fds=False, #shell=False, cwd=None, env=None, universal_newlines=False, #startupinfo=None, creationflags=0) out, err = popen.communicate(input=bytes(input.encode('utf-8'))) #get stdout, stderr os.chdir('..') #返回上级目录 return str(out.decode())
注:
*调用cmd对.java进行编译
to use,type: cd java_src javac *.java cd ..
三、合法性验证
因为不太会用python比对运行结果,所以我采用了更加简便的matlab进行结果比对
(也能够给python引入malb库,可是博主在安装过程当中碰到了严重的编码问题(可能仍是比较菜),所以还在尝试,稍后成功后会做出补充)
利用python把输入,输出存到文件中,利用matlab读取文件,把输入用matlab求导后,与输出作比对(这里我采用了给x赋 [-100,100]区间内的值,比对计算结果的方法)。
to use,type:
syms x
%fopen() while feof(fp)~=1 %fgetl() %diff() for i = -100:100: %compare
四、多看指导书!!!手动搭样例!!!!
我的认为,自动化测试几乎能够覆盖性得测试,可是为了防止指导书理解误差等带来的bug,仍是多看几遍指导书,动手再搭一些样例(尤为是指导书的特殊状况)比较稳妥。
4、互测策略
互测策略主要和上述本身的测试方法接近
主要进行黑盒测试:
一、记录本身的存在bug的样例,便于后续用来hack别人(亲测至关有效
我会把全部的样例存在txt文件中,后续直接在文件中调出,输入程序
就像这样:
二、打包
既然进行黑盒测试,不如打包好,用python直接输入、输出,方法与上述自动化测试方法中同样,是提升效率的重要方法
三、评测机疯狂测试
用本身已经搭建好的评测机,疯狂自动化测试,我的经验基本上能够覆盖掉全部bug,体验极佳
辅助白盒测试:
我会选择性调用几份代码学习顺便找bug,着重关注if-else语句逻辑,正则表达式的正确性(对发现WF极有帮助),输出逻辑等
5、基于度量的关于架构的深入反思和重构思路
这三次做业(尤为是最后一次)的代码结构是我本次算是最不满意的地方
所以,我选择单独开一个部分对本次架构作深入的反思。
对三次的代码的class和method分别作了复杂度分析
第一次做业:
主类和Term类设计存在问题
主类PolyComputer的frontDeal、getTermsAndOps的ev(G)偏高
frontDeal和getTermsAndOps中都对WF的状况过量判断,好的架构应该倾向于单独的Method中作统一判断,或者使用简介的 正则表达式一次判断。
PolyTerm类中存在的问题主要集中在toString(),其中过多的if-else给代码维护带来了极大额困难,应该从新设计一个Method作简化输出的工做,便于维 护。
第二次做业:
除了和第一次有相似的问题以外(其中简化过程的问题与第一次的问题不谋而合),类的设计的问题更为突出。直接跳到Term类,而不设置Factor类, 无疑不适合当下已经比较复杂的表达式了,正则表达式过长,Term类承担太重负担,没有设置单独求导接口带来的代码复杂度高、极其不容易维护以
及复用性差等问题凸显。
一个好的架构应该更倾向于求导方法的接口化、甚至设置单独求导类或方法(由于求导法则只会愈来愈多愈来愈复杂,应该考虑后续不断增长的需求)
设置有层次的Factor和Term,并采用继承和多态来组织。
第三次做业:
我对第三次的构造方法只是采起了对第二次简单的沿用封装和新的求导规则的从新构建(由于前面架构实在不适合第三次了),新的Derivation和 DerivationMethod的构造问题
这么多飘红,惊了
可见此次的不管是递归求导仍是求导方法的构建都是比较失败的
(1)本次更凸显Factor的重要地位,以及各类Fator、Term间千奇百怪的求导规则,嵌套因子和表达式因子的新加入就更须要Factor来承担起家庭的重任,而我寻求的层层拆解最后抽象成三类的求导准则无疑太复杂了,大概也撑不起下一次的迭代开发了,有牵一发而动全身的风险。应该采用层层拆解后把部分任务分担给各类小类来减轻Derivation的工做压力。
(2)设置求导方法接口已经刻不容缓。expr-factor,term-term,factor-factor各类间做用的求导方法和接口值得被开发。