经历过OO作业的考验与摧残,在此分享自己的心得与感悟。
这三次作业分别为:多项式加减运算、傻瓜电梯调度、ALS电梯调度。
一、多项式加减运算
这是熟悉面向对象编程思想的第一次实战作业,也是我第一次接触JAVA编程,虽然在编程中学习的过程是痛苦的,但那种所学即所得的欣喜也是不言而喻的。在实际编程过程中,相信大家均深有体会:对于多项式加减的计算并不是太过复杂,该作业的难点在于如何保证程序的鲁棒性。一款程序的设计需要考虑到方方面面的因素,站在用户群体的角度思考:如果我是用户,我会有怎样的行为。用户的输入多种多样,五花八门,即使你在说明书中明确规定程序应该接受怎样的正确输入,可也难以保证用户的误输入。因此,程序的鲁棒性至关重要。
对于结构化的字符串输入,其格式检查有两种常见的做法,其一是有限状态机,其二是正则表达式。由于有限状态机需要设计状态转移图,较为复杂,而正则表达式只需要了解输入的结构,因此,在本次作业中,我采用了正则表达式来匹配文本。这是我第一次的正则表达式:
1 private String charPattern="[[0-9]\\+\\-\\(\\)\\{\\}\\,]+"; 2 //private String numPattern="([0-9]+)"; 3 private static final String numPattern="\\([+-]?\\d{1,6}\\,([+-]?[0]{1,6}|[+]?\\d{1,6})\\)"; 4 private static final String parPattern=String.format("(%s(\\,%s){0,49})", numPattern,numPattern); 5 private static final String curPattern=String.format("([+-]{1}\\{%s\\}){1,20}?", parPattern); 6
其思路是先检查是否有非法字符,接着检查格式,程序针对不同的输入给出相应的响应。其中,格式检查这一步刚开始遇到了不小的问题。第一次匹配时,我采用的是直接整体匹配
Pattern.matches(curPattern,str)
这种方法的好处是可以直接否定掉不符合标准格式的非法输入,但是缺点也显而易见,程序无法给出确定的详细信息,即用户可能会一脸懵逼的无法得知输入错在哪里,显然是不友好的。并且这种匹配方法有一个重大缺陷:当待匹配字符串很大时,比如这次作业的压力测试,程序会CRASH,这应该是与JAVA正则匹配机制的影响。所以这种直接整体匹配的方式是无法胜任输入检查这一工作的。经过重新思考之后,我采用了以下方式:
对多项式进行一个一个的匹配,每匹配到一个多项式,便把它串接到临时字符串的末尾,等到匹配检查结束后,临时字符串中保留了输入字符串中所有正确的多项式输入。最后将临时字符串与输入字符串进行比较,如果两者相等,显然输入是合法的,否则,输入就是不合法的。
这样就完美解决了JAVA不能一次匹配较大文本的缺陷。事情到这里就完美解决了吗?显然没有,之前就说过,这种匹配虽然可以排除所有非法的格式输入,但是无法给出出错的具体信息,原因便是正则表达式太过具体,这就相当于一扇紧闭的门,一开始就把所有非法的请求拒之门外,门内的“人”是不可能得知具体的信息的。因此,经过重写之后的正则如下
1 //小括号模式 2 private String par_pattern="(\\([+-]?\\d{1,6},(\\+?\\d{1,6}|[+-]?0{1,6})\\))"; 3 //大括号模式 4 private String cur_pattern="(\\{.+?\\})";
很简单对不对,尤其是大括号模式,对于大括号模式而言,只检测成对的大括号,对于大括号里面的内容是什么并不关注,这一工作交由小括号模式去做,所有在第一阶段被漏掉的非法格式在第二阶段均被检测出来。这种二级匹配是可以做到具体到错误信息的,但相应的代码逻辑部分会大大增加,为了实现检测错误信息的功能,程序用到了分组的概念,以及匹配的起始位置,根据这些信息来对输入字符串里里外外的进行详细的检查,一旦遇到非法信息,程序打印出错误信息并退出。这种方式是我在互测阶段阅读一位大佬的代码时发现的,当时的我是无比赞叹的,所以自己也实现了一下,那种感觉很美妙,下面是部分代码。
1 while(cur_m.find() && cur_cnt<=20) 2 { 3 if(cur_m.start()!=cur_end) 4 expHander(cur_m.group(1).substring(cur_end, cur_m.start()-1)); 5 if(cur_m.end()<str.length() && ! ( str.charAt( cur_m.end() )=='+' || str.charAt( cur_m.end() )=='-' ) ) 6 expHander(str.substring(cur_m.end(),cur_m.end()+1)); 7 cur_cnt++; 8 cur_end=cur_m.end()+1; 9 par_end=1; 10 11 Matcher par_m=par.matcher(cur_m.group(1)); 12 while(par_m.find() && par_cnt<=50) 13 { 14 if(par_m.start()!=par_end) 15 expHander(cur_m.group(1).substring(par_end, par_m.start()-1)); 16 if(par_m.end()+1<cur_m.group(1).length() && !(cur_m.group(1).charAt(par_m.end()) ==',')) 17 expHander(cur_m.group(1).substring(par_m.end(),par_m.end()+1)); 18 par_cnt++; 19 par_end=par_m.end()+1; 20 21 } 22 if(par_end != cur_m.group(1).length() || par_cnt>50) 23 expHander(cur_m.group(1).substring(par_end-1)); 24 } 25 if((str.length()+1)!=cur_end || cur_cnt>20) 26 expHander(str.substring(cur_end-1));
第一次作业出现的唯一BUG就是没有考虑到空输入时程序的相应,从而导致程序CRASH,其实,只要增加一行catch便可以完美解决,不过总体来讲第一次编程体验还是挺美妙的,既熟悉了面向对象编程,同时也了解了JAVA语言,这种学习与实践的交互过程还是挺令人享受的。
下面附上程序类图以及代码统计
第一次作业的功能较为简单,类功能相对较为完整,在这里,主类PolyComputer的功能时实例化对象并执行相应的方法,InputHander类把输入及检查独立出来进行处理,PolyManager类则用来管理多项式类同时进行多项式的加减运算,Poly类则用来构造多项式对象并交由PolyManager类来管理,其中,InputHander类和PolyManager类参考了课件的推荐设计。
第一次作业的代码统计如下,可以看出,程序的“圈复杂度”较高,这与正则匹配是密不可分的,程序考虑的输入情况越复杂,相应的圈复杂度一般也越高。同时,对于Method Line of Code这一指标,PolyManager类较为复杂,可以将其功能限制一下,比如将多项式加减分配到一个多项式运算类中,这样,程序的功能就会被均衡的分配到各个类中,各个类协同完成多项式运算。
二、傻瓜电梯
如果说第一次作业是一次令人愉悦的编程体验,那么第二次作业便令人苦恼了。相比较第一次作业,第二次作业侧重于如何合理安排各个类的功能,避免出现两种极端情况:“上帝”类和“傻子”类。在开始编程时,由于对时间线的概念不清晰,虽然最后成功实现了作业要求,但是程序代码逻辑混乱,冗余代码过多,类功能分配不均,类功能不明确。这样的做法甚至导致我本人阅读代码时都需要停下来稍微思考下,理一下思路,然后才能接着阅读,是不是很荒唐,更不要说阅读代码的其他人了。在这里就不展示初次编程时的代码了。。。
本次作业的难点在于如何处理同质请求,我采用以下方法来解决。
楼梯类与电梯类维护“点灯”与“灭灯”,当拿到一条请求时,首先判断该请求是不是同质请求,如果此时对应楼层的“灯”正在亮,则表示是同质请求,否则执行该请求并“点灯”。
一部分代码如下。
1 //电梯方法 2 public void geton(int n,double sys_time) 3 { 4 Ele[n]=sys_time; 5 } 6 7 public double getoff(int n) 8 { 9 return Ele[n]; 10 } 11 //楼层方法 12 public void geton(int n,String dire,double sys_time) 13 { 14 if(dire.compareTo("UP")==0) 15 FloUp[n]=sys_time; 16 else 17 FloDown[n]=sys_time; 18 } 19 20 public double getoff(int n,String dire) 21 { 22 if(dire.compareTo("UP")==0) 23 return FloUp[n]; 24 else 25 return FloDown[n]; 26 }
参考课件的推荐设计,加上自己的思考,重写之后的程序新增一条时间线来模拟时间的流逝,楼梯类与电梯类重写了“点灯”和“灭灯”方法,同时对调度器类进行了简化,代码逻辑更加清晰,类功能分配较为均匀,多个类协作完成电梯的上行、下行,总体是比较合理的,关键是代码的可读性更高了。。。
附上类图与统计
主类ElevatorSys和InputHander类参考课件的设计,功能同前所述。Request类作为Floor和Elevator类的主类,用于维护输入请求的基本属性与方法,Floor和Elevator类在Request类的基础上,增加了独有的方法和属性,用于完善输入请求并和其他类进行交互。RequestList类则维护一个请求队列,具备队列的基本方法:入队和出队,同时支持获取指定元素。Scheduler类则用于调度电梯,处理请求队列,要么相应请求,要么判断同质请求,这里同质请求的判断综合了Floor类、Elevator类里的“灯”属性。
通过代码统计图可以看出,本次作业的类功能分配较为合理,基本能保证类之间有很好的交互性,因为每个类只维护自己的属性和方法,因此,它们需要为实现统一功能共同协作。
三、ALS电梯
本次作业是在第二次作业的基础上,增加了捎带功能,即在电梯前进的方向上,尽可能的捎带请求,从而缩短电梯的响应时间。初次尝试,完全是重写了之前的Scheduler类,换句话说,就是仅仅为了满足指导书要求为了继承而继承,功能基本由一个类单独完成,有一些类被闲置在旁。修改后的代码重载父类的方法,用于寻找可以捎带的请求,接着,由父类来寻找下一个未完成的请求。子类与父类协同进行请求的处理。
本次作业的难点在于如何寻找下一个捎带请求,我的做法如下:
模拟真实的电梯运行,即从当前楼层出发,一层一层向目标楼层前进,每上行或下行一层,遍历请求队列,如果待响应请求的请求时间小于当前的系统时间,则将该请求作为捎带请求处理,如此反复。
在这个过程中,同时寻找下一个主请求,并返回主请求在队列中的位置,用于下一轮的执行。
附上类图与统计
相比较第二次作业,本次作业新增了一个继承类ALS_Scheduler类,父类为SCheduler类。ALS_Scheduler类用于处理捎带请求,同时与父类共同寻找下一个主请求。其他类的功能与第二次作业基本相同。
ALS_Scheduler类的功能较为复杂,其实也可以理解,毕竟要从请求队列中动态的寻找捎带请求。其他的基本与第二次作业相同。
四、总结
在重写三次作业的代码时,我发现一个共性的问题是初次尝试基本能完成作业要求,但是逻辑混乱,代码冗余,类功能不明确,没有很好地自包性,而经过重新构思之后的代码显然是基本避免了之前的问题。因此,这也给了我关于如何写出质量更高的代码的启示:
在着手写代码之前构思清楚,或者在实现程序功能的基础上重写第二遍。
当然,第一种方法是高效的,但对我而言,大多数时候思路是在码代码的过程中逐渐被唤醒的,因此在现阶段我可能会更加倾向于第二种方法,不过会逐渐锻炼自己的逻辑能力,逐渐向第一种方法过渡。