当遵循Liskov替换原理(LSP)时,子类可以实现附加接口吗? [英] When adhering to Liskov Substitution Principle (LSP) can a child class implement additional interface?

查看:92
本文介绍了当遵循Liskov替换原理(LSP)时,子类可以实现附加接口吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑这个红宝石示例

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是否违反了仅在Cat上定义的LSP,因为在Animal上未实现也不需要此接口?

解决方案

Liskov替换原理与类无关.这是关于类型的. Ruby没有类型作为语言功能,因此从语言功能的角度来谈论它们真的没有任何意义.

在Ruby(通常是OO)中,类型基本上是协议.协议描述了对象响应哪些消息,以及对象如何响应它们.例如,Ruby中的一个众所周知的协议是 iteration协议,它由一个带有块的单个消息each组成,但是没有位置或关键字参数以及yield元素依次块.请注意,没有对应于该协议的类或混合.符合此协议的对象无法声明.

有一个依赖的mixin,即Enumerable.同样,由于没有与协议"概念相对应的Ruby构造,因此Enumerable无法声明此依赖关系.仅在文档(粗体强调我的):

Enumerable混合为集合类提供了几种遍历和搜索方法,并具有排序的能力. 该类必须提供方法each,该方法可以产生集合的连续成员.

就是这样.

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

所以,用Ruby类来谈论LSP是没有意义的(因为类不是类型),但是用Ruby类型来谈论LSP也没有意义(因为没有类型).您只能用头脑中的类型来谈论LSP(因为您的代码中没有).

好的,大声疾呼.但这真的很重要,确实真正真正. LSP是关于类型的.类不是类型.像C ++,Java或C♯这样的语言,所有类也都是自动类型,但是即使在这些语言中,将类型的概念(规则和约束的规范)与a的概念区分开也是很重要的.类(这是对象状态和行为的模板),仅是因为除了这些语言类型的类之外,还有其他东西(例如Java和C things中的接口以及Java中的原语) Java). 实际上,Java中的interfaceprotocol的直接端口来自Objective-C ,后者又来自Smalltalk社区.

Ph.因此,不幸的是,这些都没有回答您的问题:-D

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

我可以将 T 类型的对象替换为 S< ;: T 类型的对象,而无需更改程序的所需属性.

例如,程序不会崩溃"是一个理想的属性,因此我不应该通过用子类型的对象替换超类型的对象来使程序崩溃.或者,您也可以从另一个方向查看它:如果我可以通过将类型为 T 的对象替换为类型为的对象来违反程序的期望属性(例如,使程序崩溃) S ,则 S 不是 T 的子类型.

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

  • 方法参数类型是互变的,即,如果您覆盖方法,则子类型中的覆盖方法必须接受与被覆盖方法相同类型或更通用的参数.
  • 方法返回类型是协变的,即子类型中的覆盖方法必须返回与覆盖方法相同的类型或更具体的类型.

这两个规则只是函数的标准子类型化规则,早在Liskov之前就已经知道.

  • 子类型中的方法不得引发不仅由父类型中的重写方法引发的任何新异常,除非其类型本身是该重写方法引发的异常的子类型.

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

  • 不能在子类型中增强先决条件,即,如果用子类型替换对象,则由于调用者不知道这些条件,因此无法对调用者施加其他限制.
  • 在子类型中不能削弱后置条件,即您不能放松超类产生的保证,因为调用者可能依赖于它们.
  • 必须保留不变量,即,如果超类型保证某些东西始终为真,那么在子类型中它也必须始终为真.
  • 历史规则:操纵子类型的对象一定不能创建无法从超类型的对象观察到的历史. (这有点棘手,它表示以下意思:如果仅通过 T 类型的方法观察到 S 类型的对象,则我不能放置对象的状态,即使我使用 S 的方法来操作,观察者也会看到 T 类型的对象无法实现的状态.)

前三个规则在Liskov之前就已为人所知,但它们是以一种证明理论的方式制定的,没有考虑到 aliasing .规则的行为表述以及历史记录规则的添加使LSP适用于现代OO语言.

这是查看LSP的另一种方法:如果我有一个仅了解并关心T的检查员,并且将其交给S类型的对象,他将能够发现它是一个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的任何文档,因此我们不知道其所需的属性是什么.我们唯一可以看的是代码,它总是raise s NotImplementedError(由于Ruby核心库中没有名为NotImplementedError的常量,因此BTW实际上将是raise a NameError).因此,问题是:异常属性的raise是否属于所需属性?没有文档,我们无法分辨.

如果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没有类型,因此类型仅存在于程序员的脑海中.或在此示例中:在文档注释中.

Consider this ruby example

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

Does method sneer_majesticly violate LSP, being defined only on Cat, since this interfaces is not implemented nor needed on Animal?

解决方案

The Liskov Substitution Principle has nothing to do with classes. It is about types. Ruby doesn't have types as a language feature, so it doesn't really make sense to talk about them in terms of language features.

In Ruby (and OO in general), types are basically protocols. Protocols describe which messages an object responds to, and how it responds to them. For example, one well-known protocol in Ruby is the iteration protocol, which consists of a single message each which takes a block, but no positional or keyword arguments and yields elements sequentially to the block. Note that there is no class or mixin corresponding to this protocol. There is no way for an object which conforms to this protocol to declare so.

There is a mixin which depends on this protocol, namely Enumerable. Again, since there is no Ruby construct which corresponds to the notion of "protocol", there is no way for Enumerable to declare this dependency. It is only mentioned in the introductory paragraph of the documentation (bold emphasis mine):

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.

That's it.

Protocols and types don't exist in Ruby. They do exist in Ruby documentation, in the Ruby community, in the heads of Ruby programmers, and in implicit assumptions in Ruby code, but they are never manifest in the code.

So, talking about the LSP in terms of Ruby classes makes no sense (because classes aren't types), but talking about the LSP in terms of Ruby types makes little sense either (because there are no types). You can only talk about the LSP in terms of the types in your head (because there aren't any in your code).

Okay, rant over. But that is really, really, really, REALLY important. The LSP is about types. Classes aren't types. There are languages like C++, Java, or C♯, where all classes are also automatically types, but even in those languages it is important to separate the notion of a type (which is a specification of rules and constraints) from the notion of a class (which is a template for the state and behavior of objects), if only because there are other things besides classes which are types in those languages as well (e.g. interfaces in Java and C♯ and primitives in Java). In fact, the interface in Java is a direct port of the protocol from Objective-C, which in turn comes from the Smalltalk community.

Phew. So, unfortunately none of this answers your question :-D

What, exactly, does the LSP mean? The LSP talks about subtyping. More precisely, it defines a (at the time it was invented) new notion of subtyping which is based on behaviorial substitutability. Very simply, the LSP says:

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

For example, "the program does not crash" is a desirable property, so I should not be able to make a program crash by replacing objects of a supertype with objects of a subtype. Or you can also view it from the other direction: if I can violate a desirable property of a program (e.g. make the program crash) by replacing an object of type T with an object of type S, then S is not a subtype of T.

There are a couple of rules we can follow to make sure that we don't violate the LSP:

  • Method parameter types are contravariant, i.e. if you override a method, the overriding method in the subtype must accept parameters of the same types or more general types as the overridden method.
  • Method return types are covariant, i.e. the overriding method in a subtype must return the same type or a more specific type as the overridden method.

These two rules are just the standard subtyping rules for functions, they were known long before Liskov.

  • Methods in subtypes must not raise any new exceptions that are not only raised by the overridden method in the supertype, except for exceptions whose types are themselves subtypes of the exceptions raised by the overridden method.

These three rules are static rules restricting the signature of methods. The key innovation of Liskov were the four behavioral rules, in particular the fourth rule ("History Rule"):

  • Preconditions cannot be strengthened in a subtype, i.e. if you replace an object with a subtype, you cannot impose additional restrictions on the caller, since the caller doesn't know about them.
  • Postconditions cannot be weakened in a subtype, i.e. you cannot relax guarantees that the supertype makes, since the caller may rely on them.
  • Invariants must be preserved, i.e. if the supertype guarantees that something will always be true, then it must also always be true in the subtype.
  • History Rule: Manipulating the object of a subtype must not create a history that is impossible to observe from objects of the supertype. (This one is a bit tricky, it means the following: if I observe an object of type S only through methods of type T, I should not be able to put the object in a state such that the observer sees a state that would not be possible with an object of type T, even if I use methods of S to manipulate it.)

The first three rules were known before Liskov, but they were formulated in a proof-theoretical manner which didn't take aliasing into account. The behavioral formulation of the rules, and the addition of the History Rule make the LSP applicable to modern OO languages.

Here is another way to look at the LSP: if I have an inspector who only knows and cares about T, and I hand him an object of type S, will he be able to spot that it is a "counterfeit" or can I fool him?

Okay, finally to your question: does adding the sneer_majesticly method violate the LSP? And the answer is: No. The only way that adding a new method can violate LSP is if this new method manipulates old state in such a way that is impossible to happen using only old methods. Since sneer_majesticly doesn't manipulate any state, adding it cannot possibly violate LSP. Remember: our inspector only knows about Animal, i.e. he only knows about walk and run. He doesn't know or care about sneer_majesticly.

If, OTOH, you were adding a method bite_off_foot after which the cat can no longer walk, then you violate LSP, because by calling bite_off_foot, the inspector can, by only using the methods he knows about (walk and run) observe a situation that is impossible to observe with an animal: animals can always walk, but our cat suddenly can't!

However! run could theoretically violate LSP. Remember: objects of a subtype cannot change desirable properties of the supertype. Now, the question is: what are the desirable properties of Animal? The problem is that you have not provided any documentation for Animal, so we have no idea what its desirable properties are. The only thing we can look at, is the code, which always raises a NotImplementedError (which BTW will actually raise a NameError, since there is no constant named NotImplementedError in the Ruby core library). So, the question is: is the raiseing of the exception part of the desirable properties or not? Without documentation, we cannot tell.

If Animal were defined like this:

class Animal
  # …

  # Makes the animal run.
  #
  # @return [void]
  # @raise [NotImplementedError] if the animal can't run
  def run
    raise NotImplementedError
  end
end

Then it would not be an LSP violation.

However, if Animal were defined like this:

class Animal
  # …

  # Animals can't run.
  #
  # @return [never]
  # @raise [NotImplementedError] because animals never run
  def run
    raise NotImplementedError
  end
end

Then it would be an LSP violation.

In other words: if the specification for run is "always raises an exception", then our inspector can spot a cat by calling run and observing that it doesn't raise an exception. However, if the specification for run is "makes the animal run or else raises an exception", then our inspector can not differentiate a cat from an animal.

You will note that whether or not Cat violates the LSP in this example is actually completely independent of Cat! And it is in fact also completely independent of the code inside Animal! It only depends on the documentation. That is because of what I tried to make clear in the very beginning: the LSP is about types. Ruby doesn't have types, so the types only exist in the programmer's head. Or in this example: in documentation comments.

这篇关于当遵循Liskov替换原理(LSP)时,子类可以实现附加接口吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆