child class 遵循里氏代换原则 (LSP) 可以实现额外的接口吗?

When adhering to Liskov Substitution Principle (LSP) can a child class implement additional interface?

考虑这个ruby例子

class Animal
  def walk
     # In our universe all animals walk, even whales
     puts "walking"
  end

  def run
    # Implementing to conform to LSP, even though only some animals run
    raise NotImplementedError
  end
end

class Cat < Animal
  def run
    # Dogs run differently, and Whales, can't run at all
    puts "running like a cat"
  end

  def sneer_majesticly
    # Only cats can do this. 
    puts "meh"
  end
end

方法 sneer_majesticly 是否违反 LSP,仅在 Cat 上定义,因为此接口在 Animal 上未实现也不需要?

LSP 表示您可以放弃基础 type/interface 的任何实现,它应该会继续工作。所以没有理由违反它,尽管它提出了有趣的问题,即为什么您需要在一个实现中而不是其他实现中实现该附加接口。您是否遵循单一职责原则?

Liskov 替换原则与 classes 无关。它大约是 类型 。 Ruby 没有类型 作为语言特性,因此从语言特性的角度谈论它们并没有什么意义。

在 Ruby(和一般的 OO)中,类型基本上是 协议 。协议描述对象响应哪些消息,以及它如何响应这些消息。例如,Ruby 中的一个著名协议是 迭代协议 ,它由一条消息 each 组成,该消息接受一个块,但没有位置或关键字参数和 yields 元素顺序到块。请注意,没有 class 或 mixin 对应于此协议。符合此协议的对象无法声明。

有一个混入依赖于这个协议,即Enumerable。同样,由于没有 Ruby 构造对应于 "protocol" 的概念,因此 Enumerable 无法声明此依赖关系。它仅在the documentation的介绍性段落中提到(粗体强调我的):

The Enumerable mixin provides collection classes with several traversal and searching methods, and with the ability to sort. The class must provide a method each, which yields successive members of the collection.

就是这样。

Ruby 中不存在协议和类型。它们 存在于 Ruby 文档中,存在于 Ruby 社区中,存在于 Ruby 程序员的头脑中,并且存在于 Ruby 中的隐含假设中] 代码,但它们从未出现在代码中。

所以,用 Ruby classes 来谈论 LSP 是没有意义的(因为 classes 不是类型),但是用 classes 来谈论 LSP Ruby 类型也没什么意义(因为没有类型)。你只能根据你脑子里的类型来谈论 LSP(因为你的代码中没有)。

好了,吐槽完了。但那是真的,真的真的真的很重要。 LSP 是关于类型的。 类 不是类型。有像 C++、Java 或 C♯ 这样的语言,其中所有 classes 也自动是类型,但即使在这些语言中,区分类型的概念(它是规则和约束)来自 class 的概念(它是对象状态和行为的模板),如果只是因为除了 [=202= 之外还有 other 东西]es 在这些语言中也是类型(例如 Java 和 C♯ 中的接口以及 Java 中的原语)。 In fact, the interface in Java is a direct port of the protocol from Objective-C,这又来自 Smalltalk 社区。

呸。所以,不幸的是 none 回答了你的问题 :-D

LSP 究竟是什么意思? LSP 谈论子类型化。更准确地说,它定义了一个(在它被发明的时候)基于 行为可替代性 的子类型化的新概念。很简单,LSP 说:

I can replace objects of type T with objects of type S <: T without changing the desirable properties of the program.

例如,"the program does not crash" 是可取的 属性,所以我不应该通过用子类型的对象替换超类型的对象来使程序崩溃。或者您也可以从另一个方向查看它:如果我可以通过将类型为 T 的对象替换为来违反程序的理想 属性(例如,使程序崩溃) S 类型的对象,那么 S 不是 T.

的子类型

我们可以遵循一些规则来确保我们不违反 LSP:

  • 方法参数类型是逆变的,即如果您覆盖一个方法,子类型中的覆盖方法必须接受与被覆盖方法相同类型或更通用类型的参数。
  • 方法 return 类型是协变的,即子类型中的覆盖方法必须 return 与覆盖方法相同或更具体的类型。

这两个规则只是函数的标准子类型化规则,它们在 Liskov 之前就已为人所知。

  • 子类型中的方法不得引发任何不仅由超类型中的重写方法引发的新异常,除非异常类型本身是被重写方法引发的异常的子类型。

这三个规则是限制方法签名的静态规则。 Liskov 的关键创新是四个行为规则,特别是第四个规则 ("History Rule"):

  • 前提条件不能在子类型中加强,即如果你用一个子类型替换一个对象,你不能对调用者施加额外的限制,因为调用者不知道它们。
  • 不能在子类型中削弱后置条件,即您不能放松超类型做出的保证,因为调用者可能依赖它们。
  • 必须保留不变量,即如果超类型保证某事永远为真,那么它在子类型中也必须永远为真。
  • 历史规则:操作子类型的对象不得创建无法从超类型的对象观察到的历史。 (这个有点棘手,它的意思是:如果我只通过 T 类型的方法观察一个 S 类型的对象,我应该无法将对象置于观察者看到类型 T 对象不可能看到的状态,即使我使用 的方法S 来操纵它。)

前三个规则在 Liskov 之前就已为人所知,但它们是以理论证明的方式制定的,没有考虑 别名。规则的行为制定和历史规则的添加使 LSP 适用于现代 OO 语言。

这里换一种方式来看待LSP:如果我有一个只知道和关心T的检查员,我递给他一个S类型的对象,他能不能发现它是 "counterfeit" 还是我可以愚弄他?

好的,最后是你的问题:添加 sneer_majesticly 方法是否违反了 LSP?答案是:不。添加 new 方法违反 LSP 的唯一方法是如果此 new 方法操作 old 以仅使用 方法不可能发生的方式陈述。由于 sneer_majesticly 不操纵任何状态,添加它不可能违反 LSP。请记住:我们的检查员只知道 Animal,即他只知道 walkrun。他不知道也不关心sneer_majesticly.

如果,OTOH,你添加了一个方法 bite_off_foot 之后猫就不能再走路了,那么 你违反了 LSP,因为通过调用 bite_off_foot,检查员仅用他知道的方法(walkrun)就可以观察到动物无法观察到的情况:动物总是可以走路,但我们的猫突然不能' t!

不过run 理论上可以 违反 LSP。请记住:子类型的对象不能更改超类型的所需属性。现在,问题是: Animal 的理想属性是什么?问题是您没有为 Animal 提供任何文档,所以我们不知道它的理想属性是什么。我们唯一可以看的是代码,它总是 raises a NotImplementedError(顺便说一句,实际上 raise a NameError,因为没有名为 NotImplementedError 在 Ruby 核心库中)。所以,问题是: 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 的规范是 "always raises an exception",那么我们的检查员可以通过调用 run 并观察它没有引发异常来发现猫。但是,如果 run 的规范是 "makes the animal run or else raises an exception",那么我们的检查员就不能 区分猫和动物。

你会注意到在这个例子中Cat是否违反LSP实际上完全独立于Cat!而且它实际上也完全独立于Animal里面的代码!它 取决于文档。那是因为我在一开始就试图弄清楚:LSP 是关于 types。 Ruby 没有类型,所以类型只存在于程序员的头脑中。或者在这个例子中:在文档注释中。