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