OO第三次博客作业

OO第三次博客作业

规格化设计的发展历史

对于要求的规格化设计发展历史,我实在是没有找到对应的资料,但从程序设计方法的发展历史中可以看到规格化设计的身影,所以我就结合程序设计方法的发展历史,来说说规格化设计的发展历史。

由于计算机诞生之初,并没有编译器操作系统这一类东西,也没什么高级语言,那时的程序员只能使用机器语言来编写程序,后来为了方便程序员编写又出现了助记符也就是汇编语言。我们上学期的计组学习,也写了不少MIPS汇编语言程序,对汇编语言的各种特点也是深有体会。

  1. 灵活度极高。由于汇编语言没有固定的模式,一个极其简单的功能也可以有很多不同的实现方式。举个栗子:就一个简单的if/else语句,在高级语言中就是if/else,但是如果用汇编写,十个人能写出十个不同的版本出来。
  2. 可读性极差。很多同学在计组P2阶段都有过这样的体会,因为汇编程序没有变量名的说法,整个程序就是各种指令和寄存器翻来覆去,如果汇编程序不写注释,过上半天左右回来看之前写的代码,就几乎是不知所云了。
  3. 维护极困难。这其实是上面两点所导致的问题。上学期我们所写的汇编程序还非常简单,编写的最复杂汇编程序的也就是做了一个简单的递归操作而已,翻译成C代码也超不过50行。然而就是这样简单的代码,如果编写过程出现了bug,要想debug也是一件很费时费力的事情,很多同学P2挂了的原因就是因为没有修复程序的bug。并且一个写好的汇编程序如果想要改需求(手动滑稽),大概率只能重构了吧。

机器语言和汇编语言的这些特点也让当时的很多程序员头疼,于是就有一部分大牛决定开发新的,更友好的程序语言,面向过程的程序设计应运而生。面向过程的语言被认为是一种“高级语言”,相比面向机器的语言来说,面向过程的语言已经不再关注机器本身的操作指令、存储等方面,而是关注如何一步一步的解决具体的问题。虽然相比机器语言有了很大的提升,但是仍存在一些问题。就我们之前所编写的C语言程序来看,变量名不规范,整个程序就一个main函数从头写到尾的面条代码现象也十分常见,这样的代码从可读性和可维护性上讲不比机器语言好多少。

于是就有了下面的第一次软件危机,催生出了结构化设计。

随着计算机硬件的飞速发展,以及应用复杂度越来越高,软件规模越来越大,原有的程序开发方式已经越 来越不能满足需求了。1960 年代中期开始爆发了第一次软件危机,典型表现有软件质量低下、项目无法 如期完成、项目严重超支等,因为软件而导致的重大事故时有发生。例如 1963 年美国 (http://en.wikipedia.org/wiki/Mariner_1) 的水手一号火箭发射失败事故,就是因为一行 FORTRAN 代码 错误导致的。

软件危机最典型的例子莫过于 IBM 的 System/360 的操作系统开发。佛瑞德·布鲁克斯(Frederick P. Brooks, Jr.)作为项目主管,率领 2000 多个程序员夜以继日的工作,共计花费了 5000 人一年的工作量,写出将 近 100 万行的源码,总共投入 5 亿美元,是美国的“曼哈顿”原子弹计划投入的 1/4。尽管投入如此巨大, 但项目进度却一再延迟,软件质量也得不到保障。布鲁克斯后来基于这个项目经验而总结的《人月神话》 一书,成了史上最畅销的软件工程书籍。

为了解决问题,在 1968、1969 年连续召开两次著名的 NATO 会议,会议正式创造了“软件危机”一词, 并提出了针对性的解决方法“软件工程”。虽然“软件工程”提出之后也曾被视为软件领域的银弹,但后 来事实证明,软件工程同样无法解决软件危机。

差不多同一时间,“结构化程序设计”作为另外一种解决软件危机的方案被提出来了。 Edsger Dijkstra 于 1968 发表了著名的《GOTO 有害论》的论文,引起了长达数年的论战,并由此产生了结构化程序设计方 法。同时,第一个结构化的程序语言 Pascal 也在此时诞生,并迅速流行起来。

结构化程序设计的主要特点是抛弃 goto语句,采取“自顶向下、逐步细化、模块化”的指导思想。结构化程序设计本质上还是一种面向过程的设计思想,但通过“自顶向下、逐步细化、模块化”的方法,将软件的复杂度控制在一定范围内,从而从整体上降低了软件开发的复杂度。结构化程序方法成为了 1970 年 代软件开发的潮流。

科学研究证明,人脑存在人类短期记忆一般一次只能记住5-9个事物,这就是著名的7+-2原理。结构化程序设计是面向过程设计思想的一个改进,使得软件开发更加符合人类思维的 7+-2 特点。

结构化程序设计,一种编程范型。它采用子程序(函数就是一种子程序)、代码区块、for循环以及while循环等结构,来替换传统的goto。希望借此来改善计算机程序的明晰性、质量以及开发时间,并且避免写出面条式代码。然而结构化的程序设计,还是有其缺陷。当代码变得越来越复杂,并且需要软件自身不断扩展不断维护时,结构化的设计方法就有些力不从心了,因此面向对象的程序设计登上了历史舞台。

"分而治之"是处理大型问题的一般性原则,面向对象也是如此,面向对象的设计通过模块组织程序,将一个庞大的程序切割成很多较小的模块,通过接口来互相联系。但是如何进行分解成了主要难题。分解大程序一大利器是抽象,这也就引出了规格化抽象。规格化抽象是将执行细节(即模块如何实现)抽象为用户所需求的行为(即模块做什么)。这是从具体实现中抽象出模块,需要的仅仅是模块的实现能符合我们所依赖的表述形式。通过规格化抽象,可以把一个软件分成多个组件,使这些组件可以最终组合起来解决最初的问题。这些组件的开发由于有规格化的描述,开发较为容易,并且后期如果需要更新维护只需要对相应的组件进行更改即可,极大的简化了修改和维护的难度,对大型工程的开发有极大的帮助。因而受到程序员的热捧。

规格Bug分析

说实话,我对OO课程关于设计规格这一部分的教学方式不敢苟同,如果真的要实现规格化设计,不应该等到第一次出租车写完了之后才说要添加规格信息。而应该在第一次出租车作业之前就先有一次作业专门训练如何写规格。然后在写出租车时先用一次作业进行数据抽象,把每个类的规格和方法规格都写好,下一次作业集中实现。通过这样的方法,一方面与正常的大型工程的开发流程相适应,另一方面可以让同学们体会到规格的真正作用所在,如果规格写得好,相信可以做到像OO课上实验时那样,可以不考虑其他模块的具体实现,仅根据规格来写就不会出问题。

过程规格一方面是一个方法与其用户交互的契约,另一方面是对实现者做出的规约要求,但我们的课程实际上两点都没有实现。

一方面规格本身由于是布尔表达式可读性较差,对于不超过二三十行的方法,实际上读代码都比读规格来的直接,再加上由于数据封装大部分方法实际上都是对用户隐藏的,用户根本用不到,也就谈不上什么交互契约。

另一方面由于出租车作业主体早在第七作业就写完了,我们实际上是在根据代码写规格,谈不上对实现者的规约要求。

所以规格部分的三次作业,我认为较为合理的顺序应该是

  1. 规格写法集中训练(以较为简单的程序作为训练,锻炼先写规格再写代码,先规划再实现的流程)
  2. 出租车数据抽象及规格设计(集中进行出租车的数据抽象和规格设计,不关心具体代码的写法,前两次作业不需要互测,因为写的好的第三次作业将会顺水推舟,写的不好的,第三次作业相当于白手起家,在第三次作业自然会见分晓,这才是规格化设计的正确食用方法)
  3. 出租车作业的集中实现,根据之前所写的规格集中编写代码。

而我们目前的OO课程中,规格这一部分更像是在打补丁,而不是在做规划。

由于出租车的大部分代码早就在第一次出租车作业实现好了,之后是根据代码写规格,面对数百个方法,把之前写好的代码再写一遍规格,大部分都是重复性的工作,十分枯燥乏味,再加上互测阶段部分恶意扣分现象的发生,很多同学对规格产生厌恶心理,甚至打心底咒骂规格,一旦产生这种心理,实际上这门课程已经失败了,这也就是为什么规格这几次课程基本上课就没人听的主要原因。

规格化本身一个重要目的是为了方便实现,而在我们的课程中规格的主要目的是用来扣别人分。规格的一大目的是为了准确描述,而如果太过关注格式问题,紧盯着各种不妨碍实现者理解的细节问题不放,而忽略了规格本身的含义,就真的很让人唏嘘。

我在这几次作业中还算幸运,规格没有被发现太多问题。第一次和第三次作业没有被报规格问题,第二次作业因为一些<写成了<=,以及有几个变量名没有写对,被扣了两个jsf。

这些bug产生的原因,主要都是因为是根据代码来写规格,有一大部分工作是复制粘贴并且为了满足格式需要,在判断大小关系时要修改不等号方向,导致不等号写错,另一方面由于这几次作业出现的变量实在是太多,在写规格的过程中确实是没有太重视,导致变量名写错,本来应该判断x的范围却写成了判断y的范围,这个规格如果交给其他人来实现将会出现不可避免的错误,后果将会十分严重。

不好的写法和改进方案

  1. 下面这个就是导致我第三次程序出bug的那个使用Loadfile初始化出租车的方法的jsf,可以看到整个jsf写的十分简略,基本白话,而且几乎没有细节说明。
/**
     * @REQUIRES: scanner!=null&&taxis!=null&&output!=null;
     * @MODIFIES: scanner; taxis;
     * @EFFECTS:
     * initialize taxis;
     * (wrong format)  ==> exceptional_behavior (Exception)||exceptional_behavior (ExceptionInInitializerError);
     */

改进后的版本

/**
     * @REQUIRES: scanner!=null&&taxis!=null&&taxis.length==100&&output!=null;
     * @MODIFIES: scanner; taxis;
     * @EFFECTS:
     * (all int i; 0<=i<30; taxis[i] == new Taxi_VIP(i));
     * (all int i; 30<=i<100; taxis[i] == new Taxi_Normal(i));
     * (all int i; 0<=i<100; taxis[i] is mentioned in file ==>init the state, credit and location of taxis[i]);
     * (wrong file format) ==> exceptional_behavior (ExceptionInInitializerError);
     * (wrong taxi Information) ==> exceptional_behavior(Exception);
     */
  1. 下面的整个是红绿灯更新的方法,虽然看上去没有问题,但实际上其中的lightMap直接使用了gui自带的红绿灯二维数组guigv.lightmap赋值,虽然gui的这些静态变量被默认初始化了,但是还是应该写上
 /**
     * @REQUIRES: None;
     * @MODIFIES: lightMap;
     * @EFFECTS:
     * (all int i,j; 0 <=i<80, 0 <=j<80; (old(lightMap[i][j]==1) ==> lightMap[i][j]==2)&&(old(lightMap[i][j]==2) ==> lightMap[i][j]==1))
     */

改进后

/**
     * @REQUIRES: lightMap!=null&&lightMap.length==80&&(all int i; 0<=i<80; lightMap[i].length==80);
     * @MODIFIES: lightMap;
     * @EFFECTS:
     * (all int i,j; 0 <=i<80, 0 <=j<80; (old(lightMap[i][j]==1) ==> lightMap[i][j]==2)&&(old(lightMap[i][j]==2) ==> lightMap[i][j]==1))
     */
  1. 下面这个是更新出租车队列的方法,虽然看起来全是规规整整的表达式,但实际上并不标准,其中用到的isActivate方法是用来判断出租车是否应该在此次循环被唤醒。
/**
     * @REQUIRES: taxis!=null&&this.taxiMap!=null;
     * @MODIFIES: this.taxis; this.taxiMap;
     * @EFFECTS:
     * (all Taxi taxi in this.taxis; change state and location);
     * (all Taxi taxi in this.taxis; taxiMap[old(taxi.x)][old(taxi.y)].remove(taxi)&&taxiMap[taxi.x][taxi.y].add(taxi))
     * (exist Taxi taxi in this.taxis; taxi.move() failed)==>exceptional_behavior (Exception);
     */

改进后

/**
     * @REQUIRES: this.taxis!=null&&this.taxiMap!=null&&this.taxis.length==100&&this.taxiMap.length==80&&(all int i;0<=i<80;taxiMap[i].length==80);
     * @MODIFIES: this.taxis; this.taxiMap;
     * @EFFECTS:
     * (all Taxi taxi in this.taxis; this.taxi.isActivate ==> this.taxi.move());
     * (all Taxi taxi in this.taxis; taxiMap[old(taxi.x)][old(taxi.y)].contains(taxi)==false&&taxiMap[taxi.x][taxi.y].contains(taxi)==true)
     * (exist Taxi taxi in this.taxis; taxi.move() failed)==>exceptional_behavior (Exception);
     */
  1. 下面这个是更新一辆出租车的状态的方法,可以看到写的也十分简略。
    /**
     * @REQUIRES: None;
     * @MODIFIES: this;
     * @EFFECTS:
     * change state and stateTime;
     * (can't find a way to destination) ==> exceptional_behavior (Exception);
     * @THREAD_EFFECTS: locked(this);
     */

改进后

/**
     * @REQUIRES: this.stateTime!=null&&(this.state.equals(State.ORDER)||this.state.equals(State.SERVICE))==>this.destination!=null;
     * @MODIFIES: this;
     * @EFFECTS:
     * (old(this.state).equals(State.WAIT)&&old(this.stateTime==20000))==>(this.stateTime=0&&this.state.equals(State.STILL));
     * (old(this.state).equals(State.WAIT)&&old(this.stateTime=!20000)&&this.destination!=null)==>(this.stateTime=0&&this.state.equals(State.ORDER));
     * (old(this.state).equals(State.STILL)&&old(this.stateTime==1000)&&this.mainRequest==null)==>(this.stateTime=0&&this.state.equals(State.WAIT));
     * (old(this.state).equals(State.STILL)&&old(this.stateTime==1000)&&this.mainRequest!=null)==>(this.stateTime=0&&this.state.equals(State.SERVICE)&&this.executeMainRequest());
     * (old(this.state).equals(State.ORDER)&&this.destination.equal(this.x, this.y))==>(this.state.equals(State.STILL)&&print information);
     * (old(this.state).equals(State.SERVICE)&&this.destination.equal(this.x, this.y))==>(this.state.equals(State.STILL)&&print information&&this.requestOver());
     * (can't find a way to destination) ==> exceptional_behavior (Exception);
     * @THREAD_EFFECTS: locked(this);
     */
  1. 下面这个是新增路径流量的方法,在增加流量的同时删除时间过久的流量信息,但同样写的太过简略
/**
     * @REQUIRES: 0 <= src <6400&&0 <=dst <6400&&abs(time-System.currentTimeMillis())<=100;
     * @MODIFIES: this.flowMap;
     * @EFFECTS:
     * put FlowNode into according queue of getKey(src, dst) and delete FlowNodes which are too old;
     */

改进

/**
     * @REQUIRES: 0 <= src <6400&&0 <=dst <6400&&abs(time-System.currentTimeMillis())<=100&&this.flowMap!=null&&flow>0;
     * @MODIFIES: this.flowMap;
     * @EFFECTS:
     * this.graph[src][dst]==0 ==> return;
     * flowMap.get(getKey(src,dst))==null ==> flowMap.get(getKey(src,dst)).contains(new FlowNode(flow,time)); 
     * (all FlowNode flowNode in old(flowMap).get(getKey(src,dst));flowNode.time<time-600 ==> flowMap.get(getKey(src,dst)).contains(flowNode)==false);
     */

基本思路和体会

说到设计规格,实际上这几次作业,基本上都是写好了代码,再回过头来补规格,谈不上什么基本思路和体会。在此只能说一下我是怎么撰写规格的,相信有大量的同学和我的方法是一样的,就做一个反面教材吧。

由于每次都是对已经写好的方法来撰写规格,requires部分只需要看传进来的参数和该方法内用到的非局部变量,看这些参数的和变量是否为空,是否在规定范围内。modified更是只需要查看非局部变量是否有更改,是否对文件和标准输入输出有操作。对于effects部分,能用布尔表达式描述的用布尔表达式描述,实在描述不了的用自然语言描述,只需要关注 esult,以及对非局部变量所做的变动,是否有抛出异常。

这样的写法经过这几次作业确实是没什么收获,就是复习了一下离散一,了解了一些格式,至于规格到底有什么用,大概就是给测试者看吧,顺便消磨一些本来就不怎么充裕的时间,毕竟自己写的程序读代码都比读规格来得快,作为测试者看规格实际上也就是看看规格是不是和这个方法一致,如果真的想要发现bug,还得自己花大功夫测试。我不否认这几次作业的功能性bug数与规格bug数量确实是正相关,可其中又有多少是由于规格写的不好导致功能出了问题呢?大部分其实是功能不对,规格只是顺着错误的功能进行描述,或是功能正确但规格没写对。

总而言之,这几次作业作业给我的感受就是“鸡肋”,典型的费时费力还收获极少。有这些时间我还不如好好去看看推荐的多线程和设计模式的那两本书,相信比我在这对着写好的代码写规格收获多得多,也不用每次被互测阶段的各种可能遭遇所困扰。

心得体会

最后一次写代码的作业已经结束了,剩下的不知道是些什么,其实我也不太关心了,这十几周已经锻炼出了一颗佛系的心。经过这十二周的折磨,我的代码风格有了提升,对面向对象有了更深的理解,只不过代价大了些。大部分时间都被作业占用了,没有多少时间总结,也没有多少时间去学习新的知识。我买了推荐的那两本书,自己也研读了其中的部分章节,确实受益匪浅,其内容之广之深确实是几节课根本无法说清楚的,而我们的OO课程却做到了一节课讲小半本书的速度,大部分细节被忽略,我个人能力有限,自学速度实在是跟不上,多线程那本书才看了三章就只能抛下换另一本。大家都在吐槽这门课有多么糟糕,在我看来最糟糕的部分在于,在同学们还没有什么知识经验积累情况下强制推行超过同学们当时水平范围的要求,本身课程容量极大希望同学们能够在课下完成自学,但同时又布置大量的作业占用课下时间(要知道这学期不只有OO还有OS,OS也不是一个成天浪一浪就能学好的课程),没有什么知识经验基础,靠着这些作业来积累经验,就仿佛是在重新发明轮子,难度大还吃力不讨好。

现在回过头来看我写过的9次编程作业,我花这么多时间在这门课上到底值不值,我目前的回答是不值。这学期开学时我做了不少的计划,有不少的愿景,现在看来都是虚无缥缈罢了,这确实和我这学期的决策有关系,想把所有事情都做好的结果往往是什么事都做不好,这也算是我这学期最大的收获吧,虽然过去早就明白这个道理,但从未如此刻骨铭心。说的有点多也有点远,以上纯属个人看法。最后还是很感谢经常一起讨论的各位大佬,感谢助教在群里不厌其烦的回答,以及老师为构思这门课程做出的努力。

原文地址:https://www.cnblogs.com/panxuchen/p/9093162.html