[软件构造] 5设计规约&6抽象数据类型

软件构造

5 Designing Specification设计规约

  • 方法的规约
  • 前置、后置条件
  • 欠定规约、非确定规约
  • 陈述式、操作式规约
  • 规约specification的强度及其比较
  • 如何写出好的规约

Reading

§ MIT 6.031:06、07
§ CMU 17-214:Sep 05

1 编程语言中的函数和方法

参数类型、返回值类型是否匹配,在静态类型检查static type check阶段完成.
“方法”是程序的“积木”,可以被独立开发、测试、复用.
使用“方法”的客户端,无需了解方法内部具体如何工作—“抽象”

2规约:为了编程时更好地交流

(1) 编程文档

写下一个变量的类型实际是对这个变量进行了假设:例:这个变量永远指向一个整数。final:不可改变。
为什么要写出“假设”?第一:自己记不住;第二:别人不懂。
代码中蕴含的“设计决策”:给编译器读
注释形式的“设计决策”:给自己和别人读

(2) 方法的规约与契约contract

没规约,没法写程序;即使写出来,也不知道对错
规约是程序与客户端之间达成的一致,给“供需双方”都确定了责任,在调用的时候双方都要遵守
规约也可以提高代码效率
规约:扮演“防火墙”角色。

  • 对客户而言,看不到具体实现,开发者可以在不让客户知道的情况下修改实现。
  • 对开发者而言,不知道客户拿这个去干什么。

起一个解耦decoupling的作用:只要满足规约,用户的调用代码和开发者的实现代码可以独立的进行修改。

规约的内容

  • 输入输出的数据类型
  • 功能和正确性
  • 性能

能做什么,怎么实现

(3) 行为等价性

两个方法是否可相互替换?行为不同,但对用户来说 “是否等价”?站在客户端视角看行为等价性。
如果某些特殊情况下,两个方法表现不同,但是在客户应用的所有情况下,这两个方法表现相同,则这两个方法相同。
符合规约则等价。

(4)规约的结构:pre/post-condition

组成

  • precondition @param,
  • postcondition @return,@throws
  • exceptional behavior:前置条件不满足时干什么

前置条件:对客户端的约束,在使用方法时必须满足的条件,用requires关键字标识
后置条件:对开发者的约束,方法结束时必须满足的条件effects
契约:如果前置条件满足了,后置条件必须满足。若前置条件不满足,则方法可做任何事情。
静态类型声明是一种规约,可据此进行 静态类型检查static checking
方法前的注释也是一种规约,但需人工判定其是否满足

image-20220604195225875

image-20220604195235786

image-20220604195024288

除非在后置条件里声明过,否则方法内部不应该改变输入参数
应尽量遵循此规则,尽量不设计 mutating的spec,否则就容易引发bugs。
程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数。
尽量避免使用mutable的对象.这样就不用在规约里加许多别的限制了。
程序中可能有很多变量指向同一个可变对象(别名)
无法强迫类的实现体和客户端不保存可变变量 的"别名"。

(5)*测试和核实verify规约

黑河测试要像用户一样遵循规约。

3 设计规约

(1) 规约的分类

判断哪个规约更好:

  • 规约的确定性:输出是否确定
  • 陈述性:规约写的细不细
  • 强度:合法实现集合的大小

规约的强度S2>=S1当:

  • S2的前置条件更弱
  • 在满足S1的后置条件下,S2的后置条件更强

此时可用S2替代S1
规约变强:前置条件变松,后置条件变严。

陷阱:注意前置条件一定要满足,在此条件下后置条件一些附加的东西可能完全没用。例如此处 or -1 if no such i永远不会满足。

不管规约变得更强还是更弱,实现者都要更加小心;但如果客户有更多不同的输入或更具体的需求,现在可能就得使用更强的规格。

(2) 图表规约

方法实现:点
规约:由所有可能的方法实现组成的区域。
满足规约:点在区域范围内,不满足则在外面。
规约变强:面积变小。

  • 更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了->面积更小
  • 更强的后置条件意味着实现的自由度更低了->在图中的面积更小

(3) 设计好的规约

易读

内聚的coherent:规约描述的功能应单一、简单、易理解
不该有:

  • 许多不同的情况
  • 长长的参数表
  • 深层嵌套if语句
  • boolean flags
  • terrible use of global variables
  • printinginstead of returning

足够强:需考虑各种特殊情况,并在postcondition里给出处理措施

足够弱:必要的细节不能省,要不然别人不知道你这个方法是干什么用的

使用抽象类型:给方法的实现体和客户端更大的自由度

  • 用接口类型而不是具体的实现类型:Map or Reader HashMap FileReader

precondition?
衡量标准:检查参数合法性的代价多大

  • 不写Precondition,就要在代码内部check;若代价太大,在规约里加入precondition,把责任交给client

是否使用前置条件取决于:

(1) check的代价;

(2) 方法的使用范围

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

总结

  • 规约的“防火墙”作用:隔离开发者和使用者,可分别独立自己的部分。
  • 能减少bug
    • 统一接口协议
    • 规约中写 static typing and exceptions而不只是给人写的注释,可以减少bug
  • 易于理解
  • ready for change :
    • 开发者和使用者可独立开发,只要满足契约。
    • 规约变强仍可以用
  • Declarative specs are the most useful in practice

6 抽象数据类型

ADT的特性:表示泄露representation exposure, 抽象函数AFabstraction functions, 表示不变量RIrepresentation invariants
基于数学的形式对ADT的这些核心特征进行描述并应用于设计中。
Reading
§ MIT 6.031:10、11

1 抽象和用户定义的类型

用户定义的类型

除了编程语言所提供的基本数据类型和对象数据类型,程序员可定义自己的数据类型

传统的类型定义:关注数据的具体表示
抽象类型:强调“作用于数据上的操作”,程序员和 client无需关心数据如何具体存储的,只需设计/使用操作即可。
操作(和规约)完全定义了这种抽象数据类型,'抽象'是指把数据结构的细节、数据存储、实现方式抽象掉了。
例子1:

多种多样的实现方式:随便定义,满足操作(和规约)即可。

例子2:List类型,只要满足规约里的所有操作,如List.get();List.size(),etc即可,与内部实现方式无关,用数组、用链表来表示一个List类型都行。

2 分类类型和操作

可变类型的 对象:提供了可改变其内部数据的值的操作
Date is mutable,setMonth()改,getMonth()看改成什么了
不变数据类型: 其操作不改变内部值,而是构造新的对象
String is immutable
有些类型提供了mutableimmutable两种形式,例如
StringBuilder is mutable version of String ,但是二者不能互相取代

Creator构造器:create new objects of the type
Producer生产器:create new objects from old objects of the type
-eg: concat()method of String
Observer观察器:take objects of the abstract type and return objects of a different type
-eg: size()method of List, return an int
Mutator变值器:change objects
-eg: add()method of List, mutates a list by adding an element to the end

creator: either implemented as a constructor构造函数 , like new ArrayList(), or simply a static method instead, like Arrays.asList(),List.of(),String.valueOf(Object Obj)...
A creator implemented as a static method is often called a factory method 工厂方法

变值器通常返回 void, 如果返回值为 void,则必然意味着这个方法改变了对象的某些内部状态; 变值器也可能返回非空类型,

  • Set.add() returns a boolean that indicates whether the set was actually changed.
  • In Java’s graphical user interface toolkit, Component.add() returns the object itself, so that multiple add() calls can be chained together.

3 抽象数据类型例子

4 设计抽象类型

设计好的ADT,靠“经验法则”,提供一组操作,设计其行为规约 spec

  1. 设计简洁、一致的操作
    • 操作要少而简,不要太复杂
    • 每个操作要由设计好的目的,行为要有条理
  2. 要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低
    • 没有get()操作就无法获 取list的内部数据
    • 用遍历方式获取 list的size –太复杂 vs 提供size()操作,方便client使用

5 Representation Independence 表示独立性

表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
-eg:List的操作,与是用linked list or an array实现的无关(独立)

6 测试抽象数据类型

§ 测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否满足spec;
§ 测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。
§ 风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效。

7 不变量

好的ADT要能保持不变量。不变量:在任何时候总是true。例如:immutability就是一个典型的“不变量”。由ADT来负责其不变量,与client端的任何行为无关

表示泄露:不仅影响不变性,也影响了表示独立性:无法在不影响客户 端的情况下改变其内部表示

image-20220605083928484

因为public,原版实现可以在创建对象之后访问内部域,并且可以对域重新赋值,并没有实现immutable的功能。

将域修改成private final 可以修复能重新赋值域的问题,但是无法像之前一样用obj.field的方式来observe域的值了,需重新写observer,这里用的是写public 的getField()方法,返回域。因为public,所以能在class外面使用,起到成功observe的作用。

private:只在class内部能访问
public:在class外也能访问
final:保证immutable类型的域在这个对象被创建后就无法被修改了

在规约里写好,叫客户自觉一点?

when there isn’t any other reasonable alternative – for example, when the mutable object is too large to copy efficiently.
但是由此引发的潜在bug也将很多

除非迫不得已,否则不要把希望寄托于客户端上,ADT有责任保证自己的invariants,并避免“表示泄露”。最好的办法就是使用immutable的类型,彻底避免表示泄露。保持不变性和保持表示独立性,是ADT最重要的一个Invariant!
§ Don’t incorporate mutable parameters into object; make defensive copies § Return defensive copies of mutable fields… – Return new instance instead of modifying

image-20220605102327812

§ Or return unmodifiable view of mutable fields
§ Real lesson – use immutable components, to eliminate the need for defensive copying

8 表示不变性和抽象函数

image-20220605102928662

R:一般情况下ADT的表示比较简 单,有些时候需要复杂表示
A:抽象值构成的空间:client看到和使用的值
ADT开发者关注表示空间R,client关注抽象空间A

eg:String

Then the rep space R contains Strings, and the abstract space A is mathematical sets of characters.

R和A间的映射:

image-20220606185124439

R->A的映射为:

  • 满射:每个A总有R与之对应
  • 未必单射:不同的R可以对应一个A
  • 未必双射:有些R可能没有与之对应的A(这样的R非法)

抽象函数abstraction function:如何去解释R中的每一个值为A中的每一个值,图中的弧就表示了一个抽象函数。

§ 表示不变性RI:某个具体的“表示”是否是“合法的”
§ 也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值
§ 也可将RI看作:一个条件,描述了什么是“合法”的表示值

不同的内部表示,需要设计不同的AF和RI
选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。

原先的RI:s中不能有相同字母
改一下RI:s中可以有相同的字母,但是字母必须以升序排列
就出现了一样的表示值空间,不一样的表示不变量,换句话说,集合{x},{y}没变,变的是x的取值范围(RI),x->y的映射(AF)。
即使是同样的R、同样的RI,也 可能有不同的AF,即“解释不同”。
eg:"acgg" ①[a-c]+[g-g] ②{a,b,c,g}
设计ADT:设计抽象值空间A(右边的大圈),表示值空间R(左边的大圈),表示值空间中哪些值是合法的(R大圈里的合法的小圈RI),怎样解释他们(映射到A空间中的哪个,即箭头)
而且要把这种选择和解释明确写到代码当中。

方法支持AF/RI:原来在RI里的输入,执行该方法之后,还在RI里。

例:
RI:奇数长,升序排列
AF:(为简便,用数字k代表s[k])
0<=c<=1或
2<=c<=3或
4<=c<=5或 总之就是能不能找到一个字母插进原来的字符串,能形成长为3的升序字母子串。

image-20220606200052080

检查RI是否真的满足:
采用assert:

写一个checkRep()方法,在每个可能改变RI的语句后面都运行一次(creators, producers, mutators

observer方法可以不用,但是最好还是用。可以catch 被rep exposure造成的RI violations。

9 beneficent mutation 有益的可变性

提炼出一个定义:the abstract value should never change.

但是mutate a rep value is free, as long as it continues to map to the same abstract value, so that the change is invisible to the client.
change: beneficent mutation

也就是说,对immutable 的ADT来说,它在A空间的abstract value应是不变的。但其内部表示的R空间中的取值则可以是变化的。
这种mutation只是改变了R值,并未改变A值,对client来说是 immutable的“AF并非单射”(AF可以多对一),从一个R值变成了另一个R值,
但这并不代表在immutable的类中就可以随意出现mutator!

10 Documenting the AF, RI, and Safety from Rep Exposure

在代码中用注释形式记录AF和RI
要精确的记录RI:rep中的所有fields何为有效
要精确记录AF:如何解释每一个R值
还要记录:表示泄漏的安全声明
给出理由,证明代码并未对外泄露其内部表示——自证清白

总结

ADT的规约里只能使用client可见的内容来撰写,包括参数、返 回值、异常等。
如果规约里需要提及“值”,只能使用A空间 中的“值”。

ADT的规约里也不应谈及任何内部表示 的细节,以及R空间中的任何值
ADT的内部表示(私有属性)对外部都应严格不可见
故在代码中以注释的形式写出AF和RI而不 能在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏

§ To make an invariant hold, we need to:

  • – Make the invariant true in the initial state of the object;
  • – Ensure that all changes to the object keep the invariant true.– 在对象的初始状态不变量为true,在对象发生变化时,不变量也要为true

§ Translating this in terms of the types of ADT operations:

  • – Creators and producers must establish the invariant for new object instances; 构造器和生产器在创建对象时要确保不变量为true
  • – Mutators and observers must preserve the invariant. 变值器和观察 器在执行时必须保持不变性。
  • – Using checkRep() to check invariants before each method returns. 在每个方法return之前,用checkRep()检查不变量是否得以保持

表示泄漏的风险:一旦泄露,ADT内部表示可能会在程序的任何位置 发生改变(而不是限制在ADT内部),从而无法确保ADT的不变量是否能够始终保持为true

11 ADT invariants replace preconditions

将对tweets,username本来在规约里的设置,用ADT不变量取代,将约束条件封装在ADT内部。

summary

  • Abstract data types are characterized by their operations

  • Operations can be classified into creators, producers, observers, and mutators

  • An ADT’s specification is its set of operations and their specs.

  • A good ADT is simple, coherent, adequate, and representation-independent.

  • An ADT is tested by generating tests for each of its operations, but using the creators, producers, mutators, and observers together in the same tests.

  • § Safe from bugs.A good ADT offers a well-defined contract for a data type, so that clients know what to expect from the data type, and implementors have well-defined freedom to vary.

    A good ADT preserves its own invariants, so that those invariants are less vulnerable to bugs in the ADT’s clients, and violations of the invariants can be more easily isolated within the implementation of the ADT itself. Stating the rep invariant explicitly, and checking it at runtime with checkRep(), catches misunderstandings and bugs earlier, rather than continuing on with a corrupt data structure

  • § Easy to understand. A good ADT hides its implementation behind a set of simple operations, so that programmers using the ADT only need to understand the operations, not the details of the implementation.

    Rep invariants and abstraction functions explicate the meaning of a data type’s representation, and how it relates to its abstraction.

  • § Ready for change. Representation independence allows the implementation of an abstract data type to change without requiring changes from its clients.

    Abstract data types separate the abstraction from the concrete representation, which makes it possible to change the representation without having to change client code.

  • § An invariant is a property that is always true of an ADT object instance, for the lifetime of the object.

  • § A good ADT preserves its own invariants. Invariants must be established by creators and producers, and preserved by observers and mutators.

  • § The rep invariant specifies legal values of the representation, and should be checked at runtime with checkRep() .

  • § The abstraction function maps a concrete representation to the abstract value it represents.

  • § Representation exposure threatens both representation independence and invariant preservation.

posted @ 2022-06-06 14:26  Matrix_250  阅读(43)  评论(0编辑  收藏  举报