抽象函数&表示不变量
Reading 13: Abstraction Functions & Rep Invariants
文章目录
前言
原文来自http://web.mit.edu/6.031/www/sp17/的第13篇文章:
1、软件
避免bug | 纠正今天,在未知的未来改正 |
---|---|
易懂 | 清楚地与未来的程序员沟通,包括未来的你 |
准备改变 | 设计来适应变化而不重写 |
2、目标
今天的阅读介绍了几个想法:
• 不变量
• 表象曝光
• 抽象函数
• 表示不变量
在本文中,我们研究了一个更正式的数学概念,即一个类实现adt意味着什么,它的概念是抽象函数和表示不变量。这些数学概念在软件设计中非常实用。抽象函数将为我们提供一种在抽象数据类型上明确定义相等操作的方法(我们将在以后的类中对此进行更深入的讨论)。表示不变量将使捕获由损坏的数据结构引起的bug变得更加容易。
一、不变量
继续讨论什么是好的抽象数据类型,一个好的抽象数据类型的最后一个,也许也是最重要的属性是保留自己的不变量。阿不变量对于程序的每一种可能的运行时状态,该程序的属性始终为true。不可变性是我们已经遇到的一个关键不变因素:一旦创建了不可变对象,它在整个生命周期中都应该始终表示相同的值。说ADT保留自己的不变量这意味着ADT负责确保它自己的不变量保持不变。这并不取决于客户的良好行为。
当ADT保留自己的不变量时,关于代码的推理就变得容易多了。如果您可以指望String永远不会改变,那么在调试使用String的代码时,或者当您试图为另一个使用String的ADT建立不变量时,您可以排除这种可能性。与之形成对比的是,只有当客户端承诺不更改字符串类型时,才能保证它是不可变的。然后,您必须检查代码中可能使用字符串的所有位置。
1.不变性
在后面的阅读中,我们将看到许多有趣的不变量。现在让我们来关注一下不变性。下面是一个具体的例子:
/**
* This immutable data type represents a tweet from Twitter.
*/
public class Tweet {
public String author;
public String text;
public Date timestamp;
/**
* 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
*/
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
}
我们如何保证这些Tweet对象是不可变的–一旦创建了Tweet,它的作者、消息和日期就永远不会改变?
对不可变性的第一个威胁来自这样一个事实,即客户端可以–实际上必须–直接访问它的字段。所以没有什么能阻止我们写这样的代码:
Tweet t = new Tweet("justinbieber",
"Thanks to all those beliebers out there inspiring me every day",
new Date());
t.author = "rbmllr";
这是一个微不足道的例子表象曝光,这意味着类之外的代码可以直接修改表示形式。像这样的REP暴露不仅威胁到不变量,而且威胁到表示的独立性。我们不能在不影响直接访问这些字段的所有客户端的情况下更改Tweet的实现。
幸运的是,Java为我们提供了处理这种REP暴露的语言机制:
public class Tweet {
private final String author;
private final String text;
private final Date timestamp;
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
/** @return Twitter user who wrote the tweet */
public String getAuthor() {
return author;
}
/** @return text of the tweet */
public String getText() {
return text;
}
/** @return date/time when the tweet was sent */
public Date getTimestamp() {
return timestamp;
}
}
这个private和public关键字指示哪些字段和方法只能在类中访问,哪些字段和方法可以从类外部访问。这个final关键字还有助于确保该不可变类型的字段在构造对象后不会重新分配。
但这还不是故事的结尾:这个代表还在曝光!考虑这完全合理的客户端代码,它使用
Tweet:
/** @return a tweet that retweets t, one hour later*/
public static Tweet retweetLater(Tweet t) {
Date d = t.getTimestamp();
d.setHours(d.getHours()+1);
return new Tweet("rbmllr", t.getText(), d);
}
retweetLater获取一条Tweet,并应返回另一条具有相同消息的Tweet(称为转发)但一小时后寄出了。这个retweetLater方法可能是一个系统的一部分,它自动响应推特名人所说的有趣的事情。
这里有什么问题?这个getTimestamp调用返回对同一Date被tweet引用的对象t. t.timestamp和d是同一个可变对象的别名。因此,当日期对象被d.setHours(),这会影响t以及,如图片所示。
Tweet它的不变性不变性已经被打破了。问题是Tweet泄露了对不可变对象的引用,该对象的不可变性依赖于该对象。我们揭露了这个代表Tweet不能再保证它的对象是不可变的。完全合理的客户端代码创建了一个微妙的bug。
我们可以通过使用防御性复制来修补这种REP暴露:创建可变对象的副本以避免泄漏对REP的引用。下面是代码:
public Date getTimestamp() {
return new Date(timestamp.getTime());
}
可变类型通常有一个副本构造函数,它允许您创建一个复制现有实例值的新实例。在这种情况下,Date复制构造函数使用时间戳值,自1970年1月1日起,以毫秒为单位。作为另一个例子,StringBuilder的复制构造函数采用String。另一种复制可变对象的方法是clone(),某些类型(但不是所有类型)都支持它。这条路有一些不幸的问题clone()在Java中工作。更多信息见乔希·布洛赫有效Java,项目11。
因此,我们在返回值中做了一些防御性复制。getTimestamp。但我们还没完呢!还有代表暴露。考虑以下(同样完全合理的)客户端代码:
/** @return a list of 24 inspiring tweets, one per hour today */
public static List<Tweet> tweetEveryHourToday () {
List<Tweet> list = new ArrayList<Tweet>();
Date date = new Date();
for (int i = 0; i < 24; i++) {
date.setHours(i);
list.add(new Tweet("rbmllr", "keep it up! you can do it", date));
}
return list;
}
此代码打算推进一个Date在一天中的24小时内,创建一条每小时一条的推特。但是请注意,Tweet的构造函数保存传入的引用,因此所有24个Tweet对象都以同一时间结束,如此快照图所示。
同样,Tweet的不可变性也遭到了侵犯。我们也可以通过使用明智的防御性复制来解决这个问题,这次是在构造函数中:
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());
}
通常,您应该仔细检查所有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!
*/
这种方法有时是在没有其他合理的选择时采取的–例如,当可变对象太大,无法有效地复制时。但是,你对程序的推理能力和避免错误的能力的代价是巨大的。在没有令人信服的相反论据的情况下,抽象数据类型几乎总是值得的,以保证它自己的不变量,而防止REP暴露是必不可少的。
更好的解决方案是选择不可变的类型。如果-如易变性与不变性土拨鼠节的例子-我们使用了一个不变的日期对象,比如java.time.ZonedDateTime,而不是可变的java.util.Date,那么我们在讨论完这一节之后,就可以结束这个部分了。public和private。不可能有更多的代表接触。
2.可变数据类型的不可变包装器
Java集合类提供了一个有趣的折衷方案:不可变的包装器。
Collections.unmodifiableList()采取(可变的)List并将其封装在一个看起来像List但是谁的变异者是残疾的-set(), add(), remove()等等,抛出异常。因此,您可以使用变送器构造一个列表,然后将其封装在一个不可修改的包装器中(并丢弃对原始可变列表的引用,如易变性与不变性),并得到一个不变的列表。
这里的缺点是在运行时变得不可变,但在编译时却不能。如果您试图在编译时发出警告,Java将不会警告您sort()这个不可修改的列表。您将在运行时得到一个异常。但是这仍然比没有好,所以使用不可修改的列表、地图和集合可以非常好地降低bug的风险。
二、 REP不变和抽象函数
我们现在更深入地研究抽象数据类型背后的理论。这一理论本身不仅典雅有趣,而且对抽象类型的设计和实现具有直接的实际应用价值。如果你对这个理论有很深的理解,你就能建立更好的抽象类型,并且不太可能陷入微妙的圈套。
在思考抽象类型时,它有助于考虑两个价值空间之间的关系。
表示值(简称REP值)的空间由实际实现实体的值组成。在简单的情况下,抽象类型将实现为单个对象,但更常见的是需要一个小的对象网络,因此这个值实际上通常是相当复杂的。
抽象值的空间由类型设计支持的值组成。这些都是我们想象出来的。它们是柏拉图式实体,并不像描述的那样存在,但它们是我们想要将抽象类型的元素视为该类型的客户端的方式。例如,无界整数的抽象类型可能将数学整数作为其抽象值空间;例如,它可能被实现为基元(有界)整数数组,这与类型的用户无关。
当然,抽象类型的实现者必须对表示值感兴趣,因为使用REP值空间实现抽象值空间的错觉是实现者的工作。
Suppose, for example, that we choose to use a string to represent a set of characters:
例如,假设我们选择使用字符串来表示一组字符:
public class CharSet {
private String s;
...
}
然后REP空间R包含字符串,抽象空间A是数学字符集。我们可以以图形的方式显示这两个值空间,从一个REP值到它所代表的抽象值有一个弧线。关于这幅画有几件事要注意:
• 每个抽象值都由某个REP值映射到。实现抽象类型的目的是支持对抽象值的操作。那么,我们大概需要能够创建和操作所有可能的抽象值,因此它们必须是可表示的。
• **一些抽象值由多个REP值映射到。**之所以会出现这种情况,是因为表示不是一个严格的编码。将无序字符集表示为字符串的方法不止一种。
• **并不是所有的REP值都被映射。**在这种情况下,字符串“abbc”没有映射。在本例中,我们决定字符串不应包含重复项。这将使我们能够终止remove方法,因为我们知道最多只能有一个实例。
在实践中,我们只能说明这两个空间的几个元素及其关系;图作为一个整体是无限的。因此,我们通过给出两件事来描述它:
- 一抽象函数它将REP值映射到它们所代表的抽象值:
AF : R → A
图中的弧表示抽象函数。在函数的术语中,我们前面讨论过的性质可以表示为函数是满射的(也称为落在),不一定是内射的(一对一),因此不一定是双射的,而且通常是局部的。
2.aREP不变量将代表价值映射到布尔人:
RI : R → boolean
代表价值r, 里亚尔®当且仅当r映射为房颤。换句话说,瑞告诉我们给定的REP值是否格式良好。或者,你可以想到瑞作为一个集合:它是REP值的子集房颤被定义。
例如,在上面的图表中CharSet,RI(“”) = true, RI(“abc”) = true, and RI(“bac”) = true, but RI(“abbc”) = false.
例如,在上面的图表中CharSet, RI(“”)=真,RI(“ABC”)=真,和RI(“bac”)=真的,但是RI(“ABBC”)=假。
REP不变量和抽象函数都应该记录在代码中,就在REP本身的声明旁边:
public class CharSet {
private String s;
// Rep invariant:
// s contains no repeated characters
// Abstraction function:
// AF(s) = {s[i] | 0 <= i < s.length()}
...
}
关于抽象函数和REP不变量的一个常见混淆是,它们是由REP和抽象值空间的选择决定的,甚至是由抽象值空间单独决定的。如果是这样的话,那就没什么用了,因为他们会说一些多余的东西,这些东西已经在其他地方找到了。
抽象值空间本身并不能决定AF或RI:同一抽象类型可以有几个表示。一组字符可以同样地表示为字符串,如上面所示,也可以表示为位向量,每个可能的字符有一个位。显然,我们需要两个不同的抽象函数来映射这两个不同的REP值空间。
不太清楚为什么这两个空间的选择不能决定AF和RI。关键是,定义REP的类型,从而为REP值的空间选择值,并不能确定哪些REP值被认为是合法的,哪些是合法的,哪些是合法的,它们将如何解释。与其像上面那样决定字符串没有重复,不如允许重复,但同时要求对字符进行排序,以不递减的顺序出现。这将允许我们对字符串执行二进制搜索,从而以对数时间而不是线性时间检查成员资格。相同的REP值空间-不同的REP不变量:
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()}
...
}
即使具有相同类型的REP值空间和相同的REP不变量,我们仍然可以用不同的抽象函数对REP进行不同的解释。假设REP不变量允许任何字符串。然后,我们可以定义抽象函数,如上面所示,将数组的元素解释为集合的元素。但没有先验让代表决定解释的理由。也许我们会将连续的对字符解释为子范围,这样字符串代表"acgg"被解释为两个范围对[a-c]和[g-g],因此表示集合{a,b,c,g}。下面是AF和RI表示的样子:
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
...
}
关键是实现抽象类型意味着不仅仅是选择两个空间-规格的抽象价值空间和实施的代表价值空间-但也决定哪些代表价值是合法的,以及如何将它们解释为抽象的价值。.
正如我们前面所做的那样,在代码中写下这些假设是至关重要的,这样以后的程序员(和您未来的自己)就会知道表示的真正含义。为什么?如果不同的实施者对代表的含义不一致,会发生什么情况?
您可以找到以下示例代码:三种不同CharSet实现在GitHub上。
阅读练习
REP不变件
AF/RI尝试在没有AF/RI的情况下实现
AF/RI #2尝试在没有AF/RI#2的情况下实现
AF/RI #3尝试在没有AF/RI#3的情况下实现
例:有理数
这里有一个有理数的抽象数据类型的例子。仔细看看它的REP不变函数和抽象函数。
public class RatNum {
private final int numerator;
private final int denominator;
// Rep invariant:
// denominator > 0
// numerator/denominator is in reduced form
// Abstraction function:
// AF(numerator, denominator) = numerator/denominator
/** Make a new RatNum == n.
* @param n value */
public RatNum(int n) {
numerator = n;
denominator = 1;
checkRep();
}
/** Make a new RatNum == (n / d).
* @param n numerator
* @param d denominator
* @throws ArithmeticException if d == 0 */
public RatNum(int n, int d) throws ArithmeticException {
// reduce ratio to lowest terms
int g = gcd(n, d);
n = n / g;
d = d / g;
// make denominator positive
if (d < 0) {
numerator = -n;
denominator = -d;
} else {
numerator = n;
denominator = d;
}
checkRep();
}
}
下面是这段代码的抽象函数和REP不变的图片。国际扶轮要求numerator/denominator对应以缩减形式(即最低项)表示,因此,像(2,4)这样的对应该绘制在RI之外。
完全合理的设计与一个更允许的RI相同的ADT的另一个实现。通过这样的改变,一些操作可能会变得更昂贵,而另一些操作可能会变得更便宜。
三、检查表示不变量
表示不变量不仅仅是一个简洁的数学概念。如果您的实现在运行时断言表示不变量,那么您可以尽早捕获bug。这里有一个方法RatNum测试它的表示不变量:
// Check that the rep invariant is true
// *** Warning: this does nothing unless you turn on assertion checking
// by passing -enableassertions to Java
private void checkRep() {
assert denominator > 0;
assert gcd(Math.abs(numerator), denominator) == 1;
}
你当然应该打电话checkRep()在创建或变异REP的每个操作的末尾断言REP不变量-换句话说,创建者、生产者和变异器。回首RatNum上面的代码,您将看到它调用checkRep()在两个构造函数的末尾。
观察者方法通常不需要调用checkRep()但这样做是很好的防守练习。为什么?呼叫checkRep()在每种方法中,包括观察者,意味着您将更有可能捕获REP暴露导致的REP不变违规行为。
为什么checkRep二等兵?谁应该负责检查和执行REP不变的客户端,还是实现本身?
阅读练习
检查REP不变量
四、REP中没有空值
从规格阅读空值是麻烦和不安全的,以至于我们试图将它们从我们的编程中完全删除。在6.031中,我们的方法的先决条件和后置条件隐含地要求对象和数组是非空的。
我们将这一禁令扩展到抽象数据类型的代表。默认情况下,在6.031中,REP不变量隐式包括x != null对于每一个参考资料x在具有对象类型的REP中(包括数组或列表中的引用)。所以如果你的代表是:
class CharSet {
String s;
}
那么它的REP不变量自动包括s != null,您不需要在REP不变的注释中声明它。
当是实现REP不变的时候,checkRep()方法,但是,您仍然必须实施这个s != null检查一下,确保你的checkRep()正确失败时s是null。这种检查通常来自Java,因为如果检查REP不变变量的其他部分,则会引发异常。s都是空的。例如,如果您的checkRep()看起来是这样的:
private void checkRep() {
assert s.length() % 2 == 0;
...
}
那你就不需要assert s != null,因为打电话给s.length()如果引用为空,将同样有效地失败。但如果s,否则将不受REP不变项的检查,然后断言s != null明文规定。
Beneficent mutation有益突变
五、有益突变
回想一下,类型是不可变的当且仅当该类型的值在创建后从未更改。通过对抽象空间A和REP空间R的新理解,我们可以改进这个定义:抽象值不应该改变。但是实现可以自由地对代表值只要它继续映射到相同的抽象值,使更改对客户端是不可见的。这种变化叫做有益突变.
下面是一个使用替代代表的简单示例RatNum我们之前看到的类型。该REP具有较弱的REP不变量,不需要以最低的术语存储分子和分母:
public class RatNum {
private int numerator;
private int denominator;
// Rep invariant:
// denominator != 0
// Abstraction function:
// AF(numerator, denominator) = numerator/denominator
/**
* Make a new RatNum == (n / d).
* @param n numerator
* @param d denominator
* @throws ArithmeticException if d == 0
*/
public RatNum(int n, int d) throws ArithmeticException {
if (d == 0) throw new ArithmeticException();
numerator = n;
denominator = d;
checkRep();
}
...
}
这个较弱的REP不变量允许RatNum算术运算,简单地省略,将结果降到最低项。但是当向人类展示结果的时候,我们首先简化它:
/**
* @return a string representation of this rational number
*/
@Override
public String toString() {
int g = gcd(numerator, denominator);
numerator /= g;
denominator /= g;
if (denominator < 0) {
numerator = -numerator;
denominator = -denominator;
}
checkRep();
return (denominator > 1) ? (numerator + "/" + denominator)
: (numerator + "");
}
注意这个function toString() { [native code] }实现重新分配私有字段numerator和denominator,对表示进行变异–即使它是不可变类型上的观察者方法!但是,关键的是,突变并没有改变抽象的价值。将分子和分母除以相同的公共因子,或将两者乘以-1,对抽象函数的结果没有影响,AF(numerator, denominator) = numerator/denominator。另一种思考它的方法是AF是一个多对一个函数,并且REP值已经改变为另一个仍然映射到相同抽象值的函数。所以突变是无害的,或者恩惠.
我们将在未来的阅读中看到其他有益突变的例子。这种实现自由通常允许性能改进,如缓存、数据结构再平衡和延迟清理。
记录AF、RI和REP暴露的安全性
在类中记录抽象函数和REP不变量是很好的做法,在声明REP的私有字段时使用注释。我们已经在上面做过了。
当您描述REP不变和抽象函数时,您必须精确:
• RI仅仅是“所有字段都是有效的”这样的泛型语句是不够的。REP不变量的工作是精确地解释什么使字段值有效或无效。
• AF提供“表示一组字符”这样的通用解释是不够的。抽象函数的任务是精确定义具体字段值的解释方式。作为功能,如果我们使用文档化的AF并在实际(合法)字段值中替换,我们应该获得它们所代表的单个抽象值的完整描述。
另一篇6.031要求您编写的文档是REP暴露安全论点。这是一个注释,检查REP的每个部分,查看处理REP的该部分的代码(特别是有关参数和来自客户端的返回值,因为这是REP公开的地方),并给出了代码不公开REP的原因。
这里有一个例子Tweet它的REP不变、抽象功能和REP暴露的安全性都有完整的文档记录:
// Immutable type representing a tweet.
public class Tweet {
private final String author;
private final String text;
private final Date timestamp;
// Rep invariant:
// author is a Twitter username (a nonempty string of letters, digits, underscores)
// text.length <= 140
// Abstraction function:
// AF(author, text, timestamp) = a tweet posted by author, with content text,
// at time timestamp
// Safety from rep exposure:
// All fields are private;
// author and text are Strings, so are guaranteed immutable;
// timestamp is a mutable Date, so Tweet() constructor and getTimestamp()
// make defensive copies to avoid sharing the rep's Date object with clients.
// Operations (specs and method bodies omitted to save space)
public Tweet(String author, String text, Date timestamp) { ... }
public String getAuthor() { ... }
public String getText() { ... }
public Date getTimestamp() { ... }
}
注意,我们没有任何显式的REP不变条件timestamp(除了传统的假设timestamp!=null,我们对所有对象引用都有)。但我们仍然需要包括timestamp在REP公开安全性参数中,因为整个类型的不变性属性依赖于所有字段保持不变。
将上述论点与一个涉及可变的破碎论点的例子Date对象.
以下是支持RatNum.
// Immutable type representing a rational number.
public class RatNum {
private final int numerator;
private final int denominator;
// Rep invariant:
// denominator > 0
// numerator/denominator is in reduced form, i.e. gcd(|numerator|,denominator) = 1
// Abstraction function:
// AF(numerator, denominator) = numerator/denominator
// Safety from rep exposure:
// All fields are private, and all types in the rep are immutable.
// Operations (specs and method bodies omitted to save space)
public RatNum(int n) { ... }
public RatNum(int n, int d) throws ArithmeticException { ... }
...
}
请注意,不可变的REP特别容易为REP暴露的安全性辩护。
你可以找到的完整代码RatNum在GitHub上。
阅读练习
反对REP暴露
六、ADT规范可以讨论什么?
由于我们刚刚讨论了在何处记录REP不变和抽象函数,现在是更新规范可以谈论什么?.
抽象类型的规范T-它由其操作规范组成–应该只讨论客户可以看到的内容。这包括参数、返回值和其操作引发的异常。每当规范需要引用类型的值时T,它应该将值描述为抽象值,即抽象空间A中的数学值。
规范不应讨论REP空间R的表示或元素的细节。它应该考虑REP本身(私有字段)对客户端不可见,就像方法体及其局部变量被视为不可见一样。这就是为什么我们将REP不变和抽象函数写成类主体中的普通注释,而不是类之上的Javadoc注释。将它们编写为Javadoc注释会将它们作为类型规范的公共部分提交给它们,这将与REP独立性和信息隐藏集成在一起。
七、ADT不变量取代先决条件
现在让我们把很多东西放在一起。设计良好的抽象数据类型的一个巨大优点是,它封装并强制了我们必须在前提条件下规定的属性。例如,与这样的规范不同,它有一个复杂的前提条件:
/**
* @param set1 is a sorted set of characters with no repeats
* @param set2 is likewise
* @return characters that appear in one set but not the other,
* in sorted order with no repeats
*/
static String exclusiveOr(String set1, String set2);
我们可以使用捕获所需属性的ADT
/** @return characters that appear in one set but not the other */
static SortedSet<Character> exclusiveOr(SortedSet<Character> set1, SortedSet<Character> set2);
它击中了我们所有的目标
• 因为所需的条件(没有重复排序)可以在一个地方强制执行,SortedSet类型,并且因为Java静态检查起作用,防止使用不满足此条件的值,并在编译时出现错误。
• 它更容易理解,因为它要简单得多,而且它的名字SortedSet传达程序员需要知道的信息。
• 它更适合于改变,因为SortedSet现在可以在不改变的情况下改变exclusiveOr或者它的任何客户。
在本课程中,我们在早期习题集中使用先决条件的许多地方都会受益于自定义ADT。
阅读练习
在ADT中封装先决条件
八、如何建立不变量
不变量是一个对整个程序都是正确的属性–如果是关于一个对象的不变量,它会减少到对象的整个生命周期。
要保持不变,我们需要:
• 使不变量在对象的初始状态中变为真;
• 确保对象的所有更改都保持不变为真。
根据ADT操作的类型,这意味着:
• 创建者和生产者必须为新的对象实例建立不变量;
• 变异者和观察者必须保持不变。
REP暴露的风险使情况更加复杂。如果REP是公开的,那么对象可能会在程序中的任何地方被更改,而不仅仅是在ADT的操作中,而且我们不能保证在这些任意更改之后不变量仍然保持不变。因此,证明不变量的完整规则是:
结构感应。如果抽象数据类型的不变量是
1.由创作者和生产者设立的;
2.由变异者和观察者保存;以及
3.没有代表曝光,
那么,对于抽象数据类型的所有实例,不变量都是正确的。
阅读练习
结构感应
总结
• 不变量是ADT对象实例在对象的生存期内始终为真的属性。
• 一个好的ADT保留了它自己的不变量。不变量必须由创造者和生产者建立,并由观察者和变异者保存。
• REP不变量指定表示的法定值,并应在运行时使用checkRep().
• 抽象函数将具体的表示映射到它表示的抽象值。
• 表象暴露既威胁着表征的独立性,又威胁着不变性的保存。
今天阅读的主题与我们优秀软件的三个特性联系在一起,如下所示:
• 不受虫子的影响。一个好的ADT保留了它自己的不变量,这样这些不变量就不会那么容易受到ADT客户端错误的影响,并且在ADT本身的实现中,对不变量的违反可以更容易地被隔离。显式声明REP不变量,并在运行时使用checkRep(),更早地捕捉到误解和错误,而不是继续使用损坏的数据结构。
• 很容易理解。REP不变量和抽象函数解释了数据类型表示的含义,以及它与抽象的关系。
• 准备好换衣服了。抽象数据类型将抽象与具体表示分开,这样就可以更改表示而不必更改客户端代码。