【软件构造】第三章第三节 抽象数据型(ADT)

第三章第三节 抽象数据型(ADT)

       3-1节研究了“数据类型”及其特性 ; 3-2节研究了方法和操做的“规约”及其特性;在本节中,咱们将数据和操做复合起来,构成ADT,学习ADT的核心特征,以及如何设计“好的”ADT。java

Outline

  • ADT及其四种类型
    • ADT的基本概念
    • ADT的四种类型
    • 设计一个好的ADT
  • 表示独立性
  • 不变量和表示泄露
  • 抽象函数AF和表示不变量RI
    • AF与RI
    • 用注释写AF和RI

Notes

## ADT及其四种类型

【ADT的基本概念】git

  • 抽象数据类型(Abstract Data Type,ADT)是是指一个数学模型以及定义在该模型上的一组操做;即包括数据数据元素,数据关系以及相关的操做。
  • ADT具备如下几个能表达抽象思想的词:
    • 抽象化:用更简单、更高级的思想省略或隐藏低级细节。
    • 模块化: 将系统划分为组件或模块,每一个组件能够设计,实施,测试,推理和重用,与系统其他部分分开使用。
    • 封装:围绕模块构建墙,以便模块负责自身的内部行为,而且系统其余部分的错误不会损坏其完整性。
    • 信息隐藏: 从系统其他部分隐藏模块实现的细节,以便稍后能够更改这些细节,而无需更改系统的其余部分。
    • 关注点分离: 一个功能只是单个模块的责任,而不跨越多个模块。
  • 与传统类型定义的差异:
    • 传统的类型定义:关注数据的具体表示。
    • 抽象类型:强调“做用于数据上的操做”,程序员和client无需关心数据如何具体存储的,只需设计/使用操做便可。
  • ADT是由操做定义的,与其内部如何实现无关!程序员

【ADT的四种类型】编程

  • 前置定义:mutable and immutable types
    • 可变类型的对象:提供了可改变其内部数据的值的操做。Date
    • 不变数据类型: 其操做不改变内部值,而是构造新的对象。String

  • Creators(构造器):
    • 建立某个类型的新对象,⼀个建立者可能会接受⼀个对象做为参数,可是这个对象的类型不能是它建立对象对应的类型。可能实现为构造函数或静态函数。(一般称为工厂方法)
    • t* ->  T
    • 栗子:Integer.valueOf( )
  • Producers(生产器):
    • 经过接受同类型的对象建立新的对象。
    • T+ , t* -> T
    • 栗子:String.concat( )
  • Observers(观察器):
    • 获取抽象类型的对象而后返回一个不一样类型的对象/值。
    • T+ , t* -> t
    • 栗子:List.size( ) ;
  • Mutators(变值器):
    • 改变对象属性的方法 ,
    • 变值器一般返回void,若为void,则必然意味着它改变了对象的某些内部状态;固然,也可能返回非空类型 
    • T+ , t* -> t || T || void
    • 栗子:List.add( )
  • 解释:T是ADT自己;t是其余类型;+ 表示这个类型可能出现一次或屡次;* 表示可能出现0次或屡次。
  • 更多栗子:

【设计一个好的ADT】数组

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

  • 原则 1:设计简洁、一致的操做。
    • 最好有一些简单的操做,它们能够以强大的方式组合,而不是不少复杂的操做。
    • 每一个操做应该有明确的目的,而且应该有一致的行为而不是一连串的特殊状况。
  • 原则 2:要足以支持用户对数据所作的全部操做须要,且用操做知足用户须要的难度要低。
    • 提供get()操做以得到list内部数据
    • 提供size()操做获取list的长度
  • 原则 3:要么抽象、要么具体,不要混合 —— 要么针对抽象设计,要么针对具体应用的设计。

【测试ADT】模块化

  • 测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否知足spec;
  • 测试observers: 调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。

 

## 表示独立性

  • 表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不该影响外部spec和客户端。
  • 除非ADT的操做指明了具体的前置条件/后置条件,不然不能改变ADT的内部表示——spec规定了 client和implementer之间的契约。

【一个例子:字符串的不一样表示】函数

  让咱们先来看看一个表示独立的例子,而后考虑为何颇有用,下面的MyString抽象类型是咱们举出的例子。下面是规格说明:post

 1 /** MyString represents an immutable sequence of characters. */
 2 public class MyString { 
 3 
 4     //////////////////// Example of a creator operation ///////////////
 5     /** @param b a boolean value
 6      *  @return string representation of b, either "true" or "false" */
 7     public static MyString valueOf(boolean b) { ... }
 8 
 9     //////////////////// Examples of observer operations ///////////////
10     /** @return number of characters in this string */
11     public int length() { ... }
12 
13     /** @param i character position (requires 0 <= i < string length)
14      *  @return character at position i */
15     public char charAt(int i) { ... }
16 
17     //////////////////// Example of a producer operation ///////////////    
18     /** Get the substring between start (inclusive) and end (exclusive).
19      *  @param start starting index
20      *  @param end ending index.  Requires 0 <= start <= end <= string length.
21      *  @return string consisting of charAt(start)...charAt(end-1) */
22     public MyString substring(int start, int end) { ... }
23 }

  使用者只须要/只能知道类型的公共方法和规格说明。下面是如何声明内部表示的方法,做为类中的一个实例变量:性能

private char[] a;

  使用这种表达方法,咱们对操做的实现多是这样的:

 1 public static MyString valueOf(boolean b) {
 2     MyString s = new MyString();
 3     s.a = b ? new char[] { 't', 'r', 'u', 'e' } 
 4             : new char[] { 'f', 'a', 'l', 's', 'e' };
 5     return s;
 6 }
 7 
 8 public int length() {
 9     return a.length;
10 }
11 
12 public char charAt(int i) {
13     return a[i];
14 }
15 
16 public MyString substring(int start, int end) {
17     MyString that = new MyString();
18     that.a = new char[end - start];
19     System.arraycopy(this.a, start, that.a, 0, end - start);
20     return that;
21 }

执行下列的代码

MyString s = MyString.valueOf(true);
MyString t = s.substring(1,3);

 

  咱们用快照图展现了在使用者进行 subString 操做后的数据状态:

  这种实现有一个性能上的问题,由于这个数据类型是不可变的,那么 substring 实际上没有必要真正去复制子字符串到⼀个新的数组中。它能够仅仅指向原来的 MyString 字符数组,而且记录当前的起始位置和终⽌位置。

   为了优化,咱们能够将这个类的内部表示改成:

private char[] a;
private int start;
private int end;

   有了这个新的表示,操做如今能够这样实现:

 1 public static MyString valueOf(boolean b) {
 2     MyString s = new MyString();
 3     s.a = b ? new char[] { 't', 'r', 'u', 'e' } 
 4             : new char[] { 'f', 'a', 'l', 's', 'e' };
 5     s.start = 0;
 6     s.end = s.a.length;
 7     return s;
 8 }
 9 
10 public int length() {
11     return end - start;
12 }
13 
14 public char charAt(int i) {
15   return a[start + i];
16 }
17 
18 public MyString substring(int start, int end) {
19     MyString that = new MyString();
20     that.a = this.a;
21     that.start = this.start + start;
22     that.end = this.start + end;
23     return that;
24 }

   如今运行上面的调用代码,可用快照图从新进行 substring 操做后的数据状态:

  因为 MyString 的使用者仅依赖于其公共方法和规格说明,而不依赖其私有的存储,所以咱们能够在不检查和更改全部客户端代码的状况下进行更改。 这就是表示独立性的力量。  

 

##  不变量(Invariants)与表示泄露

  一个好的抽象数据类型的最重要的属性是它保持不变量。一旦一个不变类型的对象被建立,它老是表明一个不变的值。当一个ADT可以确保它内部的不变量恒定不变(不受使用者/外部影响),咱们就说这个ADT保护/保留本身的不变量。

【一个栗子:表示泄露】

 1 /**
 2  * This immutable data type represents a tweet from Twitter.
 3  */
 4 public class Tweet {
 5 
 6     public String author;
 7     public String text;
 8     public Date timestamp;
 9 
10     /**
11      * Make a Tweet.
12      * @param author    Twitter user who wrote the tweet
13      * @param text      text of the tweet
14      * @param timestamp date/time when the tweet was sent
15      */
16     public Tweet(String author, String text, Date timestamp) {
17         this.author = author;
18         this.text = text;
19         this.timestamp = timestamp;
20     }
21 }

  咱们如何保证这些Tweet对象是不可变的,(即一旦建立了Tweet,其author,message和 date 永远不会改变)

  对不可变性的第一个威胁来自使用者能够直接访问Tweet内部数据的事实,例如执行以下的引用操做:

1 Tweet t = new Tweet("justinbieber", 
2                     "Thanks to all those beliebers out there inspiring me every day", 
3                     new Date());
4 t.author = "rbmllr";

   这是一个表示泄露(Rep exposure)的简单例子,这意味着类外的代码能够直接修改表示。像这样的表示暴露不只威胁到不变量,并且威胁到表示独立性。若是咱们改变类内部数据的1表示方式,使用者也会相应的受到影响。

  幸运的是,java给咱们提供了处理表示暴露的方法:

 1 public class Tweet {
 2     private final String author;
 3     private final String text;
 4     private final Date timestamp;
 5 
 6     public Tweet(String author, String text, Date timestamp) {
 7         this.author = author;
 8         this.text = text;
 9         this.timestamp = timestamp;
10     }
11 
12     /** @return Twitter user who wrote the tweet */
13     public String getAuthor() {
14         return author;
15     }
16 
17     /** @return text of the tweet */
18     public String getText() {
19         return text;
20     }
21 
22     /** @return date/time when the tweet was sent */
23     public Date getTimestamp() {
24         return timestamp;
25     }
26 }

  在private和public关键字代表哪些字段和方法可访问时,只在类内部仍是能够从类外部访问。所述final关键字还保证该变量的索引不会被更改,对于不可变的类型来讲,就是确保了变量的值不可变。

  但这不能解决所有的问题:表示仍然会泄露!考虑这个彻底合理的客户端代码,它使用Tweet:

1 /** @return a tweet that retweets t, one hour later*/
2 public static Tweet retweetLater(Tweet t) {
3     Date d = t.getTimestamp();
4     d.setHours(d.getHours()+1);
5     return new Tweet("rbmllr", t.getText(), d);
6 }

  

  retweetLater 但愿接受一个Tweet对象而后修改Date后返回一个新的Tweet对象。

  这里有什么问题?其中的 getTimestamp 调用返回一个同样的 Date 对象,它会被t、t.timestamp 和 d 同时索引。所以,当日期对象被突变,d.gsetHours( ) 被调用时,t 也会影响日期,如快照图所示。

  

  这样,Tweet的不变性就被破坏,Tweet将本身内部对于可变对象的索引“泄露”了出来,所以整个对象都变成可变的了,使用者在使用时也容易形成隐藏的bug。

  咱们能够经过使用防护性拷贝来修补这种风险:制做可变对象的副本以免泄漏对表明的引用。代码以下:

public Date getTimestamp() {
    return new Date(timestamp.getTime());
}

  可变类型一般具备一个专门用来复制的构造函数,它容许建立一个复制现有实例值的新实例。在这种状况下,Date的复制构造函数就接受了一个timestamp值,而后产生一个新的对象。

  复制可变对象的另外一种方法是clone(),某些类型但不是所有类型支持该方法。然而clone()在Java中的工做方式存在问题,更多可参考 Effective Java , item 11

   如今咱们已经经过防护性复制解决了 timestamp 返回值的问题。但咱们尚未完成任务!还有表示泄露。考虑这个很是合理的客户端代码:

 1 /** @return a list of 24 inspiring tweets, one per hour today */
 2 public static List<Tweet> tweetEveryHourToday () {
 3     List<Tweet> list = new ArrayList<Tweet>(); 
 4     Date date = new Date();
 5     for (int i = 0; i < 24; i++) {
 6         date.setHours(i);
 7         list.add(new Tweet("rbmllr", "keep it up! you can do it", date));
 8     } 
 9     return list;
10 }

 

  此代码旨在建立24个Tweet对象,为每一个小时建立一条推文。但请注意,Tweet的构造函数保存传入的引用,所以全部24个Tweet对象最终都以同一时间结束,如此快照图所示。

  可是,Tweet的不变性再次被打破了,由于每⼀个Tweet建立时对Date对象的索引都是⼀样的。因此咱们应该对建立者也进⾏防护性编程:

1 public Tweet(String author, String text, Date timestamp) {
2     this.author = author;
3     this.text = text;
4     this.timestamp = new Date(timestamp.getTime());
5 }

  一般来讲,要特别注意ADT操做中的参数和返回值。若是它们之中有可变类型的对象,确保你的代码没有直接使⽤索引或者直接返回索引。

  你可能反对说这看起来很浪费。为何要制做全部这些日期的副本?为何咱们不能经过像这样仔细书写的规范来解决这个问题?

/**
 * Make a Tweet.
 * @param author    Twitter user who wrote the tweet
 * @param text      text of the tweet
 * @param timestamp date/time when the tweet was sent. Caller must never 
 *                   mutate this Date object again!
 */
public Tweet(String author, String text, Date timestamp) {

   这种方法通常只在特不得已的时候使用——例如,当可变对象太大而没法有效地复制时。可是,由此引起的潜在bug也将不少。除非无可奈何,不然不要把但愿寄托于客户端上,ADT有责任保证本身的不变量,并避免表示泄露。

   最好的办法就是使用immutable的类型,完全避免表示泄露,例如 java.time.ZonedDateTime 而不是 java.util.Date

 

## 抽象函数AF与表示不变量RI

【AF与RI】

  

  • 在研究抽象类型的时候,先思考一下两个值域之间的关系:
    • 表示域(rep values)里面包含的是值具体的实现实体。通常状况下ADT的表示比较简单,有些时候须要复杂表示。 
    • 抽象域(A)里面包含的则是类型设计时支持使用的值。这些值是由表示域“抽象/想象”出来的,也是使用者关注的。
  • ADT实现者关注表示空间R,用户关注抽象空间A 。
  • R->A的映射特色:
    • 每个抽象值都是由表示值映射而来 ,即满射:每一个抽象值被映射到一些rep值
    • 一些抽象值是被多个表示值映射而来的,即未必单射:一些抽象值被映射到多个rep值
    • 不是全部的表示值都能映射到抽象域中,即未必双射:并不是全部的rep值都被映射。

 

  • 抽象函数(AF):R和A之间映射关系的函数
AF : R → A 
  • 表示不变量(RI):将rep值映射到布尔值
RI : R → boolean  
    • 对于表示值r,当且仅当r被AF映射到了A,RI(r)为真。 
    • 表示不变性RI:某个具体的“表示”是不是“合法的”
    • 也可将RI看做:全部表示值的一个子集,包含了全部合法的表示值
    • 也可将RI看做:一个条件,描述了什么是“合法”的表示值
    • 在下图中,绿色表示的就是RI(r)为真的部分,AF只在这个子集上有定义。

  

  • 表示不变量和抽象函数都应该记录在代码中,就在表明自己的声明旁边,如下图为例 

public class CharSet {
    private String s;
    // Rep invariant:
    //   s contains no repeated characters
    // Abstraction function:
    //   AF(s) = {s[i] | 0 <= i < s.length()}
    ...
}

 

public class CharSet {
    private String s;
    // Rep invariant:
    //   s[0] <= s[1] <= ... <= s[s.length()-1]
    // Abstraction function:
    //   AF(s) = {s[i] | 0 <= i < s.length()}
    ...
}

 

public class CharSet {
    private String s;
    // Rep invariant:
    //   s.length() is even
    //   s[0] <= s[1] <= ... <= s[s.length()-1]
    // Abstraction function:
    //   AF(s) = union of {s[2i],...,s[2i+1]} for 0 <= i < s.length()/2
    ...
}

【用注释写AF和RI】

  • 在抽象类型(私有的)表示声明后写上对于抽象函数和表示不变量的注解,这是一个好的实践要求。咱们在上面的例子中也是这么作的。
  • 在描述抽象函数和表示不变量的时候,注意要清晰明确:
    • 对于RI(表示不变量),仅仅宽泛的说什么区域是合法的并不够,你还应该说明是什么使得它合法/不合法。
    • 对于AF(抽象函数)来讲,仅仅宽泛的说抽象域表示了什么并不够。抽象函数的做用是规定合法的表示值会如何被解释到抽象域。做为一个函数,咱们应该清晰的知道从一个输入到一个输入是怎么对应的。
  • 本门课程还要求你将表示暴露的安全性注释出来。这种注释应该说明表示的每一部分,它们为何不会发生表示暴露,特别是处理的表示的参数输入和返回部分(这也是表示暴露发生的位置)。
  • 下面是一个Tweet类的例子,它将表示不变量和抽象函数以及表示暴露的安全性注释了出来:
 1 // Immutable type representing a tweet.
 2 public class Tweet {
 3 
 4     private final String author;
 5     private final String text;
 6     private final Date timestamp;
 7 
 8     // Rep invariant:
 9     //   author is a Twitter username (a nonempty string of letters, digits, underscores)
10     //   text.length <= 140
11     // Abstraction function:
12     //   AF(author, text, timestamp) = a tweet posted by author, with content text, 
13     //                                 at time timestamp 
14     // Safety from rep exposure:
15     //   All fields are private;
16     //   author and text are Strings, so are guaranteed immutable;
17     //   timestamp is a mutable Date, so Tweet() constructor and getTimestamp() 
18     //        make defensive copies to avoid sharing the rep's Date object with clients.
19 
20     // Operations (specs and method bodies omitted to save space)
21     public Tweet(String author, String text, Date timestamp) { ... }
22     public String getAuthor() { ... }
23     public String getText() { ... }
24     public Date getTimestamp() { ... }
25 }

  注意到咱们并无对 timestamp 的表示不变量进行要求(除了以前说过的默认 timestamp!=null)。可是咱们依然须要对timestamp 的表示暴露的安全性进行说明,由于整个类型的不变性依赖于全部的成员变量的不变性。

相关文章
相关标签/搜索