在不破坏 LSP 的情况下覆盖虚拟布尔纯方法
Overriding virtual boolean pure method without LSP breaking
例如我们有以下结构:
class Base
{
[pure]
public virtual bool IsValid(/*you can add some parameters here*/)
{
//body
}
}
class Child : Base
{
public override bool IsValid(/*you can add some parameters here*/)
{
//body
}
}
请问Base::IsValid()
和Child::IsValid()
可以填不同的body但不和LSP冲突吗?假设它只是分析方法,我们不能改变实例的状态。
我们能做到吗?
我对任何例子都感兴趣。
我试图了解虚拟(实体)布尔方法在一般情况下是否是 反模式。
LSP 的思想并不禁止子 classes 的多态性。相反,它强调什么可以更改,什么不能更改。
一般来说,这意味着:
- 任何覆盖函数接受和returns相同类型的覆盖函数;包括可能抛出的异常(输入类型可能会扩展被覆盖的类型,输出类型可能会缩小它们 - 这仍然会保留该限制)。
- "History Rule" - Child 对象的 "Base" 部分不得由 Child 的函数更改为使用 Base class 函数永远无法达到的状态。因此,期望 Base 对象的函数永远不会得到意外的结果。
- 不能在 Child 中更改 Base 的不变量。也就是说,关于 Base class 行为的任何一般假设都必须由 Child 保留。
前两个项目符号非常明确。 "invariants" 更有意义。例如,如果实时环境中的某些 class 要求其所有功能 运行 在某个恒定时间内,则其子类型中的所有重写功能也必须遵守该要求。
在您的情况下,IsValid() 意味着某些东西,并且 "something" 必须保留在所有子类型下。例如,假设您的 Base class 定义了一个产品,并且 IsValid() 告诉您该产品是否可以销售。使每种产品有效的确切原因可能有所不同。例如,它必须设置价格才能有效出售。但儿童产品也必须经过电力测试才能销售。
在此示例中,我们保留所有要求:
- 函数的输入输出类型不变。
- Child 对象的 Base-part 的状态没有以 Base 无法预期的方式改变 class。
- 保留了class的不变量:没有价格的Child物件仍然卖不出去; not being valid的意思还是一样(不允许出售),只是按照child匹配的方式计算而已。
你可以得到更多的解释here。
===
编辑-根据注释进行一些补充说明
多态性的整个思想是相同的功能由不同的子类型以不同的方式完成。 LSP 不违反多态性,但描述了多态性应该注意什么。
特别是,LSP 要求在代码需要 Base
的地方可以使用任何子类型 Child
,并且为 Base
所做的任何假设都适用于他的任何 Child
。在上面的例子中,IsValis()
并不意味着"has a price"。相反,它的意思恰恰是:产品有效吗?在某些情况下,有价格就足够了。在其他情况下,它还需要电力检查,但在其他情况下,它可能还需要一些其他属性。如果 Base
class 的设计者不要求通过设置价格使产品生效,而是将 IsValid()
作为单独的测试,则不会违反 LSP。什么例子会造成这种违规行为?一个示例,其中询问对象是否 IsValid()
,然后调用基 class 的函数 ,该函数不应更改有效性,并且该函数更改 Child
不再有效。这违反了 LSP 的历史规则。此处其他人提供的已知示例是正方形作为矩形的子项。但是只要相同的函数调用序列不需要特定的行为(同样 - 没有定义设置价格使产品有效;它只是在某些类型中恰好是这样) - LSP 是按要求持有的.
LSP 背后的基本思想不是阻碍 Override
Base
class 方法的能力,而是避免改变 Base
[=28] 的内部状态=](改变 base class class 的数据成员)以 Base class 不会有的方式。
It simply states: Any type (class) that inherits another type, must be
substitutive to that type, so that if Child
class inherits Base
class, then anywhere in the code where an object of Base
class is
expected, we can provide a Child
class object without changing the
system behavior.
但这并不妨碍我们修改Child class的成员。违反此示例的著名示例是 Square/Rectangle 问题。您可以找到示例 here.
的详细信息
在您的情况下,由于您只是分析 IsValid()
中的一些数据,并没有修改 Base
class 的内部状态,因此不应该违反 LSP。
Barbara Liskov,Jeannette Wing 1994:
“令 q(x) 是关于类型 T 的对象 x 的 属性 可证明的。那么 q(y) 应该对于类型 S 的对象 y 是可证明的
其中 S 是 T 的子类型”.
简而言之:当代码的行为不改变时,基类型可以被子类型替换。这意味着一些固有的限制。
这里有一些例子:
异常
class Duck { void fly() {} }
class RedheadDuck : Duck { void fly() {} }
class RubberDuck : Duck { void fly() { throw new CannotFlyException(); }}
class LSPDemo
{
public void Main()
{
Duck p = new Duck ();
p.fly(); // OK
p = new RedheadDuck();
p.fly(); // OK
p = new RubberDuck();
p.fly(); // Fail, not same behavior as base class
}
}
方法参数的逆变
class Duck { void fly(int height) {} }
class RedheadDuck : Duck { void fly(long height) {} }
class RubberDuck : Duck { void fly(short height) {} }
class LSPDemo
{
public void Main()
{
Duck p = new Duck(); p.fly(int.MaxValue);
p = new RedheadDuck(); p.fly(int.MaxValue); // OK argumentType long(Subtype) >= int(Basetype)
p = new RubberDuck(); p.fly(int.MaxValue); // Fail argumentType short(Subtype) < int(Basetype)
}
}
return 类型的协方差
class Duck { int GetHeight() { return int.MaxValue; } }
class RedheadDuck: Duck { short GetHeight() { return short.MaxValue; } }
class RubberDuck: Duck { long GetHeight() { return long.MaxValue; } }
class LSPDemo {
public void Main()
{
Duck p = new Duck(); int height = p.GetHeight();
p = new RedheadDuck(); int height = p.GetHeight(); // OK returnType short(Subtype) <= int(Basetype)
p = new RubberDuck(); int height = p.GetHeight(); // Fail returnType long(Subtype) > int(Basetype)
}
}
历史限制
class Duck
{
protected string Food { get; private set; }
protected int Age { get; set; }
public Duck(string food, int age)
{
Food = food;
Age = age;
}
}
class RedheadDuck : Duck
{
void IncrementAge(int age)
{
this.Age += age;
}
}
class RubberDuck : Duck
{
void ChangeFood(string newFood)
{
this.Food = newFood;
}
}
class LSPDemo
{
public void Main()
{
Duck p = new Duck("apple", 10);
p = new RedheadDuck();
p.IncrementAge(1); // OK
p = new RubberDuck();
p.ChangeFood("pie"); // Fail, Food is defined as private set in base class
}
}
还有更多...希望您能理解。
首先,您的回答:
class Base
{
[pure]
public virtual bool IsValid()
{
return false;
}
}
class Child : Base
{
public override bool IsValid()
{
return true;
}
}
基本上,LSP 说(它是 "subtype" 的定义):
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. (Liskov, 1987)
"But I can't replace an o1
of type Base
by any o2
of type Child
, because they obviously behave differently!"针对这个言论,我们要绕道而行
什么是子类型?
首先,请注意 Liskov 不仅在谈论 classes,而且在谈论类型。 类 是类型的实现。类型的实现有好有坏。我们将尝试区分它们,尤其是在涉及子类型时。
里氏替换原则背后的问题是:什么是子类型?通常,我们假设子类型是其超类型的特化及其功能的扩展:
> The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra (Liskov, 1987)
另一方面,大多数编译器假定子类型是 class,它至少具有相同的方法(相同的名称、相同的签名,包括协变和异常),继承或重新定义(或为第一次)和标签(inherits
、extends
、...)。
但是这些标准是不完整的并且会导致错误。这里有两个臭名昭著的例子:
SortedList
是 (?) List
的子类型:它代表一个排序的列表(专业化)。
Square
是 (?) Rectangle
的子类型:它代表一个四边相等的矩形(特化)。
为什么 SortedList
不是 List
?由于 List
类型的语义。类型不仅是签名的集合,方法也具有语义。 在语义上,我指的是对象的所有授权使用(记住维特根斯坦:"the meaning of a word is its use in the language")。例如,您希望在放置它的位置找到一个元素。但是如果列表总是排序的,新插入的元素将被移动到它的 "right" 位置。因此,您不会在放置它的位置找到该元素。
为什么 Square
不是 Rectangle
?假设您有一个方法 set_width
:对于正方形,您也必须更改高度。但是 set_width
的语义是改变宽度但保持高度不变。
(正方形不是长方形吗?这个问题有时会引起激烈的讨论,所以我会详细说明这个问题。我们都知道正方形是长方形。但是在纯数学的天空中确实如此, 其中对象是不可变的。如果您定义 ImmutableRectangle
(具有固定的宽度、高度、位置、角度和计算的周长、面积...),那么 ImmutableSquare
将是 [=31 的子类型=] 根据 LSP。乍一看,这种不可变的 classes 似乎不是很有用,但有一种方法可以解决这个问题:用创建新对象的方法替换 setter,就像你在任何函数式语言。例如,ImmutableSquare.copyWithNewHeight(h)
将 return 一个新的... ImmutableRectangle
,其高度为 h
,宽度为正方形的 size
。)
我们可以使用 LSP 来避免这些错误。
为什么我们需要 LSP?
但是为什么,在实践中,我们需要关心 LSP 吗? 因为编译器不捕获 class。您可能有一个子class 不是子类型的实现。
对于 Liskov(和 Wing,1999),类型规范包括:
- The type's name
- A description of the type's value space
- A definition of the type's invariant and history properties;
- For each of the type's method:
- Its name;
- Its signature (including signaled exceptions);
- Its behavior in terms of pre-conditions and post-conditions
如果编译器能够为每个 class 执行这些规范,它将能够(在编译时或运行时,取决于规范的性质)告诉我们:"hey, this is not a subtype!" .
(实际上,有一种试图捕捉语义的编程语言:Eiffel。在 Eiffel 中,不变量、前置条件和 post 条件是定义 class。因此,您不必关心 LSP:运行时会为您完成。那会很好,但 Eiffel 也有局限性。这种语言(任何语言?)的表达力都不够定义 isValid()
的完整语义,因为此语义不包含在 pre/post 条件或不变量中。)
现在,回到这个例子。在这里,我们对 isValid
语义的唯一指示是方法的名称:如果对象有效,它应该 return 为真,否则为假。您显然需要上下文(可能还有详细的规范或领域知识)来了解什么是有效的,什么是无效的。
实际上,我可以想象十几种情况,其中 Base
类型的任何对象都有效,但 Child
类型的所有对象都无效(请参阅答案顶部的代码)。例如。将 Base
替换为 Passport
,将 Child
替换为 FakePassword
(假设假密码是密码......)。
因此,即使 Base
class 表示:"I'm valid",Base
类型表示:"Almost all of my instances are valid, but those who are invalid should say it!" 这就是为什么您有 Child
class 实现 Base
类型(并派生 Base
class)表示:"I'm not valid".
一个更有趣的例子
但我认为您选择的示例不是检查 pre/post 条件和不变量的最佳示例:由于该函数是纯函数,它可能不会按设计破坏任何不变量;由于 return 值是一个布尔值(2 个值),所以没有有趣的 post 条件。如果你有一些参数,你唯一可以拥有的就是一个有趣的前提条件。
我们举一个更有趣的例子:一个集合。在伪代码中,你有:
abstract class Collection {
abstract iterator(); // returns a modifiable iterator
abstract size();
// a generic way to set a value
set(i, x) {
[ precondition:
size: 0 <= i < size() ]
it = iterator()
for i=0 to i:
it.next()
it.set(x)
[ postcondition:
no_size_modification: size() = old size()
no_element_modification_except_i: for all j != i, get(j) == old get(j)
was_set: get(i) == x ]
}
// a generic way to get a value
get(i) {
[ precondition:
size: 0 <= i < size() ]
it = iterator()
for i=0 to i:
it.next()
return it.get()
[ postcondition:
no_size_modification: size() = old size()
no_element_modification: for all j, get(j) == old get(j) ]
}
// other methods: remove, add, filter, ...
[ invariant: size_positive: size() >= 0 ]
}
这个集合有一些抽象方法,但是set
和get
方法已经是实体方法了。此外,我们可以说,他们
对于链表是可以的,但对于数组支持的列表则不行。让我们尝试为随机访问集合创建一个更好的实现:
class RandomAccessCollection {
// all pre/post conditions and invariants are inherited from Collection.
// fields:
// self.count = number of elements.
// self.data = the array.
iterator() { ... }
size() { return self.count; }
set(i, x) { self.data[i] = x }
get(i) { return self.data[i] }
// other methods
}
显然get
和RandomAccessCollection
中的set
的语义符合Collection
class的定义。特别是,满足所有 pre/post 条件和不变量。换句话说,LSP
的条件得到满足,因此 LSP 得到尊重:我们可以在每个程序中用 模拟 [= 替换任何 Collection
类型的对象133=] 类型 RandomAccesCollection
的对象而不破坏程序的行为。
结论
如你所见,尊重LSP比打破它容易。但有时我们会破坏它(例如,尝试创建一个继承 RandomAccessCollection
的 SortedRandomAccessCollection
)。 crystal LSP 的明确表述有助于我们缩小错误范围以及应该采取哪些措施来纠正设计。
更一般地说,如果基础 class 有足够的内容来实现方法,则虚拟(实体)布尔方法不是反模式。但是如果基础 class 太抽象以至于每个子class 都必须重新定义方法,那么就让方法抽象。
参考资料
Liskov 有两篇主要的原创论文:Data Abstraction and Hierarchy (1987) and Behavioral Subtyping Using Invariants and Constraints(1994 年、1999 年,与 J. M. Wing 合着)。请注意,这些是理论论文。
例如我们有以下结构:
class Base
{
[pure]
public virtual bool IsValid(/*you can add some parameters here*/)
{
//body
}
}
class Child : Base
{
public override bool IsValid(/*you can add some parameters here*/)
{
//body
}
}
请问Base::IsValid()
和Child::IsValid()
可以填不同的body但不和LSP冲突吗?假设它只是分析方法,我们不能改变实例的状态。
我们能做到吗?
我对任何例子都感兴趣。
我试图了解虚拟(实体)布尔方法在一般情况下是否是 反模式。
LSP 的思想并不禁止子 classes 的多态性。相反,它强调什么可以更改,什么不能更改。 一般来说,这意味着:
- 任何覆盖函数接受和returns相同类型的覆盖函数;包括可能抛出的异常(输入类型可能会扩展被覆盖的类型,输出类型可能会缩小它们 - 这仍然会保留该限制)。
- "History Rule" - Child 对象的 "Base" 部分不得由 Child 的函数更改为使用 Base class 函数永远无法达到的状态。因此,期望 Base 对象的函数永远不会得到意外的结果。
- 不能在 Child 中更改 Base 的不变量。也就是说,关于 Base class 行为的任何一般假设都必须由 Child 保留。
前两个项目符号非常明确。 "invariants" 更有意义。例如,如果实时环境中的某些 class 要求其所有功能 运行 在某个恒定时间内,则其子类型中的所有重写功能也必须遵守该要求。
在您的情况下,IsValid() 意味着某些东西,并且 "something" 必须保留在所有子类型下。例如,假设您的 Base class 定义了一个产品,并且 IsValid() 告诉您该产品是否可以销售。使每种产品有效的确切原因可能有所不同。例如,它必须设置价格才能有效出售。但儿童产品也必须经过电力测试才能销售。
在此示例中,我们保留所有要求:
- 函数的输入输出类型不变。
- Child 对象的 Base-part 的状态没有以 Base 无法预期的方式改变 class。
- 保留了class的不变量:没有价格的Child物件仍然卖不出去; not being valid的意思还是一样(不允许出售),只是按照child匹配的方式计算而已。
你可以得到更多的解释here。
===
编辑-根据注释进行一些补充说明
多态性的整个思想是相同的功能由不同的子类型以不同的方式完成。 LSP 不违反多态性,但描述了多态性应该注意什么。
特别是,LSP 要求在代码需要 Base
的地方可以使用任何子类型 Child
,并且为 Base
所做的任何假设都适用于他的任何 Child
。在上面的例子中,IsValis()
并不意味着"has a price"。相反,它的意思恰恰是:产品有效吗?在某些情况下,有价格就足够了。在其他情况下,它还需要电力检查,但在其他情况下,它可能还需要一些其他属性。如果 Base
class 的设计者不要求通过设置价格使产品生效,而是将 IsValid()
作为单独的测试,则不会违反 LSP。什么例子会造成这种违规行为?一个示例,其中询问对象是否 IsValid()
,然后调用基 class 的函数 ,该函数不应更改有效性,并且该函数更改 Child
不再有效。这违反了 LSP 的历史规则。此处其他人提供的已知示例是正方形作为矩形的子项。但是只要相同的函数调用序列不需要特定的行为(同样 - 没有定义设置价格使产品有效;它只是在某些类型中恰好是这样) - LSP 是按要求持有的.
LSP 背后的基本思想不是阻碍 Override
Base
class 方法的能力,而是避免改变 Base
[=28] 的内部状态=](改变 base class class 的数据成员)以 Base class 不会有的方式。
It simply states: Any type (class) that inherits another type, must be substitutive to that type, so that if
Child
class inheritsBase
class, then anywhere in the code where an object ofBase
class is expected, we can provide aChild
class object without changing the system behavior.
但这并不妨碍我们修改Child class的成员。违反此示例的著名示例是 Square/Rectangle 问题。您可以找到示例 here.
的详细信息在您的情况下,由于您只是分析 IsValid()
中的一些数据,并没有修改 Base
class 的内部状态,因此不应该违反 LSP。
Barbara Liskov,Jeannette Wing 1994:
“令 q(x) 是关于类型 T 的对象 x 的 属性 可证明的。那么 q(y) 应该对于类型 S 的对象 y 是可证明的
其中 S 是 T 的子类型”.
简而言之:当代码的行为不改变时,基类型可以被子类型替换。这意味着一些固有的限制。
这里有一些例子:
异常
class Duck { void fly() {} } class RedheadDuck : Duck { void fly() {} } class RubberDuck : Duck { void fly() { throw new CannotFlyException(); }} class LSPDemo { public void Main() { Duck p = new Duck (); p.fly(); // OK p = new RedheadDuck(); p.fly(); // OK p = new RubberDuck(); p.fly(); // Fail, not same behavior as base class } }
方法参数的逆变
class Duck { void fly(int height) {} } class RedheadDuck : Duck { void fly(long height) {} } class RubberDuck : Duck { void fly(short height) {} } class LSPDemo { public void Main() { Duck p = new Duck(); p.fly(int.MaxValue); p = new RedheadDuck(); p.fly(int.MaxValue); // OK argumentType long(Subtype) >= int(Basetype) p = new RubberDuck(); p.fly(int.MaxValue); // Fail argumentType short(Subtype) < int(Basetype) } }
return 类型的协方差
class Duck { int GetHeight() { return int.MaxValue; } } class RedheadDuck: Duck { short GetHeight() { return short.MaxValue; } } class RubberDuck: Duck { long GetHeight() { return long.MaxValue; } } class LSPDemo { public void Main() { Duck p = new Duck(); int height = p.GetHeight(); p = new RedheadDuck(); int height = p.GetHeight(); // OK returnType short(Subtype) <= int(Basetype) p = new RubberDuck(); int height = p.GetHeight(); // Fail returnType long(Subtype) > int(Basetype) } }
历史限制
class Duck { protected string Food { get; private set; } protected int Age { get; set; } public Duck(string food, int age) { Food = food; Age = age; } } class RedheadDuck : Duck { void IncrementAge(int age) { this.Age += age; } } class RubberDuck : Duck { void ChangeFood(string newFood) { this.Food = newFood; } } class LSPDemo { public void Main() { Duck p = new Duck("apple", 10); p = new RedheadDuck(); p.IncrementAge(1); // OK p = new RubberDuck(); p.ChangeFood("pie"); // Fail, Food is defined as private set in base class } }
还有更多...希望您能理解。
首先,您的回答:
class Base
{
[pure]
public virtual bool IsValid()
{
return false;
}
}
class Child : Base
{
public override bool IsValid()
{
return true;
}
}
基本上,LSP 说(它是 "subtype" 的定义):
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. (Liskov, 1987)
"But I can't replace an o1
of type Base
by any o2
of type Child
, because they obviously behave differently!"针对这个言论,我们要绕道而行
什么是子类型?
首先,请注意 Liskov 不仅在谈论 classes,而且在谈论类型。 类 是类型的实现。类型的实现有好有坏。我们将尝试区分它们,尤其是在涉及子类型时。
里氏替换原则背后的问题是:什么是子类型?通常,我们假设子类型是其超类型的特化及其功能的扩展:
> The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra (Liskov, 1987)
另一方面,大多数编译器假定子类型是 class,它至少具有相同的方法(相同的名称、相同的签名,包括协变和异常),继承或重新定义(或为第一次)和标签(inherits
、extends
、...)。
但是这些标准是不完整的并且会导致错误。这里有两个臭名昭著的例子:
SortedList
是 (?)List
的子类型:它代表一个排序的列表(专业化)。Square
是 (?)Rectangle
的子类型:它代表一个四边相等的矩形(特化)。
为什么 SortedList
不是 List
?由于 List
类型的语义。类型不仅是签名的集合,方法也具有语义。 在语义上,我指的是对象的所有授权使用(记住维特根斯坦:"the meaning of a word is its use in the language")。例如,您希望在放置它的位置找到一个元素。但是如果列表总是排序的,新插入的元素将被移动到它的 "right" 位置。因此,您不会在放置它的位置找到该元素。
为什么 Square
不是 Rectangle
?假设您有一个方法 set_width
:对于正方形,您也必须更改高度。但是 set_width
的语义是改变宽度但保持高度不变。
(正方形不是长方形吗?这个问题有时会引起激烈的讨论,所以我会详细说明这个问题。我们都知道正方形是长方形。但是在纯数学的天空中确实如此, 其中对象是不可变的。如果您定义 ImmutableRectangle
(具有固定的宽度、高度、位置、角度和计算的周长、面积...),那么 ImmutableSquare
将是 [=31 的子类型=] 根据 LSP。乍一看,这种不可变的 classes 似乎不是很有用,但有一种方法可以解决这个问题:用创建新对象的方法替换 setter,就像你在任何函数式语言。例如,ImmutableSquare.copyWithNewHeight(h)
将 return 一个新的... ImmutableRectangle
,其高度为 h
,宽度为正方形的 size
。)
我们可以使用 LSP 来避免这些错误。
为什么我们需要 LSP?
但是为什么,在实践中,我们需要关心 LSP 吗? 因为编译器不捕获 class。您可能有一个子class 不是子类型的实现。
对于 Liskov(和 Wing,1999),类型规范包括:
- The type's name
- A description of the type's value space
- A definition of the type's invariant and history properties;
- For each of the type's method:
- Its name;
- Its signature (including signaled exceptions);
- Its behavior in terms of pre-conditions and post-conditions
如果编译器能够为每个 class 执行这些规范,它将能够(在编译时或运行时,取决于规范的性质)告诉我们:"hey, this is not a subtype!" .
(实际上,有一种试图捕捉语义的编程语言:Eiffel。在 Eiffel 中,不变量、前置条件和 post 条件是定义 class。因此,您不必关心 LSP:运行时会为您完成。那会很好,但 Eiffel 也有局限性。这种语言(任何语言?)的表达力都不够定义 isValid()
的完整语义,因为此语义不包含在 pre/post 条件或不变量中。)
现在,回到这个例子。在这里,我们对 isValid
语义的唯一指示是方法的名称:如果对象有效,它应该 return 为真,否则为假。您显然需要上下文(可能还有详细的规范或领域知识)来了解什么是有效的,什么是无效的。
实际上,我可以想象十几种情况,其中 Base
类型的任何对象都有效,但 Child
类型的所有对象都无效(请参阅答案顶部的代码)。例如。将 Base
替换为 Passport
,将 Child
替换为 FakePassword
(假设假密码是密码......)。
因此,即使 Base
class 表示:"I'm valid",Base
类型表示:"Almost all of my instances are valid, but those who are invalid should say it!" 这就是为什么您有 Child
class 实现 Base
类型(并派生 Base
class)表示:"I'm not valid".
一个更有趣的例子
但我认为您选择的示例不是检查 pre/post 条件和不变量的最佳示例:由于该函数是纯函数,它可能不会按设计破坏任何不变量;由于 return 值是一个布尔值(2 个值),所以没有有趣的 post 条件。如果你有一些参数,你唯一可以拥有的就是一个有趣的前提条件。
我们举一个更有趣的例子:一个集合。在伪代码中,你有:
abstract class Collection {
abstract iterator(); // returns a modifiable iterator
abstract size();
// a generic way to set a value
set(i, x) {
[ precondition:
size: 0 <= i < size() ]
it = iterator()
for i=0 to i:
it.next()
it.set(x)
[ postcondition:
no_size_modification: size() = old size()
no_element_modification_except_i: for all j != i, get(j) == old get(j)
was_set: get(i) == x ]
}
// a generic way to get a value
get(i) {
[ precondition:
size: 0 <= i < size() ]
it = iterator()
for i=0 to i:
it.next()
return it.get()
[ postcondition:
no_size_modification: size() = old size()
no_element_modification: for all j, get(j) == old get(j) ]
}
// other methods: remove, add, filter, ...
[ invariant: size_positive: size() >= 0 ]
}
这个集合有一些抽象方法,但是set
和get
方法已经是实体方法了。此外,我们可以说,他们
对于链表是可以的,但对于数组支持的列表则不行。让我们尝试为随机访问集合创建一个更好的实现:
class RandomAccessCollection {
// all pre/post conditions and invariants are inherited from Collection.
// fields:
// self.count = number of elements.
// self.data = the array.
iterator() { ... }
size() { return self.count; }
set(i, x) { self.data[i] = x }
get(i) { return self.data[i] }
// other methods
}
显然get
和RandomAccessCollection
中的set
的语义符合Collection
class的定义。特别是,满足所有 pre/post 条件和不变量。换句话说,LSP
的条件得到满足,因此 LSP 得到尊重:我们可以在每个程序中用 模拟 [= 替换任何 Collection
类型的对象133=] 类型 RandomAccesCollection
的对象而不破坏程序的行为。
结论
如你所见,尊重LSP比打破它容易。但有时我们会破坏它(例如,尝试创建一个继承 RandomAccessCollection
的 SortedRandomAccessCollection
)。 crystal LSP 的明确表述有助于我们缩小错误范围以及应该采取哪些措施来纠正设计。
更一般地说,如果基础 class 有足够的内容来实现方法,则虚拟(实体)布尔方法不是反模式。但是如果基础 class 太抽象以至于每个子class 都必须重新定义方法,那么就让方法抽象。
参考资料
Liskov 有两篇主要的原创论文:Data Abstraction and Hierarchy (1987) and Behavioral Subtyping Using Invariants and Constraints(1994 年、1999 年,与 J. M. Wing 合着)。请注意,这些是理论论文。