oo面向对象--规格化设计
oo面向对象--规格化设计
规格化设计与抽象
要了解规格化设计首先要了解抽象化的程序设计,两者是密不可分的。
抽象化(Abstraction)
抽象化是将数据与程序,用语义呈现他们的外观,但是隐藏起它的实现细节。
抽象的过程可以看做多对一的映射,让我们忽略个体信息,将不同事物当做相同的事物对待,能够减少程序的复杂度,使得程序员可以专注在处理少数重要的部分。
发展
近几年来,程序员对已经产生的抽象水平产生不满,甚至不满足于在高级语言程序中普遍实现的抽象水平,解决这个问题的方法在于创造“更高级的语言”,用相对固定的数据结构以及有很强功能的原始类型来实现,但是这种方法的缺陷在于嘉定编程语言的设计者在设计语言的时候将大多数用户需要的抽象设计引进设计语言中,包含如此众多的内置抽象的编程语言可能笨拙的很难使用。
另一种可取代的方法是设计一种语言机制,允许程序员在需要的时候构建自己的抽象方法。通常的机制是使用过程(procedure)。编程语言包含了两种重要的抽象方法:参数化抽象(abstraction by parameterization)和规格化抽象(abstraction by specification)。其中:
- 参数化抽象是用参数替换数据特征来进行抽象。这样能够归纳出模块,从而使其可以用于更多的情况。例如,可以定义一个排序抽象,既能够实现对实数数组的排序,又能够实现对整型数组的排序,或者甚至对数组类型这类一般结构都有用。
- 规格化抽象是将运行细节(即模块如何实现)抽象为用户所需求的行为(即模块做什么)。这是从具体实现中抽象出模块,需要的仅仅是模块的实现能够匹配我们所依赖的表述形式。
程序设计中,抽象类别包括下列4类:
1:过程抽象:能够引入一些新的操作;
2:数据抽象:能够引入新的数据对象类型;
3:反复运算抽象:能够反复运算遍历在集合中的元素,而不必显示如何获得元素的细节;
4:类型层次:能够从多个单独的数据类型中抽象成几组相关的类型。
规格
抽象用于将一个程序分解为多个模块,然而抽象是无形的,没有描述,我们就无法知道如何将它与其实现的抽象区分。规格描述了服务的提供者和用户之间的协定,提供者同意编写一个属于规格满足集的模块。
规格是对一个方法/类/程序的外部可感知行文的抽象表示,它的目的是为了定义抽象的行为。
如果一个实现提供了所描述的行为,这个实现就满足了一个规格。
规格的含义是满足其所有程序的集合,这个集合称为规格满足集。
作业中的规格bug
此处为自己统计的bug,由于报告的jsf个数有限,自己重新看了一次还是发现了许多bug和不规范的地方。
bug类别 | Requires不完整 | Modifies不完整 | Effects不完整 | Requires逻辑错误 | Modifies逻辑错误 | Effects逻辑错误 | Effects内容为实现算法 | 不符合JSF |
---|---|---|---|---|---|---|---|---|
个数 | 7 | 3 | 5 | 0 | 0 | 2 | 0 | 0 |
方法行数 | 3,5,5,13,22,20,4 | 5,7,7 | 20,30,15,17,15 | 30,15 |
BUG产生原因
1、有些方法或者类的JSF遗漏
比如自己写的Exception类里面忘记写JSF,一些信息存储的类里面的构造方法有时候也会忘记写,还是自己不够细心。
2、没有按照标准格式书写
Effects之后的内容应该为布尔表达式。像是代码中出现的(\result = str),这种里面的=应该写成==
3、程序里面一些传入对象的方法,对应的Requires有的没写。
有传入还是应该写明有Requires的要求,比如传入对象不等于null,或者对象的repOK 为ture之类。
4、某些过程会有附加的隐式输入,例如读入文件和在System.out写信息,这些也属于过程的输入
5、还是偷了很多懒。。
JSF不规范的写法及改进
有些例子前置条件和后置条件一起修改了。
1、前置与后置条件:
public int getDirection(Coordinate now, Coordinate next){
/**
* @REQUIRES: None;
* @MODIFIES: None;
* @EFFECTS: (next.getX()-now.getX()==1&&next.getY()-now.getY()==0)==>(\result = 0);
(next.getX()-now.getX() == -1 && next.getY()-now.getY()==0)==>(\result = 1);
(next.getX()-now.getX() == 0 && next.getY()-now.getY()==1)==>(\result = 2);
(next.getX()-now.getX()== 0 &&next.getY()-now.getY() == -1)==>(\result = 3);
* @THREAD_REQUIRES:
* @THREAD_EFFECTS:
*/
}
这里输入的两个坐标分别是当前坐标和下一步的坐标,这两个坐标需要满足的条件是在地图上是相邻的,由于可能出现道路突然断开的状况,所以两个坐标的连接状态未知,当时想着在代码里面处理了这种异常状况所以没有写requires,但是其实也能在requires中体现,然后在代码中处理这种情况,抛出异常或者返回等操作,如果不写的话也要加入两个坐标不为空,有输入最好要限制其内容,什么都不写不是一种好的习惯。
这里的effects书写也不规范,要用双等号。
修改版本:
public int getDirection(Coordinate now, Coordinate next){
/**
* @REQUIRES: now.getX()>=0 && now.getX() < 80
* && now.getY() >= 0 && now.getY() < 80 && next.getX()>=0
* && next.getX() < 80 && next.getY() >= 0 && next.getY() < 80
* && graph[now.getX()*80+now.getY()][next.getX()*80+next.getY()] == 1;
* @MODIFIES: None;
* @EFFECTS:
* (next.getX()-now.getX()==1&&next.getY()-now.getY()==0)==>(\result = 0);
* (next.getX()-now.getX() == -1 && next.getY()-now.getY()==0)==>(\result == 1);
* (next.getX()-now.getX() == 0 && next.getY()-now.getY()==1)==>(\result == 2);
* (next.getX()-now.getX() == 0 &&next.getY()-now.getY() == -1)==>(\result == 3);
*/
}
2、后置条件:
public void servingHandler() throws InterruptedException{
/**管理出租车服务状态
* @REQUIRES: None;
* @MODIFIES: this.newtime;this.state;this.coordinate;this.schedulerInfo;
* * this.info;
* @EFFECTS: 对本线程的状态和参数进行改变
* @THREAD_REQUIRES:
* @THREAD_EFFECTS: \locked(trackingInfo);
*/
}
这个方法实现的过程有些复杂,当时直接用自然语言瞎写了一下,没有规范化表述。不过在此函数中调用了其他函数,现在不知道如何将调用其他函数的effects写清楚。。
public void servingHandler() throws InterruptedException{
/**
* @REQUIRES: None;
* @MODIFIES:\this.newtime;\this.state;
* \this.coordinate;\this.schedulerInfo;\this.info;\System.out;
* InfoQueue;
* @EFFECTS:
* desMove(currentRequest.getDestination());
* this.credit.addAndGet(3);
* InfoQueue.contains(schedulerInfo);
* schedulerInfo.contains(info);
* System.out!=null;
* @THREAD_REQUIRES:None;
* @THREAD_EFFECTS: \locked(trackingInfo);
*/
}
3、前置条件和后置条件:
@Override
public void desMove(Coordinate destination) throws InterruptedException {
/**出租车找最短路径行驶,找最小流量的边
* @REQUIRES: (\all Coordinate a,b;map;graph[a][b] == 1)
* @MODIFIES: this.coordinate;this.Info;newtime;
* @EFFECTS: this.coordinate == destination;
* @THREAD_REQUIRES:
* @THREAD_EFFECTS:
*/
}
这个函数是移动出租车到目标点,requires没有限制destination的范围,后置条件写的过于简略,没有将移动过程写清楚。
@Override
public void desMove(Coordinate destination) throws InterruptedException {
/**出租车找最短路径行驶,找最小流量的边
* @REQUIRES: now.getX()>=0 && now.getX() < 80 && now.getY() >= 0
* && now.getY() < 80
* &&(\all Coordinate a,b;a.getX()>=0 && a.getX() < 80 && a.getY() >= 0 && a.getY() < 80 && b.getX()>=0 && b.getX() < 80 && b.getY() >= 0 && b.getY() < 80;graph[a.getX()*80+a.getY()][b.getX()*80+b.getY()] == 1)
* //保证地图连通
* @MODIFIES: this.coordinate;this.Info;newtime;
* @EFFECTS:pathway == TaxiGUI.getInstance().getShortestPath(destination))==>(\all Coordinate a;pathway;this.info.contains(a))
* && (\all int i,int j;j < pathway.size()&&i == j * 5;info.contains(i))
* && \this.coordinate == pathway.get(pathway.size()-1)
* && \this.newtime == newtime + waitime + 5*pathway.size()&&info.contains(newtime);
*/
}
4、前置条件和后置条件:
public static boolean judegeValid(Coordinate a, Coordinate b){
/**
* @REQUIRES: None;
* @MODIFIES: None;
* @EFFECTS: 判断是否符合坐标输入条件
*/
}
由于有输入参数,应该对参数范围加以规范和约束。
effects书写也不符合规范。
public static boolean judegeValid(Coordinate a, Coordinate b){
/**
* @REQUIRES: a instanceof Coordinate && b instanceof Coordinate && a! = null && b!=null;
* @MODIFIES: None;
* @EFFECTS:(a.isOutOfBound()||b.isOutOfBound()||a.equals(b))==> \result == false;
*!(a.isOutOfBound()||b.isOutOfBound()||a.equals(b))==> \result == true;
*/
}
5、前置条件和后置条件:
public void RoadStatusHandler(String cmd){
/**
* @REQUIRES:
* @MODIFIES: TaxiGUI;
* @EFFECTS: 改变道路开关;
*/
}
String cmd不能是null,虽然也在代码中有处理,effects没写清楚。。
public void RoadStatusHandler(String cmd){
/**
* @REQUIRES: str instanceof String;str!=null;
* @MODIFIES: TaxiGUI;System.out;
* @EFFECTS: !cmd.isValid()==>System.out == "#set Status Format Error";
* cmd.matches(this.m)&&cmd.isValid()&&m.group(5).equals("OPEN")==>TaxiGUI.getInstance().graph[m.group(1)*80+m.group(2)][m.group(3)*80+m.group(4)]==1;
* cmd.matches(this.m)&&cmd.isValid()&&m.group(5).equals("CLOSE")==>TaxiGUI.getInstance().graph[m.group(1)*80+m.group(2)][m.group(3)*80+m.group(4)]==0;
*/
}
6、前置条件和后置条件
public void RequestHandler(String cmd) throws RequestFormatErrorException,InterruptedException{
/**
* @REQUIRES:
* @MODIFIES: rcv;
* @EFFECTS: 请求读入
*/
}
没有写异常情况,requires不完整,effects不完整
public void RequestHandler(String cmd) throws RequestFormatErrorException,InterruptedException{
/**
* @REQUIRES:cmd instanceof String && cmd! = null;
* @MODIFIES: rcv;System.out;
* @EFFECTS: cmd.isValid()==>\this.rcv.contains(new PassengerRequest request);
* !cmd.isValid()==>e == exceptional_behavior(RequestFromatErrorException)&&System.out == e.getMessage();
*/
}
功能bug和规格bug在方法上的聚集关系
方法名 | 功能bug数目 | 规格bug数目 |
---|---|---|
public void input() | 2 | 1 |
public void SetRoadStatus(Coordinate a1,Coordiante a2) | 1 | 0 |
public void randomMove() | 1 | 1 |
我的功能bug主要在代码实现上面有问题,其实和规格的bug联系并没有很大。
规格设计思路及体会
老师在课上强调,要先写出规格再开始写代码。也就是要先抽象出这个类或者方法的功能,输入输出再进行代码的书写,写出来的内容要符合规格的叙述。所以构思抽象规格是放在首位的,关注的是行为,而不是实现行为的细节。
一个规格的语义包含三部分:需求(Requires),修改(Modifies),结果(Effects)。
1、requires:规格需要有足够的限制性来排除其抽象的用户不可接受的任何实现,这就需要在requires里面明确指出来。一个局部的过程规格总是包含了一个requires格式,有些时候编程应该检查requires中的约束条件,如果不满足就跑出一个异常。
2、modifies:列出了被修改的所有输入名称,做到不遗漏。
3、effects:所有为被requires格式排除的输入描述了过程的行为,必须定义产生了何种输出,同时必须定义被列在modifies格式中的输入做的修改所产生的结果。在本课程中需要用到规范化的表达,实际工程上也有用到简洁的自然语言表达,但是一定要表述清楚,并且符合逻辑。
4、线程类的requires和effects需要书写,但一般不要给使用者限制requires.
实际上因尽可能避免使用requires格式,因为如果输入产生了不满足requires的格式的时候,规格并没有给出相应的操作。所以设计抽象的时候要设计成满足最小限度的受限,对过程行为的细节仅仅做必要的限制,给实现者更多的自由,可以进行更加有效的编程。