【问题标题】:When adhering to Liskov Substitution Principle (LSP) can a child class implement additional interface?遵循 Liskov 替换原则 (LSP) 时,子类可以实现附加接口吗?
【发布时间】:2017-06-09 10:36:53
【问题描述】:

考虑这个 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 上没有实现也不需要此接口?

【问题讨论】:

    标签: ruby oop solid-principles liskov-substitution-principle


    【解决方案1】:

    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,即他只知道walkrun。他不知道也不关心sneer_majesticly

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

    然而run 可能理论上违反了 LSP。请记住:子类型的对象不能更改超类型的所需属性。现在,问题是:Animal 的理想属性是什么?问题是您没有为Animal 提供任何文档,所以我们不知道它的理想属性是什么。我们唯一能看到的是代码,它总是raises 和NotImplementedError(顺便说一句,实际上是raiseNameError,因为在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 中。

    【讨论】:

    • 真的,真的,真的很好的答案。谢谢你,乔格花时间!所以 SOLID 不能真正应用于 Ruby OOP,只有 SOID(因为你关于类型的观点)? :-)
    • 不,LSP 适用于 Ruby,我在回答中举了一个例子。您似乎认为因为 Ruby 没有类型,所以类型并不重要。那不是真的。事实上,你可能需要更多地考虑 Ruby 中的类型而不是 Scala,因为一切都发生在你的脑海中:类型在程序中是不可见的,你必须自己记住它们,并且类型不是由语言检查的,您必须自己检查它们。如果方法的文档说存在某个后置条件,并且您创建了一个违反该后置条件的子类型并且......
    • ... 尝试在程序中使用该子类型的对象,程序会在运行时崩溃,而导致崩溃的原因是违反了 LSP。在它的基础上,LSP 告诉您何时可以安全地将一种类型的对象替换为另一种类型的对象。忽略 LSP 会使您的程序出错。
    • LSP 对文档的依赖让我觉得这个原则的弱点在于未记录的代码数量和不准确的记录代码数量。要求准确的文档来应用 LSP 似乎是在实践中很少实现的先决条件。我相信文档在 SOLID 原则中是 LSP 独有的,这似乎使其远不如其他原则实用。
    • ……在其他情况下,你不能。例如,查看 Javadocs 中前置条件、后置条件和不变量的数量。并将它们与 Eiffel 进行比较,后者在语言中原生支持它们。
    【解决方案2】:

    LSP 说你可以加入任何基本类型/接口的实现,它应该可以继续工作。因此,它没有理由违反这一点,尽管它提出了一些有趣的问题,即为什么您需要在一个实现而不是其他实现中实现该附加接口。您是否遵循单一责任原则?

    【讨论】:

    • 嗯,冷笑,是猫独有的能力。那么,在您看来,为什么它不符合 SRP?或者你是在暗示我应该使用接口隔离原则?我不认为 ISP 在这里适用,因为我没有对动物实施嘲笑,只是对猫。我的意思是嘲笑的内部可以调用另一个类,但它实际上与继承自 Animal 的其他类分开
    • 如果你有一些使用Animal接口的代码,它会不知道冷笑,永远不要调用那个方法。您这样做的唯一原因是,如果您在其他地方使用了 Cat 用于其他目的,这表明 Cat 有不止一项责任。
    • 所以你建议实现一个类 Sneer,它需要一个动物,比如猫,然后让它冷笑?附:我也更新了我的热门评论
    • 至于使用 Animal 的代码 - 我不希望它知道或能够使 Animal 冷笑。只有猫应该能够做到这一点。我从没见过鹦鹉,会冷笑的:-)
    猜你喜欢
    • 2014-08-16
    • 1970-01-01
    • 2019-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-18
    • 1970-01-01
    相关资源
    最近更新 更多