当遵守Liskov替换原则(LSP)时,子类可以实现额外的接口吗?

时间:2017-06-08 17:47:44

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

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

2 个答案:

答案 0 :(得分:9)

Liskov替换原则与班级无关。它是关于类型。 Ruby没有类型作为语言功能,所以在语言功能方面谈论它们并没有多大意义。

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

在此协议上有一个依赖的mixin,即Enumerable。同样,由于没有与#34; protocol&#34;的概念相对应的Ruby构造,因此Enumerable无法声明此依赖关系。它只在the documentation粗略强调我的)的介绍段落中提到:

  

Enumerable mixin为集合类提供了多种遍历和搜索方法,并具有排序功能。 该类必须提供方法each,该方法会生成集合的连续成员。

那就是它。

Ruby中不存在协议和类型。他们存在于Ruby文档,Ruby社区,Ruby程序员的头脑中,以及Ruby代码中的隐含假设中,但它们从未在代码中体现过。

所以,谈论Ruby类的LSP是没有意义的(因为类不是类型),但是根据Ruby类型谈论LSP也没什么意义(因为没有类型)。你只能根据头脑中的类型来谈论LSP(因为你的代码中没有任何内容)。

好的,咆哮。但事实上,真的真的真的很重要。 LSP是关于类型的。类不是类型。有些语言如C ++,Java或C♯,其中所有类也是自动类型,但即使在这些语言中,将类型(规则和约束的规范)的概念与概念分开也很重要。 class(它是对象的状态和行为的模板),如果只是因为除了那些类型的类之外还有其他的东西(例如Java和C♯中的接口以及使用Java)。 In fact, the interface in Java is a direct port of the protocol from Objective-C,反过来来自Smalltalk社区。

呼。所以,遗憾的是,没有一个能回答你的问题:-D

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

  

我可以使用 S&lt;:T 类型的对象替换 T 类型的对象,而无需更改程序所需的属性。

例如,&#34;程序不会崩溃&#34;是一个理想的属性,所以我不应该通过用超类型的对象替换超类型的对象来使程序崩溃。或者您也可以从另一个方向查看它:如果我可以通过用类型的对象替换 T 类型的对象来违反程序的理想属性(例如,使程序崩溃) S ,然后 S 不是 T 的子类型。

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

  • 方法参数类型是逆变的,即如果重写方法,则子类型中的重写方法必须接受与重写方法相同类型或更一般类型的参数。
  • 方法返回类型是协变的,即子类型中的重写方法必须返回与重写方法相同的类型或更具体的类型。

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

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

这三条规则是限制方法签名的静态规则。利斯科夫的关键创新是四个行为规则,特别是第四条规则(&#34;历史规则&#34;):

  • 在子类型中无法强化前提条件,即如果用子类型替换对象,则不能对调用者施加额外限制,因为调用者不知道它们。
  • 后置条件不能在子类型中被削弱,即你不能放松超类型所做的保证,因为调用者可能会依赖它们。
  • 必须保留不变量,即如果超类型保证某些内容永远为真,那么它在子类型中也必须始终为真。
  • 历史规则:操纵子类型的对象不得创建从超类型的对象无法观察的历史记录。 (这个有点棘手,它意味着以下内容:如果我只通过 T 类型的方法观察到 S 类型的对象,我应该无法放置对象处于一种状态,即观察者看到的类型为 T 的对象是不可能的,即使我使用 S 的方法来操纵它。)< / LI>

前三条规则在Liskov之前已知,但它们是以证明理论的方式制定的,并未将别名考虑在内。规则的行为表述以及历史规则的添加使LSP适用于现代OO语言。

这是查看LSP的另一种方式:如果我有一个只知道并关心T的检查员,并且我给他一个S类型的对象,他是否能够发现这是一个&#34;假冒&#34;或者我可以欺骗他吗?

好的,最后你的问题是:添加sneer_majesticly方法是否违反了LSP?答案是:否。添加 new 方法的唯一方法是违反LSP,如果此 new 方法在这样的情况下操纵状态仅使用方法无法实现的方式。由于sneer_majesticly不会操纵任何状态,因此添加它不可能违反LSP。请记住:我们的检查员只知道Animal,即他只知道walkrun。他并不了解或关心sneer_majesticly

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

然而的! run 可能理论上违反了LSP。请记住:子类型的对象不能更改超类型的理想属性。现在,问题是: Animal的理想属性?问题是你没有提供Animal的任何文档,所以我们不知道它的理想属性是什么。我们唯一可以看到的是代码,它总是raise sa NotImplementedError(BTW实际上raiseNameError,因为没有名为{{的常量1)}在Ruby核心库中)。所以,问题是:是否需要属性的异常部分的NotImplementedError?没有文件,我们无法分辨。

如果raise定义如下:

Animal

然后它是LSP违规。

但是,如果class Animal # … # Makes the animal run. # # @return [void] # @raise [NotImplementedError] if the animal can't run def run raise NotImplementedError end end 定义如下:

Animal

然后它成为LSP违规。

换句话说:如果class Animal # … # Animals can't run. # # @return [never] # @raise [NotImplementedError] because animals never run def run raise NotImplementedError end end 的规范是&#34;总是引发异常&#34;,那么我们的检查员可以通过调用run来发现一只猫,并观察它没有&#39; ;提出异常。但是,如果run的规范是&#34;使动物运行或者引发异常&#34;,那么我们的检查员可以区分猫与动物。

您将注意到run在此示例中是否违反LSP实际上完全独立于Cat!它实际上也完全独立于Cat内的代码! 取决于文档。这是因为我在一开始就试图弄清楚:LSP是关于类型。 Ruby没有类型,因此类型只存在于程序员的头脑中。或者在此示例中:在文档注释中。

答案 1 :(得分:1)

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