【MIT软件构造】Abstraction Functions & Rep Invariants

不变量(Invariants)

对于一个ADT,在它的整个生命周期中都应保持一个性质是不变的,这个性质便是不变量。如前文提到的类的不变性(Immutability)就是一个不变量,指在整个ADT的生命周期中,这个类的属性值均不应改变

表示暴露(representation exposure)

对于下列一个Tweet类,我们说它是存在表示暴露的,即这个ADT的属性暴露给了用户

/**
 * 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的属性值,可以发现public声明是罪魁祸首。

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

所以我们想当然的将public改为private,同时添加getter,如下:

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;
    }

}

但对下列用户使用的代码来说,它仍然存在表示泄露。由于Date是可变类型,用户通过getTimestamp()得到了Date的引用,进而能直接改变Date(不通过Tweet类的方法)。换句话来说,用户本来是想接受一个Tweet对象,返回一个新的Tweet对象,但却意外的把原来的Tweet对象改变了,这打破了Tweet的不变量:不可改变性(Immutability)。

/** @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);
}

思索片刻以后可以作出修改,getter返回一个新的可变类型Date副本给用户,使用户无法改变原Tweet对象,这种技术叫保护复制(defensive copy)

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

但你也许会在使用这个ADT的某一天发现,Tweet仍然存在表示泄露!可以看到由于Date是可变类型,向Tweet的构造器传入一个Date的引用产生一个Tweet对象后,我们可以通过改变这个Date来改变Tweet类内的Date!一切都是因为Date是可变类型:

/** @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副本,从此副本来构造Tweet,至此终于完美的解决了问题,设计出了一个较好的ADT

public Tweet(String author, String text, Date timestamp) {
    this.author = author;
    this.text = text;
    this.timestamp = new Date(timestamp.getTime());
}

需要指出的是在某些时候,表示暴露的去除十分复杂或几乎不可能,这个时候在规约中规定用户的行为成为没有方法的方法,比如上述Tweet类,规约可以强行规定用户不能改变传入的Date:

/**
 * 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) {

当然Java针对这一现象也提供了一些解决办法,称为不可变包裹(Immutable wrappers),如Collections.unmodifiableList()将List转换为一个新的“List”,而这个新的“List”失去了他的改变器(mutators),如set(), add(), remove()等,实现了从可变类型到不可变类型的转变。而当一个ADT终于去除了表示暴露的问题,我们应在注释中添上表示没有暴露的原因:

	// 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.

R空间 & A空间

af&ri的引入是从两个空间的定义开始的
R空间(space of representation values),指一个ADT内部实现所用到的所有数据存储、表示的结构,即这个ADT中的数据本来是什么样子的
A空间(space of abstract values),指一个ADT在用户看来包含了哪些数据的值,即这个ADT中的数据看起来是什么样子的

public class CharSet {
    private String s;
    ...
}

对于这个抽象数据类型CharSet,在某一个时刻他的R空间与A空间可能是这样的,R空间可能有很多String,但在用户看来,它里面只有两个字符串的集合。
在这里插入图片描述
可以看到上图的几个特征:
1、A空间中每个元素都有R空间元素与其对应
2、一个A空间中的元素可能有多个R空间元素与其对应
3、不是所有的R空间中的元素都对应到A空间中

抽象函数(Abstraction Functions) & 表示不变性(Rep Invariants)

抽象函数是一个从R空间映射到A空间的映射,如上图所示,它不一定是单射,但一定是满射
AF : R → A

表示不变性是一个从R空间映射到boolean的映射,当R空间的r元素映射到了A空间的某个元素时,RI(r)为true,否则为false
RI : R → boolean

下图绿色为满足RI的元素,红色为不满足RI的元素
在这里插入图片描述
AF&RI应该在代码中记录,下图记录对应上图(需要注意的是,AF&RI并不属于规约,由于涉及到内部实现,它不应被用户看到)

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

检测属性

由上图分析可知,我们应尽量减少RI映射为false的R空间数据,因为这一部分数据是没有任何用处的,甚至还会引起错误,所以在ADT中我们应时常检测属性是否出错(R空间是否出现了红色部分数据),一般称为checkRep。

// Check that the rep invariant is true
// *** Warning: this does nothing unless you turn on assertion checking
//     by running Java with -enableassertions (or -ea)
private void checkRep() {
    assert denominator > 0;
    assert gcd(Math.abs(numerator), denominator) == 1;
}

需要强调的是,这个函数应该在每次ADT的属性被创造或改变后调用检测,且修饰词应为private。别忘记前文提到的,我们应尽量避免null的出现,所以不妨放在checkRep里

有益的改变

通过AF&RI,我们可以重新认识不可变类型,我们只需要保证A空间是不可变的,但是R空间在某些时候可以通过一些改变简化我们的操作,而这些改变对于用户来说也是完全不可见的。

总结

1、对于一个ADT的不变量,任何对象的整个生命周期都应保持不变
2、一个好的ADT的不变量由构造器、产生器构建,由观察器、改变器遵守
3、表示不变性声明了ADT属性的合法值,通过checkRep检查
4、抽象函数是一个从具体类型空间到抽象类型空间的映射
5、表示暴露将影响表示不变性与表示独立性

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值