软件构造心得(五)

  这篇博客开始进入抽象数据结构(ADT),个人认为,抽象数据结构是编码中极为重要的一环。

  抽象数据类型ADT

  抽象数据类型ADT,用于描述一个相对抽象的数据类型,它能够成功地将数据类型与实现的数据结构、使用形式分离开来,让用户只关注自己需要关注的抽象数据类型(与数据剥离开)。

  一、ADT中的方法(假设该ADT名为Object):

  根据各方法的功能,可以分为以下几类:Creator、Producer、Observer和Mutator。

  Creator是构造器,负责创建新的ADT对象(Object),一般是不输入参数创建初始对象或者输入参数创建新对象,如new();

  Producer是产生器,用于产生新的Object。一般是输入旧的Object,然后输入0至若干个其他类型的参数,产生一个新的Object,如"+"、”-“、”*“、”/“;

  Observer是观察器,用于产生用户所需要的与该对象相关的内部信息,如size()、contains();

  Mutator是变值器,用于修改Object的值,如add()、remove()。

  以上四种方法各有不同的地方。就返回值来看,Creator和Producer都是返回Object类型,Observer一般返回的是非Object类型的值,Mutator一般没有返回值或者返回的是是否修改成功等信息;就传入参数而言,Creator和Producer就有了区别,Creator不会传入Object类型的参数而Producer会,因为Creator是无中生有而Producer是以旧换新(可以是多对一的关系);就实现过程而言,Observer理论上没有任何修改操作,而Creator、Producer是返回新的Object(类似于immutable中的修改,复制副本后更新),Mutator则是直接对Object的值进行修改(因此很明显地,immutable类型的Object无Mutator)。

 

  二、表示独立性:

  表示独立性是为了防止内部表示的泄露,一方面用户并不需要关注其内部数据即实现方式,因此表示泄露只会增加用户的负担;另一方面,表示泄露还会影响用户使用的安全性,因为若知道了内部的表示,就可以无视规约和给定方法直接修改表示,如下面例子,因为tweet类型中的作者、时间和推文都是public类型,这会导致用户知道tweet的表示,从而使得用户可以直接使用以下方式(规约未给定的方法)恶意篡改,将作者名字替换等。

public class Tweet {
    public String author;
    public String text;
    public Date timestamp;

    ... ...
}

//已知一开始t对象如下,并直接修改其author来篡改名字
Tweet t = new Tweet("justinbieber", 
                    "Thanks to all those beliebers out there inspiring me every day", 
                    new Date());
                    
t.author = "rbmllr";

  因此为了保证表示独立性,最好将Tweet里面的field改为private类,以防止泄露,并按照给定的方法进行合法操作。

 

  在心得(四)中,着重介绍了immutable和mutable的差异,而表示独立性也离不开它们。在Creator、Producer和Mutator中,会涉及到很多关于修改的东西,可能内部某一个mutable类型的field就会在修改中指向外部的某一个对象(如上面timestamp可能指向外部的某一个Date对象)。当外部的Date对象被修改时,timestamp的值也会遭到修改,此时显然保证不了表示独立性。为了保证表示独立性,此时将类型改为immutable类型显然是很好的,但是可能在时间成本、维护难度上有些许提升。除了改为immutable类型之外,也可以在某一field前加上final(指向不变,即snapshot diagram中的双线箭头),从而使得指向的对象只有一个,不可改变指向对象(若对象为mutable则可改变其值)。当然还有在实现方法中的对参数的防御性拷贝、对返回值的拷贝等,都能够使得表示不泄露(不指向外部),从而保证表示的独立性。

 

  三、ADT的RI(Rep Invariant)和AF(Abstraction Function)

  AF,即抽象函数,是从R-->A的一个映射。对于抽象函数AF,它不一定是一个单射,一定是一个满射。先来解释一下R域和A域,R域是开发者实现ADT所用的数据类型构成的空间,而A域是用户所关注的空间。就数据类型性质而言,A域中代表的元素是相对抽象的,而R域是相对具象的。还是以上面Tweet类型为例子,A域就是推文这个空间,R域就是开发者为了实现推文所组成的(author, text, timestamp)的这个String X String X Date这三维笛卡尔乘积所组成的空间。

  然而,String、String和Date这三个类型随机所组成的元素不一定都满足它是一个推文,即三元组(author,text,timestamp)所组成的集合是String X String X Date 的子集(如(”“,”“,1990.13.32)就不是一个合法的推文,其中”“表示空字符串)。因此,AF不是一个单射(不合法元素无映射);同时对于每一个A域上的元素都有至少一个R域元素与之对应,否则它不会被用户所关注(无意义的元素)。

  那么我们用什么来判断R域中哪些元素是合法的呢?这就要用到RI(表示不变量)来进行规定。RI规定了R域中合法的能够映射到A域的元素集合,本质上是R域的一个子集,它也是合法元素的一个形式化定义。还是以上面推文的例子来看,它的RI是author、text都不为空且都只有”A-Z“、”a-z“"0-9"", . # ! @"组成,timestamp要满足合法公历日期。

  通过AF和RI,就可以将内部的具体实现映射到用户所需的抽象函数中,也是ADT的一个本质体现,RI规定了合法的R域,而AF则对每一个合法值进行了解释(说明其如何映射到抽象空间A域中)。而对于两个ADT,若R、RI和AF相同,可以视作它们是等价的。

  因此,在规约中规定了RI和AF之后,就要时刻遵守规约,每一步都要求新的Object满足其RI。而实际上如何判断新的Object是否满足RI,就可以通过根据RI手写heckRep()方法,在每一次产生新的Object或修改Object都调用判断即可(一定要满足封闭性,即满足RI的Object经过操作后产生的新Object仍要满足RI)。

 

  需要注意的是,RI可以在不同的方法中有所变化,如以下关于分数的例子:

@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 + "");
}

  在toString中,其RI必须满足denominator为正整数且numerator和denominator的最大公约数为1,然而在其他的操作中(如”+“操作),为了计算的简便,RI只需满足denominator为非0整数即可,可以不用化简,到最后输出才得到最简。

 

  四、构造ADT的注意事项:

  1、不得泄露内部表示,包括fields以及关于RI、AF、Safety from Rep Exposure等(规约只能用用户可见的内容,不能涉及内部表示);

  2、证明ADT是否保持不变量:Creator、Producer和Mutator产生的新对象是否满足RI;是否造成表示泄露。

posted @ 2022-06-12 12:31  立志马院的newbee  阅读(21)  评论(0编辑  收藏  举报