BUAA-OO-2022Spring Unit3 总结

BUAA OO 2022 Unit3 总结

张凯歌 20373067

自测过程

在本单元中,我依旧和同学相互合作,有同学负责进行数据生成,我负责对拍程序撰写。然后每次作业之后,都进行自动化测试。这个单元同前几个单元一样,黑盒测试不能完全有效的覆盖所有情况,需要针对 JML 中的描述针对性的构造特殊样例,保证测试的完全覆盖。

课下测试

JUnit

JUnit中有两个基本对象,一个是TestCase;一个是TestSuite。TestCase可以为测试提供一组方法。比如可以使用setup方法在每项测试开始前建立所需环境,可以用teardown方法销毁测试之后的环境。其他方法可以提供诸如检查变量是否为null、比较变量以及捕捉异常的功能。创建测试的时候只需要继承TestCase类,并且按照需要编写自己的测试方法即可。这部分甚至可以在IDEA的插件帮助下快速完成。因此可以节省大量事件用在编写测试用例上面。一个测试的例子可以详见附录A相关内容。TestSuite是由几个TestCase或者其他TestSuite组成的,通过使用TestSuite我们可以创建测试的一个树形结构。JUnit比较易用、简便。重要的是可以收集很多的Junit测试保证测试回归性。但是正如学长学姐们在讨论区的说明,我确实发现了JUnit的如下问题:

  • JUnit的测试需要基于对于JML完全理解的基础上

    比如这次JML中有限制群组上限为1111人,getReceivedMessages返回最近的4条消息等。这些条件都是不太容易发现的,如果对于JML阅读不仔细,那么这些条件就可能无法被测试到,从而使用JUnit达不到预期效果

  • 需要提前构造好数据

    构造数据需要在编写代码之前构造。之前没有建立起这样的习惯。

课下强测

这一单元的测试和前两个单元不太一样。这个单元的测试比较固定,确定输入就可以确定输出,所以在测试上可以无脑对拍。在本单元中,我和wxg、ghy、lsz、qs同学一起完成了测试程序的编写和自动化测试部分。其中数据生成部分由wxg同学编写。在数据生成器的编写上,采用了常量池和随机数的经典做法。常量池保证了边界数据范围、随机数保证了测试数据覆盖性。数据生成器中的函数对应着一条指令。数据生成的过程,本质上是根据随机数调用函数产生指令的过程。在此基础上,我们可以编写相应的代码,生成完全图、单组的图等等特殊数据,确保数据强度,和程序鲁棒性。

边界数据

边界数据的构造一般是通过阅读JML和对于随机生成的数据进行手动重新构造完成的。第一点,就像刚刚提到的,随机生成的数据可能不能很好的保证覆盖到JML规定的所有行为,比如1111人和getReceivedMessages。此时就需要根据JML进行手动构造。第二点,根据随机生成的数据进行手动重新构造也是一种边界数据产生的途径。这种方法常见于前两个单元。随机数据的生成中对于某些侧重点的指令,比如qgvs,在测试的过程中可能就会出现TLE或者WA的问题,此时就可以通过分析随机数据生成的结果,手动构造针对性更强,强度更高的数据。

互测构造

在三次互测中,我表现的比较佛。一方面是JML规定了实现的方式,所以大家在实现的过程中都大同小异。当我在阅读他人的代码的时候,很难发现他人的问题。另一方面是卡人的数据都是我们课下造好了的,所以可以在互测的过程中直接使用。

图模型构建和维护策略

作业中的结构

作业的背景是一个社交网络,但是如果仔细阅读JML会发现,整个系统中person就是一个个节点,relation是点之间的边。而在Network中的各个方法都是维护这个图的方法。比如addRealtion就是在点之间加边;isCircle就是检查两个点之间的连通性;queryBlockSum就是查询这个图中连通块个数;queryLeastConnection是求某个点组成的最小生成树;sendIndirectMessage求出两个点之间最短路等等。

算法总结

并查集

并查集是一种树形的数据结构,用于处理一些不相交集合的合并和查询问题。并查集通常包含两种方法,分别是查找和合并。查找是查询两个元素是否在同一个集合中;合并是将两个不相交的集合合并成一个集合。在作业中,可以将person作为一个个点,然后relation作为边,将所有认识的人看成一个集合。这样查询两个人是否认识,就是查询两个人是否在一个集合中;而addRelation中添加两个人的认识关系,就是将AB两个人分别所在的集合合并起来,这正好对应了并查集中的查找和合并操作。使用并查集后,可以简化queryBlockSum和isCircle的查询速度。具体的实现方法就是采用”代表元“法,就是每个集合选择一个固定的元素作为整个集合的代表元素。

这里给出一种经典的C++语言实现并查集的方法,可以作为Java语言实现参考:

// 1. 并查集的存储
int fa[SIZE];
// 2. 并查集的初始化
// 假设有 n 个元素,起初所有元素各自构成一个独立的集合,即有n棵1个点的树。
for (int i = 1; i <= n; i++) fa[i] = i;
// 3. 并查集的 Get 操作
// 若x是树根,则x就是集合的代表,否则递归访问fa[x]直到根节点
int get(int x) {
   if (x == fa[x]) return x;
   return fa[x] = get(fa[x]);
}
// 4. 并查集的 Merge 操作
// 合并元素x和元素y所在的集合,等价于让x的树根作为y的树根的子节点
void merge(int x, int y) {
   fa[get(x)] = get(y);
}

并查集的算法中还有按秩合并等优化方法,但是经过实测优化程度不大。所以有兴趣自己学吧。

克鲁斯卡尔算法(最小生成树)

queryLeastConnection这个函数是求某个点可达的所有点组成的最小生成树。在这个方面有prim算法和kruskal算法可选。最后我选择了kruskal算法,因为我维护了边的序列而且实现了判断两个两个点是否联通的简单方法。这个算法的思想是将所有边按照权重大小排序,从小到大选择依次选择每条边,如果这条边加入最小生成树的答案中没有使当前图出现回路即可。如果这个点所在的连通集有n个点,则根据定义,他的最小生成树一定有n-1条边。优化方法:判断加边后是否出现回路——维护一个并查集,并查集表示和某个点联通的所有点集,只要待加入的边两端的点不在同一个点集内即可;边按照权重排序——维护一个有序的边序列,这部分可以使用二分法找到合适的位置插入。这里给出一个C++经典实现

struct rec {int x, y, z;} edge[MAX];
bool operator <(rec a, rec b) {
   return a.z < b.z;
}
int kruskal() {
   int ans = 0;
   sort(edge + 1, edge + m + 1);
   for (int i = 1; i <= n; i++) fa[i] = i;
   for (int i = 1; i <= m; i++) {
       int x = get(edge[i].x);
       int y = get(edge[i].y);
       if (x == y) continue;
       fa[x] = y;
       ans += edge[i].z;
  }
   return ans;
}

迪杰斯特拉算法(最短路)

sendIndirectMessage这个方法所求的是两个点之间的最短路。求单源最短路这个问题的模型是,给定一张有向图,(x,y,z)表示从点x到点y长度为z的有向边。这个问题中经典的方法有Dijkstra方法,这是一个贪心算法,它适用于所有边都是非负数的图。当边长z都是非负数时,全局最小值不可能被其他点更新,故选出的最短的节点x必然满足:dist[x]已经是起点到x的最短路,不断选择最短路就能对全局所有点更新最短路。这里由于求两个特定点之间的最短路,因此可以使用剪枝方法:如果当前找到的点就是id2,当即退出。这里给出一个经典的C++算法

priority_queue<pair<int, int>> q;
void dijkstra() {
   memset(d, 0x3f, sizeof(d));
   memset(v, 0, sizeof(v));
   d[1] = 0;
   q.push(make_pair(0, 1));
   while (q.size()) {
       int x = q.top().second; q.pop();
       if (v[x]) continue;
       v[x] = 1;
       for (int i = head[x]; i; i = next[i]) {
           int y = ver[i], z = edge[i];
           if (d[y] > d[x] + z) {
               d[y] = d[x] + z;
               q.push(make_pair(-d[y], y));
          }
      }
  }
}

这里使用了大根堆,并且插入负数变成小根堆。Java中含有堆容器,但是这里可以使用二分法就如上面一样,无非就是维护一个有序的集合。

性能问题和修复策略

容器使用

JML之提供一个参考的数据存储容器,可以使用ArrayList,也可以根据实际需要选择不同的容器。由于每个人的id不同,我们可以选择HashMap存储每个人的id到Person的关系,来达到快速访问。

Network

private final HashMap<Integer, Person> people;
private final HashMap<Integer, Group> groups;
private int qbsAns = 0;
//private final ArrayList<Message> messages;
private final HashMap<Integer, Message> messages;
//private HashMap<Person, Person> fathers = new HashMap<>();
private final HashMap<Integer, Person> fathers = new HashMap<>();
private final ArrayList<Edge> edges = new ArrayList<>();
private final HashMap<Integer, Integer> emojiMessages = new HashMap<>();
private final HashMap<Integer, Integer> dist = new HashMap<>();

Group

private final int id;
private final HashMap<Integer, Person> people;
private int valueSum = 0;
private int ageSum = 0;

Person

private final int id;
private final String name;
private final int age;
private int socialValue;
private int money;
private final HashMap<Integer, Person> acquaintance;  // id - person
private final HashMap<Person, Integer> value; // person - value
private final ArrayList<Message> messages = new ArrayList<>();

代码编写和维护

在代码编写的时候随时维护答案,维护边集有序、维护并查集

例如qbs指令查询当前有多少个联通块,这样可以在,addPerson的时候、addRelation的时候更新qbs的值;

边集有序维护的方法:

使用二分法,找到新边的合适位置,在插入进去,用时O(logn + n)

public static void addEdge(ArrayList<Edge> arrayList, Edge edge) {
   int l = 0;
   int r = arrayList.size();
   int mid;
   int value = edge.getValue();
   while (l < r) {
       mid = (l + r) / 2;
       if (value > arrayList.get(mid).getValue()) {
           l = mid + 1;
      } else {
           r = mid;
      }
  }
   arrayList.add(l, edge);
}

并查集维护方法:

//private Person findFather(HashMap<Person, Person> fa, Person person) {
private Person findFather(HashMap<Integer, Person> fa, Person person) {
   if (fa.get(person.getId()) == person) {
       return person;
  } else {
       Person father = findFather(fa, fa.get(person.getId()));
       //fa.put(person, father);
       fa.put(person.getId(), father);
       return father;
  }
}

Network 扩展

扩展要求

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

扩展方法

框架:Advertiser、Producer和Customer继承自Person;增加Advertisement和Product,继承自Message;Customer中增加偏好,偏好用一个32位的数表示,每一位代表一种爱好;Product中同样包含着一个32位的int类型的数,表示产品的具有的属性。当产品的属性和顾客的偏好有交集时,顾客会购买产品。

增加异常:

方法1:Advertiser发送广告

/*
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser);
@ ensures getPerson(id).advertisements.length == \old(getPerson(id).advertisements.length) - 1;
@ ensures (\forall int i; 0 <= i && i < people.length; (getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length) + 1 && people[i].messages[0] == \old(getPerson(id).advertisements[0]) && (\forall int j; 1 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j - 1]))));
@ ensures (\forall int i; 0 <= i && i < people.length; !(getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length && (\forall int j; 0 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j]))));
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) (\forall int i; 0 <= i && i < people.length; people[i].getId() != id || (people[i].getId() == id && !people[i] instanceof Advertiser));
*/
public void sendAdvertisement(int id) throws PersonIdNotFoundException;

方法2:查询销量

/*
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer);
@ requires (\exists int i; 0 <= i && i < getPerson(id).products.length; getPerson(id).products[i].getId() == productId);
@ \results == getPerson(id).getProduct(productId).money * (\sum int i; 0 <= i && i < people.length; (\exists int j; 0 <= j && j < people[i].products.length; prople[i].products[j].equals(getPerson(id).getProduct(productId))));
@ public exceptional_behavior
@ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer)
@ public exceptional_behavior
@ signals (ProductNotFoundException e) !(\exists int i; 0 <= i && i < getPerson(id).products.length; getPerson(id).products[i].getId() == productId);
*/
public int querySalaryValue(int id, int productId) throws PersonIdNotFoundException, ProductIdNotFoundException;

方法3:顾客根据广告购买产品

/*
@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Customer);
@ requires (\forall int i; 0 <= i && i < getPerson(id).advertisemens.length; getPerson(id).money < advertisements[i].money || getPerson(id).preferInfo & advertisements[i].productInfo == 0 || (\exists int j; 0 <= j && j < \old(getPerson(id).products.length); \old(getPerson(id).products[j]).equals(getPerson(id).advertisements[i].product)));
@ requires \old(getPerson(id).money) == getPerson(id).money + (\sum int i; 0 <= i && i < \old(getPerson(id).advertisements.length); (getPerson(id).advertisement.contains(\old(getPerson(id).advertisements[i]))) ==> \old(getPerson(id).advertisements[i]).money);
@ signals (PersonIdNotFoundExeception e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Customer);
*/
public void purchase(int id) throws PersonIdNotFoundException, ProductIdNotFoundException;

学习体会

规格化设计:本单元中主要学习了什么是规格化设计和契约式设计。规格化设计就是定义一个开发人员必须遵守的规约。对于一个类来说,类包含数据规格和方法规格。

  • 数据规格:类所管理的数据内容,及其有效性条件(invariant,constraint)

    • invariant:任何时刻数据内容都必须满足的约束条件

    • constraint:任何时刻对于数据内容的修改都必须满足的条件

  • 方法规格:类所规定的操作,权利 + 义务 + 注意事项,包括前置条件 + 后置条件 + 副作用

    • 前置条件:规定了开发人员可以依赖的初始条件

    • 后置条件:开发人员必须保证的需求满足结果

规格化设计中,比较重要的是契约式编程,这部分可以看附录B

基于JML的规格模式:规格是对于数据和方法方方面面的约束,规格模式是对于规格的设计。在这方面JML是一种规格设计的模型语言。

  • JML定义了许多描述规格的方法和语法,具体内容可以参考附录C

  • JML写好了可以清楚明白的传递规格的定义和编写者思想,没有自然语言的二义性,因为一切条件和限制都清清楚楚的写到规格定义中了,通过阅读JML任何人都可以或者规格的定义,即使一个人最开始不知道什么是“最短路”,什么是”连通图“,通过反复阅读JML最终也能够理解;但是JML同样也有缺点,就是比较复杂,这一点从助教团队不断的修改指导书也能看出来,刚刚Network扩展任务中我也感受到了这一点,短短几句话就能说明白的事情,往往需要写很多JML。

  • 而且我认为JML具有一种依赖性,它把自然语言和抽象的规格说明分割开了。用自然语言说明一个方法的时候,可能暗含一些条件,不用说大家都能明白的那种;但是在JML中,这些应说未说的条件需要全部写出来,这对于编写者是一个挑战,因为一些理所应当、稀疏平常的条件不太容易一下子想起来。当阅读者阅读到JML的时候,又会切断自然思考的模式,就着JML推测编写者的含义,这时候如果一些条件没写出来,很容易出现读者”较真“的情况,或者在讨论区问一些看似没有必要问的问题,这一点在三四单元都很容易出现,所以第四单元助教甚至开了一个精华帖子。这种”较真“的情况恰恰说明了JML隔绝了人们使用自然语言的思考。

附录A JUnit 测试方法

  1. 确定我们要实现的契约

    require
       count <= capacity
       not key.empty
    ensure
       has (x)
       item (key) = x
       count = old count + 1
    invariant
       0 <= count
       count <= capacity
  2. 确定 JUnit 测试点

    invariant count <= capacity
    invariant 0 <= count

       @Test void it_creates_dictionary_with_specified_capacity() {
           assertThat(new Dictionary(5).capacity(), is(equalTo(5)));
      }

    invariant count <= capacity
    invariant 0 <= count

       @Test void on_creation_the_count_is_zero() {
           assertThat(new Dictionary(5).count(), is(equalTo(0)));
      }

    invariant count <= capacity
    invariant 0 <= count

       @Test void it_rejects_a_negative_capacity() {
           assertThrows(ContractViolationException.class, ()->
                   new Dictionary(-1));
      }

    ensure has (x)
    ensure item (key) = x

       @Test void an_entry_can_be_added_and_looked_up_by_its_key() {
           Dictionary dict = new Dictionary(1);
           dict.put("key1", "value1");
           assertThat(dict.get("key1"), is(equalTo("value1")));
      }

    ensure count = old count + 1    

       @Test void after_adding_an_entry_the_count_is_increased_by_one() {
           Dictionary dict = new Dictionary(1);
           int expectedCount = dict.count() + 1;
           dict.put("key1", "value1");
           assertThat(dict.count(), is(equalTo(expectedCount)));
      }

    require not key.empty

       @Test void it_will_not_accept_an_entry_with_a_null_key() {
           Dictionary dict = new Dictionary(1);
           assertThrows(ContractViolationException.class, ()->
                   dict.put(null, "value1"));
      }

    require not key.empty

       @Test void it_will_not_accept_an_entry_with_an_empty_key() {
           Dictionary dict = new Dictionary(1);
           assertThrows(ContractViolationException.class, ()->
                   dict.put("", "value1"));
      }

    require count <= capacity

       @Test void it_will_not_accept_an_entry_when_it_is_full() {
           Dictionary dict = new Dictionary(1);
           dict.put("key1", "value1");
           assertThrows(ContractViolationException.class, ()->
                   dict.put("key2", "value2"));
  3. 测试类

    class DictionaryTest {

       @Test void it_creates_dictionary_with_capacity_5() {
           assertThat(new Dictionary(5).capacity(), is(equalTo(5)));
      }

       @Test void it_creates_dictionary_with_capacity_1000() {
           assertThat(new Dictionary(1000).capacity(), is(equalTo(1000)));
      }

       @Test void on_creation_the_count_is_zero() {
           assertThat(new Dictionary(5).count(), is(equalTo(0)));
      }


       @Test void it_rejects_a_negative_capacity() {
           assertThrows(ContractViolationException.class, ()->
                   new Dictionary(-1));
      }

       @Test void an_entry_can_be_added_and_looked_up_by_its_key() {
           Dictionary dict = new Dictionary(1);
           dict.put("key1", "value1");
           assertThat(dict.get("key1"), is(equalTo("value1")));
      }

       @Test void after_adding_an_entry_the_count_is_increased_by_one() {
           Dictionary dict = new Dictionary(1);
           int expectedCount = dict.count() + 1;
           dict.put("key1", "value1");
           assertThat(dict.count(), is(equalTo(expectedCount)));
      }

       @Test void it_will_not_accept_an_entry_with_a_null_key() {
           Dictionary dict = new Dictionary(1);
           assertThrows(ContractViolationException.class, ()->
                   dict.put(null, "value1"));
      }

       @Test void it_will_not_accept_an_entry_with_an_empty_key() {
           Dictionary dict = new Dictionary(1);
           assertThrows(ContractViolationException.class, ()->
                   dict.put("", "value1"));
      }

       @Test void it_will_not_accept_an_entry_when_it_is_full() {
           Dictionary dict = new Dictionary(1);
           dict.put("key1", "value1");
           assertThrows(ContractViolationException.class, ()->
                   dict.put("key2", "value2"));
      }
    }
  4. 为什么我们不直接使用 if 方法?

    Dictionary(int capacity) {
       if (capacity < 1) {
           throw new IllegalArgumentException();
      }
       this.capacity = capacity;
       contents = new HashMap<>(capacity);
    }
    Dictionary(int capacity) {
       Contract.require(capacity > 0,
                        "Capacity must be greater than zero");
       this.capacity = capacity;
       contents = new HashMap<>(capacity);
    }

    上下两种方法都可以实现前置条件和后置条件的检测。我们要选择哪个?上面那种方法固然可以实现检测前置条件,但是这里的 if 和程序正常逻辑中的if有什么分别?如果不给这个特殊的if一个特殊的含义,这是否会干扰我们后续检查代码?

  5. 最终

    class Dictionary {
       private int capacity;
       private Map<string,object> contents;

       public Dictionary(int capacity) {
           Contract.require(capacity > 0,
               "Capacity must be greater than zero");
           this.capacity = capacity;
           contents = new HashMap<>(capacity);
      }
       public void put(String key, Object value) {
           Contract.require(StringUtils.isNotEmpty(key),
               "Key cannot be empty");
           Contract.require(contents.size() < capacity,
               "Dictionary is full");
           contents.put(key, value);
      }
       public Object get(String key) { return contents.get(key); }
       public int capacity() { return capacity; }
       public int count() { return contents.size(); }
    }    
    public class Contract {
       public static void require(boolean expression, String message) {
           if (!expression) throw new ContractViolationException(message);
      }
    }

附录B 契约式编程 design by contract

什么是契约式编程?

契约式编程于1980年代由 Bertrand Mayer 提出,这是一种软件设计方法,侧重于指定软件、方法之间交互的契约。软件的运行中,各个方法之间的交互可以看成是客户端-服务器模式(client-server model),服务器做出需要某些承诺或者叫做契约来向客户端提供服务,同时客户端必须遵守这些承诺,否则将无法保证可以得到服务器的服务。上述契约一般是使用服务器时的前置条件,通常也包含后置条件和不变式(invariant)。从中我们可以提炼出一个一般的思想:Hoare triples(霍尔三元组),它是由计算机科学家 Tony Hoare 在1969年提出的概念,我们可以通过考虑一段代码的执行如何改变计算状态来推理软件的正确性。基本的公式是:\{P\}C\{Q\}

P 表示前置条件(Post Condition),C 代表计算(Calculation),Q代表后置条件(Post Condition)。前置条件和后置条件都使用谓词逻辑(predicate logic, also known as first-order logic)进行表述。在 DbC 中,不变式是某些数据结构或者系统状态不会在计算过程中被改变或者影响的方面。

跳出 Java,来到更大的世界

由于我们的课程叫做面向对象程序设计,不是java程序设计,所以我想介绍一些契约式编程在 java 之外的应用(为什么强调应用在 Java?因为我们可以从这两次的作业中看到,虽然 JML 语言应用了契约式编程思想,但是仍然需要在方法前面写一堆谓词逻辑来保证行为的正确性。而且这种逻辑很容易写错助教修订了很多次对吧)。契约式编程的创始人 Mayer 创造了这样的一种规范化的定义,但是Mayer 并没有止步于此,他还开发了一种编程语言,其中内置了契约式编程,并且借此提高编译速度——Eiffel 语言。

class DICTIONARY [ELEMENT]
feature
put (x: ELEMENT; key: STRING) is
-- Insert x so that it will be retrievable
-- through key.
require
count <= capacity
not key.empty
ensure
has (x)
item (key) = x
count = old count + 1
end

... Interface specifications of other features ...

invariant
0 <= count
count <= capacity
end

require 关键字定义了前置条件,在这里比如数量小于总容量,不能添加没有key的元素等

ensure 关键字定义了后置条件,比如说这里保证了 put 方法之后一定含有这个元素,新的count为原count+1等

invariant 关键字定义了不随计算改变的整个系统的状态,所以他的范围比require和ensure更大,在这里比如count大于等于0,count小于总容量等限制条件。

契约式编程不只是测试

契约式编程不是测试,它更像是一种检查。有人可能会认为任何形式的检查都要等到程序完成进入提交阶段才能进行,但是契约式编程恰恰是提供了一种在程序运行时进行检查的思想。前置条件、后置条件和不变式恰恰防止了无效的参数或者错误的数据被传入方法中、防止了方法随意的提供无用的或者有害的输出、防止了方法无意的改变了整个系统的状态。

一个可能的应用场景

在微服务器日益普及,某个给定的应用程序可能由许多素未谋面的人独立开发出来;用时客户从注册表中查找某个服务,也常常不清楚他们正在调用的服务接口到底是什么样子的。在互联网的环境中,程序的运行环境不再像我们本机一样可靠和安全,不确定的输入和攻击可能从四面八方涌来。当我们的软件或者方法被其他使用者调用时,DbC 提供了一种确保代码仍然能够可靠运行的方法。

契约式编程的广泛应用

Java

在 java 中我们可以使用某些插件来使用契约式编程,比如 COFOJA 和 ANNOTATED-CONTRACTS。Cofoja 是谷歌开发团队在2011年左右开始的项目,大约在2015年左右火了一把,之后就一直萎靡不振。在这个插件中有三种关键的定义

  • @Requires ——方法前置条件

  • @Ensures ——方法后置条件

  • @Invariant ——类或者接口不变量

缺点:有不少使用者反映这个插件不是很好用,不变量的使用不如自己手写。而且 IDEA 对这个插件不是完全兼容。

Sebastian Hoss 为 Java 创造了一个基于注释的库,可以支持一部分DbC 指令,

。。。

契约式编程还有许多其他广泛的应用场景,比如 .Net 语言、Ruby、JavaScript、Python、Golang

附录C JML 表达式

原子表达式(略)

  1. \result:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。

  2. \old(expr):表示一个表达式expr在相应方法执行前的取值,该表达式涉及到评估expr中的对象是否发生变化。 如果是引用(如hashmap),对象没改变,但进行了插入或删除操作。v和odd(v)也有相同的取值。

  3. \not_assigned(x,y,...):用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回 false 。用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值。

  4. \not_modified(x,y,...):该表达式限制括号中的变量在方法执行期间的取值未发生变化。

  5. \nonnullelements(container):表示container对象中存储的对象不会有null。

  6. \type(type):返回类型type对应的类型(Class),如type(boolean)为Boolean.TYPE。TYPE是JML采用的缩略表示,等同于Java中的 java.lang.Class。

  7. \typeof(expr):该表达式返回expr对应的准确类型。如\typeof(false)为Boolean.TYPE。

量化表达式(略)

  1. \forall:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

  2. \exists:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

  3. \sum:返回给定范围内的表达式的和。

  4. \product:返回给定范围内的表达式的连乘结果。

  5. \max:返回给定范围内的表达式的最大值。

  6. \min:返回给定范围内的表达式的最小值。

  7. \num_of:返回指定变量中满足相应条件的取值个数。可以写成(\num_of T x; R(x);P(x)),其中T为变量x的类型,R(x)为x的取值范围;P(x)定义了x需要满足的约束条件。从逻辑上来看,该表达式也等价于(\sum T x;R(x)&&P(x);1)

集合表达式(略)

可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。集合构造表达式的一般形式为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。

操作符(略)

  • E1<:E2子类型操作符:如果类型E1是类型E2的子类型(sub type)或相同类型,则该表达式的结果为真,否则为假。任意一个类X,都必然满足X.TYPE<:Object.TYPE

  • b_expr1<==>b_expr2b_expr1<=!=>b_expr2等价关系操作符:其中b_expr1和b_expr2都是布尔表达式。

  • b_expr1==>b_expr2b_expr1<==b_expr2推理操作符:相当于离散的->,只有(1,0)是false。

  • \nothing\everthing变量引用操作符:表示当前作用域访问的所有变量。前者空集,后者全集。变量引用操作符经常在assignable句子中使用,如 assignable \nothing表示当前作用域下每个变量都不可以在方法执行过程中被赋值。

posted on   Zhang_kg  阅读(59)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示