Liskov 替换原则与类无关。这是关于类型的。 Ruby 没有将类型作为语言特性,因此从语言特性的角度来讨论它们是没有意义的。
在 Ruby(以及一般的 OO)中,类型基本上是协议。协议描述了对象响应哪些消息,以及它如何响应它们。例如,Ruby 中一个众所周知的协议是 迭代协议,它由单个消息 each 组成,该消息接受一个块,但没有位置或关键字参数和 yields 元素顺序地块。请注意,没有与此协议对应的类或 mixin。符合此协议的对象无法声明。
有一个依赖这个协议的mixin,即Enumerable。同样,由于没有对应于“协议”概念的 Ruby 构造,Enumerable 无法声明此依赖关系。仅在the documentation的介绍段中提到(bold强调我的):
Enumerable mixin 为集合类提供了几种遍历和搜索方法,以及排序能力。 该类必须提供一个方法each,它会产生集合的连续成员。
就是这样。
Ruby 中不存在协议和类型。它们确实存在于 Ruby 文档、Ruby 社区、Ruby 程序员的头脑中以及 Ruby 代码中的隐含假设中,但它们从未在代码中体现出来。
所以,用 Ruby 类来讨论 LSP 是没有意义的(因为类不是类型),但是用 Ruby 类型来讨论 LSP 也没有什么意义(因为没有类型)。您只能根据头脑中的类型来谈论 LSP(因为您的代码中没有任何类型)。
好了,废话不多说。但这真的,真的,真的,真的很重要。 LSP 是关于类型的。类不是类型。有像 C++、Java 或 C♯ 这样的语言,其中所有类也是自动类型,但即使在这些语言中,将类型的概念(它是规则和约束的规范)与类型的概念分开也很重要类(它是对象的状态和行为的模板),如果只是因为除了这些语言中的类型的类之外还有 other 东西(例如 Java 和 C 中的接口和原语爪哇)。 In fact, the interface in Java is a direct port of the protocol from Objective-C,来自 Smalltalk 社区。p>
呸。所以,不幸的是,这些都没有回答你的问题:-D
LSP 到底是什么意思? LSP 讨论子类型化。更准确地说,它定义了一个(在它被发明的时候)基于行为可替代性的子类型化的新概念。很简单,LSP 说:
我可以用 S <: t> 类型的对象替换 T 类型的对象,而无需更改程序所需的属性。
例如,“程序不会崩溃”是一个理想的属性,所以我不应该通过用子类型的对象替换超类型的对象来使程序崩溃。或者你也可以从另一个方向来看:如果我可以通过将 T 类型的对象替换为 类型的对象来违反程序的理想属性(例如,使程序崩溃) S,则 S 不是 T 的子类型。
我们可以遵循几条规则来确保我们不违反 LSP:
- 方法参数类型是逆变的,即,如果您重写一个方法,子类型中的重写方法必须接受与被重写方法相同类型或更通用类型的参数。
- 方法返回类型是协变的,即子类型中的覆盖方法必须返回与被覆盖方法相同的类型或更具体的类型。
这两个规则只是函数的标准子类型化规则,它们早在 Liskov 之前就已为人所知。
- 子类型中的方法不得引发任何新异常,这些异常不仅由超类型中的被覆盖方法引发,但其类型本身是被覆盖方法引发的异常的子类型的异常除外。
这三个规则是限制方法签名的静态规则。 Liskov 的关键创新是四个行为规则,特别是第四个规则(“历史规则”):
- 先决条件不能在子类型中得到加强,即如果用子类型替换对象,则不能对调用者施加额外的限制,因为调用者不知道这些限制。
- 不能在子类型中削弱后置条件,即您不能放松超类型做出的保证,因为调用者可能会依赖它们。
- 必须保留不变量,即如果超类型保证某事始终为真,那么它在子类型中也必须始终为真。
-
历史规则:操作子类型的对象不能创建无法从超类型的对象观察到的历史。 (这个有点棘手,它的意思如下:如果我只通过 T 类型的方法观察一个 S 类型的对象,我应该无法将对象处于一种状态,使得观察者看到的状态对于 T 类型的对象来说是不可能的,即使我使用 S 的方法来操作它。)
前三个规则在 Liskov 之前就已为人所知,但它们是以证明理论的方式制定的,并未考虑 别名。规则的行为制定,以及History Rule的加入,使得LSP适用于现代OO语言。
这是查看 LSP 的另一种方式:如果我有一个只知道和关心 T 的检查员,我递给他一个 S 类型的对象,他是否能够发现它是一个“假冒”还是我可以骗他?
好的,最后回答你的问题:添加sneer_majesticly 方法是否违反了LSP?答案是:不。添加 new 方法可能违反 LSP 的唯一方法是,如果此 new 方法以这样的方式操纵 old 状态仅使用 old 方法是不可能发生的。由于sneer_majesticly 不操纵任何状态,添加它不可能违反LSP。请记住:我们的检查员只知道Animal,即他只知道walk 和run。他不知道也不关心sneer_majesticly。
OTOH,如果你添加了一个方法 bite_off_foot,之后猫就不能再走路了,那么你违反了 LSP,因为通过调用 bite_off_foot,检查员可以,只使用他所知道的方法(walk 和 run)观察到了动物无法观察到的情况:动物总是可以走路,但我们的猫突然不能了!
然而! run 可能理论上违反了 LSP。请记住:子类型的对象不能更改超类型的所需属性。现在,问题是:Animal 的理想属性是什么是?问题是您没有为Animal 提供任何文档,所以我们不知道它的理想属性是什么。我们唯一能看到的是代码,它总是raises 和NotImplementedError(顺便说一句,实际上是raise 和NameError,因为在Ruby 核心库中没有名为NotImplementedError 的常量)。所以,问题是:raiseing 是否是理想属性的异常部分?没有文档,我们无法判断。
如果Animal 是这样定义的:
class Animal
# …
# Makes the animal run.
#
# @return [void]
# @raise [NotImplementedError] if the animal can't run
def run
raise NotImplementedError
end
end
那么它不会违反 LSP。
但是,如果 Animal 是这样定义的:
class Animal
# …
# Animals can't run.
#
# @return [never]
# @raise [NotImplementedError] because animals never run
def run
raise NotImplementedError
end
end
那么它会违反 LSP。
换句话说:如果run 的规范是“总是引发异常”,那么我们的检查员可以通过调用run 并观察它没有引发异常来发现一只猫。但是,如果 run 的规范是“让动物跑起来或者引发异常”,那么我们的检查员不能区分猫和动物。
你会注意到,在这个例子中Cat是否违反了LSP实际上是完全独立于Cat的!而且它实际上也完全独立于Animal里面的代码!它仅取决于文档。那是因为我在一开始就试图阐明:LSP 是关于types。 Ruby 没有类型,所以类型只存在于程序员的头脑中。或者在这个例子中:在文档 cmets 中。