软件构造系列学习笔记(3.2)————设计规约

设计规约

上一节关注了编程语言中的“数据类型”、 “变量”、“值”,尤其是mutable和 immutable的类型/值/引用。

本节转向“方法/函数/操作”如何定义—— 编程中的“动词”、规约。

目录:

  • Function / method in programming language
  • Specification: Programming for communication
  • Designing specifications
  • Summary

Functions & methods in programming languages

  • Method
public static void threeLines() {
    STATEMENTS;
}
public static void main(String[] arguments){ 
    System.out.println("Line 1"); 
    threeLines(); 
    System.out.println("Line 2"); 
}  
  • Parameters
[…] NAME (TYPE NAME, TYPE NAME) { 
    STATEMENTS 
}

To call:

NAME(arg1, arg2); 

参数类型是否匹配,在静态类型检查阶段完成。

  • Return Values
public static TYPE NAME() { 
    STATEMENTS; 
    return EXPRESSION; 
}

void means “no type”。
返回值类型是否匹配,也在静态类型检查阶段完成。

“方法”是程序的“积木”,可以被独立开发、测试、复用。
使用“方法”的客户端,无需了解方法内部具体如何工作—“抽象”。

完整的方法:
这里写图片描述


Specification: Programming for communication

Programming for communication

为什么要写出“假设”?第一:自己记不住;第二:别人不懂。

代码中蕴含的“设计决策”:给编译器读;注释形式的“设计决策”:给自己和别人读。

Specification and Contract (of a method)

规约是团队工作的关键。没规约,没法写程序;即使写出来,也不知道对错。

规约起到了契约的作用。代表着程序与客户端之间达成的一致。

Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守。

Why specifications:

  • 很多bug来自于双方之间的误解
  • 不写下来,那么不同开发者的理解就可能不同
  • 没有规约,难以定位错误

Advantages:

  • 精确的规约,有助于区分责任
  • 客户端无需阅读调用函数的代码,只需理解spec即可

An example of specification:
A method add() of a Java class BigInteger
这里写图片描述

可以看到,规约可以隔离“变化”,无需通知客户端 ;规约也可以提高代码效率;扮演“防火墙”角色 等一系列好处。

这里写图片描述

行为等价性(Behavioral equivalence)

行为等价性考虑的是在客户端视角下两个方法在某些情况下是否可以相互替换。
以下面两个函数为例:

static int findFirst(int[] arr, int val) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == val) return i;
    }
    return arr.length;
}
static int findLast(int[] arr, int val) {
    for (int i = arr.length - 1 ; i >= 0; i--) {
        if (arr[i] == val) return i;
    }
    return -1;
}

当val缺失时,findFirst返回arr的长度,findLast返回-1;
当val出现两次时,findFirst返回较低的索引,findLast返回较高的索引。
但是,当val恰好出现在数组的一个索引处时,这两个方法表现相同。
每当他们调用方法时,他们都会传入一个正好具有一个元素val的arr,对于这样的客户,这两种方法是一样的。

另外,我们可以根据规约判断是否行为等价。

static int findExactlyOne(int[] arr, int val)
  requires: val occurs exactly once in arr
  effects:  returns index i such that arr[i] = val

这两个函数符合这个规约,故它们等价。
Note:规约与具体实现无关,因此规范不应该讨论方法类的局部变量或方法类的私有字段。

Specification structure

一个方法的规约由几个子句组成:

  • Precondition:由关键字require表示
  • Postcondition:由关键字effects表示
  • Exceptional behavior:如果违反前置条件,它会做什么。

前置条件:对客户端的约束,在使用方法时必须满足的条件 。
后置条件:对开发者的约束,方法结束时必须满足的条件 。
契约:如果前置条件满足了,后置条件必须满足。前置条件不满足,则方法可做任何事情。

静态类型声明是一种规约,可据此进行静态类型检查static checking。
方法前的注释也是一种规约,但需人工判定其是否满足。

Specifications for mutating methods

除非在后置条件里声明过,否则方法内部不应该改变输入参数。
应尽量遵循此规则,尽量不设计 mutating的spec,否则就容易引发bugs。
程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数 。
尽量避免使用mutable的对象 。

Designing specifications

Classifying specifications

用于判断“哪个规约更好”:

  • 规约的确定性
  • 规约的陈述性
  • 规约的强度
Deterministic vs. underdetermined specs

确定的规约:给定一个满足precondition的输入,其输出是唯一的、明确的

static int findExactlyOne(int[] arr, int val)
  requires: val occurs exactly once in arr
  effects:  returns index i such that arr[i] = val

欠定的规约:同一个输入可以有多个输出

static int findOneOrMore,AnyIndex(int[] arr, int val)
  requires: val occurs in arr
  effects:  returns index i such that arr[i] = val

非确定的规约:同一个输入, 多次执行时得到的输出可能不同

Declarative vs. operational specs

操作式规约(Operational specs),例如:伪代码 。
声明式规约(Declarative specs):没有内部实现的描述,只有 “初-终”状态 。
声明式规约更有价值 。
Example of Operational specs:

static String join(String delimiter, String[] elements)
effects: returns the result of looping through elements and alternately appending an element and the delimiter

Example of Declarative specs:

static String join(String delimiter, String[] elements)
effects: returns concatenation of elements in order, with delimiter inserted between each pair of adjacent elements
Stronger vs. weaker specs

如何比较两个规约,以判断是否可以用一个规约替换另一个?
若规约的强度S2>=S1:

  • S2前置条件更弱
  • S2后置条件更强

就可以用S2替代S1。
spec变强:更放松的前置条件+更严格的后置条件。

Original spec:

static int findExactlyOne(int[] a, int val)
  requires: val occurs exactly once in a
  effects:  returns index i such that a[i] = val

A stronger spec:

static int findOneOrMore,AnyIndex(int[] a, int val)
  requires: val occurs at least once in a
  effects:  returns index i such that a[i] = val

A much stronger spec:

static int findOneOrMore,FirstIndex(int[] a, int val)
  requires: val occurs at least once in a
  effects:  returns lowest index i such that a[i] = val

越强的规约,意味着implementor的自由度和责任越重,而client的责任越轻。

Designing good specifications
  • 规约应该是内聚的:Spec描述的功能应单一、简单、易理解。
  • 规约应该是信息丰富的:不能让客户端产生理解的歧义。
  • 规约应该有足够的强度:需要满足客户端基本需求。
  • 规约的强度也不能太强:太强的spec,在很多特殊情况下难以达到。
  • 规约应该使用抽象类型:在规约里使用抽象类型,可 以给方法的实现体与客户端更大的自由度
Precondition or postcondition?
  • 对程序员来说,不写Precondition,就要在代码内部check;若代价太大, 在规约里加入precondition, 把责任交给client。
  • 对用户来说,客户端不喜欢太强的 precondition,不满足precondition的输入会导致失败。

惯用做法是:不限定太强precondition,而是在postcondition中抛出异常:输入不合法 。尽可能在错误的根源处fail,避免其大规模扩散。

衡量标准:检查参数合法性的代价多大?
归纳:是否使用前置条件取决于(1)check的代价;(2)方法的使用范围

  • 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client。
  • 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常。

Summary

  1. 规约作为程序实现者与其客户之间的关键防火墙。它使得单独开发成为可能:客户端可以自由地编写使用该过程的代码,而无需查看其源代码,并且实现者可以自由地编写实现该过程的代码,而不必知道它将如何使用。
  2. Safe from bugs :一个很好的规范清楚地记录了客户和实施者依赖的相互假设。 错误通常来自界面上的分歧,并且规范的存在会降低这一点。在您的规范中使用机器检查的语言功能,比如静态类型和异常,而不仅仅是可读的评论,可以更多地减少错误。
  3. Easy to understand :一个简短的规范比实现本身更容易理解,并且使其他人不必阅读代码。
  4. Ready for change :规范在代码的不同部分之间建立契约,允许这些部分独立更改,只要它们继续满足要求。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值