6. 测试抽象数据类型
-我们通过为每个操作创建测试来为自动测试构建测试套件
-这些测试不可避免地会相互影响。
-测试构造器、生产器和变值器的唯一方法是对结果对象调用观察器,同样,测试观察器的唯一方法是创建对象供他们观察。
▪ 测试creators, producers, and mutators:调用observers来观察这些operations的结果是否满足spec;
▪ 测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。
▪ 风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效
7. 不变量
7.1.ADT的不变量
- 一个好的抽象数据类型最重要的属性是它保留了自己的不变量。ADT需要始终保持其不变量
- 不变量是程序的一个属性,对于程序的每个可能的运行时状态,它总是真的。不变量:程序在任何时候总是true的性质
-Immutability是一个至关重要的不变量:一旦创建,一个不可变的对象应该在其整个生命周期内始终代表相同的值。例如:immutability就是一个典型的"不变量" - 说ADT保存自己的不变量意味着ADT负责确保自己的不变量保持不变。由ADT来负责其不变量,与客户端的任何行为无关
-它不依赖客户的良好行为。
-正确性不依赖于其他模块。
7.2.为什么需要不变量?
- 当ADT保存自己的不变量时,关于代码的推理变得容易得多。为什么需要不变量:保持程序的"正确性",容易发现错误
-如果你可以相信Strings永远不会改变,那么当你调试使用Strings的代码时,或者当你试图为另一个使用Strings的ADT建立不变量时,您可以排除这种可能性。
-与字符串类型形成对比,字符串类型保证只有在其客户端承诺不改变它时才是不可变的。然后,你必须检查代码中可能使用字符串的所有地方。如果没有这个不变量,那么在所有使用String的地方,都要检查其是否改变了 - 假设客户会试图破坏不变量(恶意黑客或诚实的错误),总是要假设客户有"恶意"破坏ADT不变量的行为----防御性编程
7.3.如果是可变的
- 不变性的第一个威胁来自于客户可以直接访问它的字段
这是表示暴露的一个例子 - 这意味着类外的代码可以直接修改表示。不仅影响不变量,也影响了表示独立性:无法在不影响客户端的情况下改变其内部表示
–像这样的重复曝光不仅威胁到不变量,还威胁到表示独立性。
–我们不能在不影响所有直接访问这些字段的客户端的情况下更改Tweet的实现。
7.4.如何使不变量不可变
- private和pubilc关键字表示哪些字段和方法只能在类内访问,哪些可以从类外部访问。
- final关键字也有助于保证这个不可变类型的字段在对象构造后不会被重新分配。
7.5.要把责任交给客户吗?
你可能会反对,因为觉得以上似乎是浪费。为什么我们不能仅仅通过一个精心编写的规范来解决这个问题?
- 当没有其他合理的选择时,有时会采用这种方法——例如,当可变对象太大而无法有效复制时。当复制代价很高时,不得不这么做
- 但是你对程序进行推理的能力和避免错误的能力的成本是巨大的, 由此引发的潜在bug也将很多
7.6.使用不可变类型而不是可变类
在没有令人信服的相反论点的情况下,抽象数据类型保证其自身的不变量几乎总是值得的,防止重复暴露对此至关重要。
除非迫不得已,否则不要把希望寄托于客户端上,ADT有责任保证自己的不变量,并避免"表示泄露"。
最好的办法就是使用不变的的类型,彻底避免表示泄露。如果我们使用了一个不可变的日期对象,比如java.time.ZonedDateTime,而不是可变的java.util.Date,那么我们在讨论了公共和私有之后就结束了这一部分。不可能有进一步的重复曝光。
7.7.总结
- 不要将可变参数并入对象;制造防御武器
- 返回可变字段的防御副本…
–返回新实例,而不是修改 - 或者返回可变字段的不可修改视图
- 使用不可变的组件,消除防御性复制的需要
- 保持不变性和避免表示泄露,是ADT作重要的一个不变法则
8.Rep不变量和抽象函数(ADT理论)
8.1.两个值空间
R:表示值空间(rep值)
由实际实现实体的值组成。
表示值构成的空间:实现者看到和使用的值
- ADT将作为单个对象实现,但更常见的是需要一个小的对象网络,因此该值通常相当复杂。一般情况下ADT的表示比较简单,有些时候需要复杂表示
A:抽象值空间
由该类型设计支持的值组成。
抽象值构成的空间:客户端看到和使用的值
- 它们是柏拉图式的实体,并不像描述的那样存在,但它们是我们作为抽象类型的客户来看待抽象类型元素的方式。
- 例如,无界整数的抽象类型可能将数学整数作为其抽象值空间;例如,它可能被实现为一个基元(有界)整数数组,这一事实与该类型的用户无关
8.2.两个值空间举例
抽象类型的实现者必须对表示值感兴趣,因为实现者的工作是使用表示值空间实现抽象值空间的意象。
ADT开发者关注表示空间R,客户关注抽象空间A
例如,假设我们选择使用一个字符串来表示一组字符:
然后表示空间R包含字符串,抽象空间A是数学字符集。
8.3.R和A之间的映射
1)每个抽象值都被映射到一个表示值(满射).
- 实现抽象类型的目的是支持对抽象值的操作。据推测,我们需要能够创建和操作所有可能的抽象值,并且它们必须因此具有代表性。
2)一些抽象值被多个表示值映射(未必单射).
- 出现这种情况是因为表示不是严格的编码。将无序的字符集表示为字符串的方式不止一种。
3)并非所有表示值都已映射(未必双射).
- 在这种情况下,字符串“abbc”没有映射。在这种情况下,我们决定字符串不应包含重复项。这将允许我们在遇到特定角色的第一个实例时终止remove方法,因为我们知道最多只能有一个。
8.4.抽象函数
抽象函数:将表示值映射到它们所代表的抽象值,R和A之间映射关系的函数,即如何将R中的每一个值解释为A中的每一个值。
AF : R → A
图表中的弧线显示了抽象功能。
- 在函数的术语中,这些性质可以通过这样的说法来表达,即函数是满射的(也称为上射),不一定是内射的(一对一),因此不一定是双射的,并且通常是部分的。
注:
AF: 满射、非单射、未必双射
R中的部分值并非合法的,在A中无映射值
8.5.表示不变量(RI):另一个重要的ADT不变量
将表示值映射到布尔值的表示不变量:
RI : R →布尔值
对于表示值r,当且仅当r由AF映射时,RI( r )为真。
换句话说,RI告诉我们给定的表示值是否格式正确。
或者,你可以把RI看作一个集合:它是表示值的子集,在这个子集上定义了AF(抽象函数)
- 表示不变性RI:某个具体的"表示"是否是"合法的"
- 也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值
- 也可将RI看作:一个条件,描述了什么是"合法"的表示值
8.6.记录RI和AF
表示不变量和抽象函数都应该记录在代码中,紧挨着表示值本身的声明:
8.7.是什么决定了AF和RI?
抽象值空间本身不能决定AF或RI:
- 同一抽象类型可以有多种表示。
- 一组字符同样可以表示为字符串,如上所述,或者表示为位向量,每个可能的字符对应一位。
- 显然,我们需要两个不同的抽象函数来映射这两个不同的表示值空间。
- 同一个ADT,可以有多种表示;不同的内部表示,需要设计不同的AF和RI
定义表示值的类型,并因此选择表示值空间的值,并不决定哪些表示值将被视为合法,而那些合法的表示值将如何解释。
也就是说:选择某种特定的表示方式R,进而指定某个子集是"合法"的(RI),并为该子集中的每个值做出"解释“(AF)——即如何映射到抽象空间中的值
例如,如果我们允许字符串中有重复,但同时要求字符排序,以不减少的顺序出现,那么将有相同的表示值空间,但不同的表示不变量(RI)。也就是说:同样的表示空间R,可以有不同的RI
即使表示值空间的类型相同,RI也相同,我们仍然可以用不同的抽象函数来解释表示值。也就是:即使是同样的R、同样的RI,也可能有不同的AF,即"解释不同"。
- 也许我们会将连续的字符对解释为子范围,因此字符串rep“acgg”被解释为两个范围对[a-c]和[g-g],因此代表集合{a,b,c,g}
8.8RI和AF如何影响ADT的设计
关键是设计一个抽象类型不仅意味着选择两个空间——specification(规约)的抽象值空间和implementation(实现)的表示值空间——还意味着决定使用什么表示值以及如何解释它们。
- 设计ADT:
-(1)选择R和A;
-(2) RI --------合法的表示值;
-(3) 如何解释合法的表示值------映射AF
做出具体的解释:每个表示值如何映射到抽象值
在你的代码中写下这些假设是至关重要的,这样未来的程序员(和你未来的自己)就知道这个表示实际上意味着什么。而且要把这种选择和解释明确写到代码当中。
例子:有理数的ADT
观察器方法可以不用,但建议也要检查,以防止你的"万一":观察器方法通常不需要调用checkRep(),但无论如何这样做是很好的防御做法。
- 在每个方法中调用checkRep(),包括观察器,意味着您更有可能捕捉到由rep暴露导致的RI的违规。