OO博客作业3:第9-11周作业总结

一、总结介绍规格化设计的大致发展历史和为什么得到了人们的重视

1、规格化设计的大致发展历史

规格化设计,又称契约式设计,最早由Bertrand Meyer于1986年提出,出自于《面向对象软件构造》。其基础是形式验证、形式规格和Hoare logic。

2、规格化设计得到重视的原因

在大型软件工程开发中,协同工作能力的重要性日益凸显。“对于一个大型系统来说,光保证它的各组成成分的质量是不够的。而最有价值的是确保在任何两个组成部分的交接处设计明晰的彼此义务和权利规范,即所谓契约。”(《面向对象软件构造》)

Bertrand Meyer曾在访谈中提到:“对于软件和软件设计者来说,为了保证各方面的正确性和健壮性,他们就必须懂得通讯的准确约束规范。在这个地方,我们就将商业中的契约概念应用到软件中。……商业中,我们会通过契约——对我们期望的彼此义务和权利的准确表述——的方式彼此协调。对于软件来说,我们撰写客户和服务例程时,都必须严格遵循同样的契约表述。”

《契约式设计的收益》一文中将规格化设计的优点概括为以下5方面:获得更优秀的设计、得到更出色的文档、支持复用、提高可靠性、帮助调试。

二、表格分析规格bug

由于没有被发现规格bug,此环节略过。

三、分析自己规格bug的产生原因

由于没有被发现规格bug,此环节略过。

四、分别列举5个前置条件和5个后置条件的不好写法,并给出改进写法

1、前置条件的不好写法

1-1:方法将形如”(x,y)”或”x,y”的字符串转化成一个坐标点对象,原写法:

(\exist String in; in == str1 + "," + str2);

更好的写法是:

in == String.format(“(%d,%d)”, x, y) || in == String.format(“%d,%d”, x, y) ;

0 <= x < 80; 0 <= y < 80;

明确指出格式具体是个什么样子。

1-2:普通出租车的构造方法。原写法:

int i, SafeFile fp, TaxiGUI g;

更好的写法:

\exist int i; 0 <= i < 100;

\exist SafeFile fp; fp != null;

\exist TaxiGUI g; g != null;

方法实际上没有对输入进行检查,因此前置条件必须有更严格的约束。

1-3:普通出租车的run()方法。原写法:

(\exist int status; 0<=status<=3);

更好的写法:

none;

不变式的内容是默认成立的,没必要写在这里。

1-4:这个方法写出来完全没有意义:

/**

* @REQUIRES:(\exist int i; 0<=i<6400);

* @MODIFIES:none;

* @EFFECTS:(the point that i represents is in this city)==>(\result == true);

* (the point that i represents is not in this city)==>(\result == true);

*/

public boolean incity(int i) {

int x = i / 80;

int y = i % 80;

return (x >= 0 && x < 80 && y >= 0 && y < 80);

}

如果认真检查,会发现前置条件满足时,即使什么都不做,后置条件也满足。这部分代码一定是神志不清时写出来的。

2、后置条件的不好写法

2-1:方法传入一个点坐标,判断当前对象代表的坐标是否位于传入坐标周围的4*4区域内,即是否需要发送给该出租车。原写法:

(\result == (src.within()))

更好的写法:

\result == (src.x – 2 <= x <= src.x + 2 && src.y – 2 <= y <= src.y + 2)

这种简单的布尔类型判断方法在写后置条件时难免照抄代码原文。从工程角度,这样的方法不写规格也无大碍,但是作为作业训练,还是要写。

2-2:重写了点坐标类的toString方法。原写法:

(\exist String re; re == "(x,y)"); (\result == re);

更好的写法:

(\result == ret)==>(ret == “(” + x + “,” + y + “)”);

对后置条件的理解出现偏差。

2-3:针对控制台输入的格式检查。原写法:

(\exists byte op);

* (in.matches(req))==>(op == 1);

* (in.matches(load))==>(op == 2);

* (in.matches(road))==>(op == 3);

* (in == end)==>(op == 4);

* else (op == -1);

* (\result == op);

更好的写法:

(\result == 0)==>(in == “END”);

(\result == 1)==>(in.matches(req));

(\result == 2)==>(in.matches(load));

(\result == 3)==>(in.matches(road));

(\result == -1)==>otherwise;

首先,JSF应该避免使用中间变量。原来的写法暴露了实现细节。

其次,更推荐将\result作为蕴含式的前件。

最后,所谓“更好的写法”应该避免使用otherwise。这里保留是考虑程序的可扩展性,即新增控制台指令。然而,这就要求程序员必须头脑清醒,明确otherwise何时被触发。

2-4:向队列加入一条请求。原写法:

(queue.length == \old(queue)+1);

(queue[length-1] == req);

更好的写法:

(queue.size == \old(queue).size + 1) && (queue.contains(req));

原文第二行属于暴露细节的写法。出现相似问题的还有从队列取出请求的方法。

2-5:SafeFile类的构造方法。原写法:

(!fname.valid())==>(throw Exception);

(fname.valid)==>(create the file and its writer);

更好的写法:

(fname.valid)==>(this.file == File(fname));

( ! fname.valid)==>(exceptional_behavior(IOException));

原文写法没有遵循JSF语法。

五、按照作业分析被报的功能bug与规格bug在方法上的聚集关系

第九次作业:道路临时开关、道路流量

没有被发现任何bug

第十次作业:红绿灯系统

BUG10-1:出租车crash

奇怪,这个BUG无法复现。

第十一次作业:可追踪出租车

BUG11-1:出租车回头问题,流量处理不当

相关方法:public void run_edge()

方法规格没被报告bug,但是规格是拿自然语言表述的。可见自己在一个方法里面加入了过多功能,代码行数达到50行。

BUG11-2:远距离出租车接客

相关方法:

public int dispatch(Request r)

public boolean receive(Request r)

public boolean within(_Point src)

方法dispatch的规格经自己检查与实际方法代码有出入,可见自己没有按照规格编写方法。自己对于“什么样的出租车可以接单”这个问题在规格设计时就没想清楚,写出来的代码自然是有漏洞的。

BUG11-3:出租车寻找最短路径时数组越界

相关方法:public void search(int dst)

规格写得太简略,而且逻辑有问题。下一个点必须在城市地图内。这一点不但写规格时没写,而且写代码时也没注意,不出问题才怪。

BUG11-4:不同时的相同位置请求冲突。

相关方法:public void search(int dst)

不完全是这个方法的问题。没有认真读下发的gui.java代码,使用里面的函数时没有仔细分析其实现逻辑。

六、归纳自己在设计规格和撰写规格时的基本思路和体会

1、针对方法规格

方法规格主要包含REQUIRES, MODIFIES, EFFECTS三部分。

REQUIRES即前置条件,是调用者传入参数等方面所必须满足的约定。如果需要传对象或容器,一般都会要求对象不为空,容器有元素。此外,我还会思考数据具有的实际含义。运行在一个城市的出租车系统,传入的坐标点显然不能超出城市范围,出租车的位置和状态必须有效,等等。不做格式检查的输入提取,也要在前置条件写清楚输入格式。

MODIFIES写方法会改变的数据。这里的数据应该理解为用户(或使用你代码的程序员)关心的数据,因此局部变量的改变大可不必写在这里。类属性的改变也没有必要全写,只需要填写呈现给用户看的那部分数据的改变。

EFFECTS即后置条件,是方法执行完毕返回结果、类属性等数据所满足的约定。初学者(包括我)往往容易将部分算法逻辑展示在里面,但这是不可取的行为。实际上,EFFECTS向方法调用者屏蔽了部分细节,进行了一层抽象。调用者和用户只关心方法执行后的效果、返回值满足的条件,并不关心值是如何得到的。另外,EFFECTS内容为实现算法也往往暗示着作者是在方法完成之后再补充的规格。根据Head First Java的讲法,正确的顺序首先是写伪代码和测试代码,即首先想清楚方法的执行效果,然后才是具体实现(写正常代码)。本末倒置往往是这类问题的深层原因。

初学者写面向对象程序,容易出现所谓“面条代码”的问题,将所有业务逻辑放在一个方法里面。我自己提炼出一个规律:方法的代码超过了50行,EFFECTS部分往往也超过5行。如果在写规格时感到写起来非常困难(不知如何概括),或是写了太多(超过5行),往往方法的功能还能进行分拆。因此,先写EFFECTS的另一个好处是倒逼程序员对业务逻辑进行拆分整合,避免“面条代码”。

2、针对类规格

类规格的核心是编写OVERVIEW,对于子类可以编写INHERIT,不变式也可以写在INVARIANT部分。

OVERVIEW阐明类的目标或整体轮廓。OO程序的类大体可以分为以方法为核心和以数据为核心两种。前者的OVERVIEW侧重类的目标:提供什么方法、如何处理数据。后者的OVERVIEW侧重整体轮廓:管理什么数据。对于后者,应该避免我互测环节拿到的将所有属性列举出来的错误。类需要对用户进行一层抽象,给用户看到的应该是他关心的、抽象过后的数据。把所有细节展示给用户实则是不安全的行为,代码可维护性也不好。就拿普通出租车而言,用户关心的顶多是出租车编号、当前状态、当前位置。其它一些标记抢单之类的变量没必要让用户知道和看到。

INVARIANT即表示不变式,是判定对象是否有效的布尔表达式,对应boolean repOK()方法。在实现其它方法前写出表示不变式和repOK方法,可以倒逼设计者思考类所构造的对象的实际意义,在编写方法过程中绷紧一根弦,确保方法调用前后对象的有效性。写不变式时应抓住程序的实际应用场景,以指导书规定和尝试辅助判断。

3、杂感

首先谈谈自己出现的问题:越来越懒得做自我测试。用来处理测试文件输入的那个类竟然一组样例也没跑过,最后肯定是崩。尽管自己可以找冯如杯比赛忙、操作系统难写之类的理由,但是测试者从来不会心慈手软。这也不是将来走上工作岗位应该有的态度。自己编写测试用例时,也不能对自己太好。为了测试多线程的正确性,两条请求之间间隔的时间不能太长,否则很多潜在的问题查不出来。(例如BUG11-2)总之,写完代码之后要多进行自我测试,写出来的程序错误才更少。

其次谈谈普遍出现的问题:要求用JSF写规格后,很多人没有把它当成训练面向对象思维的途径,而当成了增加扣分收入的法宝。他们对拿到的互测代码大量地报告规格BUG,其中很多BUG未必真实。这实际上曲解了老师的本意,浪费了提升自己能力的机会,得不偿失。一方面,希望这些学生端正学习态度,切实锻炼自身能力,而不是追求一个外在的分数。另一方面,希望教学组增加对乱扣分行为的检查和惩处力度。

总之,编写规格的质量不要仅依赖测试者的评价,要多结合课件、文档说明,找自己的不足并改进。

上一篇:第四次团队作业——项目Alpha版本发布


下一篇:JQuery官方学习资料(译):$ vs $()