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

kon*_*ung 2 ruby oop liskov-substitution-principle solid-principles

考虑这个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
Run Code Online (Sandbox Code Playgroud)

方法是否sneer_majesticly违反LSP,仅在Cat上定义,因为Animal上没有实现这个接口也不需要它?

Jör*_*tag 11

利斯科夫替代原则与阶级无关.这是关于类型.Ruby没有类型作为语言功能,因此在语言功能方面谈论它们并没有多大意义.

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

有一个mixin 依赖于这个协议,即Enumerable.同样,由于没有与"协议"概念相对应的Ruby构造,因此无法Enumerable声明此依赖关系.它只在文件的介绍段落中提到(大胆强调我的):

Enumerable混入提供了集合类与几个遍历和搜索方法,以及排序的功能.该类必须提供一个方法each,该方法生成集合的连续成员.

而已.

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

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

好吧,咆哮.但是,这是真的,真的,真的,真的很重要.LSP是关于类型的.类不是类型.有些语言如C++,Java或C♯,其中所有类也是自动类型,但即使在这些语言中,将类型(规则和约束的规范)的概念与概念分开也很重要. class(它是对象的状态和行为的模板),如果只是因为除了那些类型的类之外还有其他东西(例如Java和C中的接口以及Java中的原语).事实上,interfaceJava是protocolObjective-C 的直接端口,后者又来自Smalltalk社区.

唷.所以,不幸的是,没有一个能回答你的问题:-D

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

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

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

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

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

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

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

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

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

前Liskov之前已经知道了前三个规则,但它们是以证明理论的方式制定的,没有考虑到混叠.规则的行为表述以及历史规则的添加使得LSP适用于现代OO语言.

这是查看LSP的另一种方式:如果我有一个只知道和关心的检查员T,并且我给他一个类型的对象S,他是否能够发现它是"伪造的"或者我能欺骗他吗?

好的,最后你的问题:添加sneer_majesticly方法是否违反了LSP?答案是:否.添加方法可能违反LSP 的唯一方法是,如果这种方法以仅使用方法不可能发生的方式操纵状态.由于不操纵任何状态,添加它不可能违反LSP.记住:我们的检查员只知道,即他只知道和.他不知道或不关心.sneer_majesticlyAnimalwalkrunsneer_majesticly

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

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

如果Animal定义如下:

class Animal
  # …

  # Makes the animal run.
  #
  # @return [void]
  # @raise [NotImplementedError] if the animal can't run
  def run
    raise NotImplementedError
  end
end
Run Code Online (Sandbox Code Playgroud)

然后它不会是LSP违规.

但是,如果Animal定义如下:

class Animal
  # …

  # Animals can't run.
  #
  # @return [never]
  # @raise [NotImplementedError] because animals never run
  def run
    raise NotImplementedError
  end
end
Run Code Online (Sandbox Code Playgroud)

那么这是一个LSP违规.

换句话说:如果规范run是"总是引发异常",那么我们的检查员可以通过调用run并观察它没有引发异常来发现猫.但是,如果规范run是"使动物运行或者引发异常",那么我们的检查员就无法将猫与动物区分开来.

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

  • 不,LSP适用于Ruby,我在答案中举了一个例子.您似乎认为因为Ruby没有类型,所以类型并不重要.事实并非如此.事实上,你可能会觉得*更*关于Ruby的类型比,也就是说,在Scala中,因为一切都发生在你的脑袋:该类型在程序可见,你必须自己记住他们,和类型没有用语言检查,你必须自己检查.如果方法的文档说明存在某个后置条件,并且您创建了违反该后置条件的子类型并且... (2认同)
  • ...尝试在程序中使用该子类型的对象,程序将在运行时爆炸,并且它爆炸的原因违反了LSP.在其基础上,LSP告诉您何时将一种类型的对象替换为另一种类型的对象是安全的.忽略LSP会使您的程序出错. (2认同)
  • ......在别人看来,你做不到.例如,查看Javadocs中的前置条件,后置条件和不变量的数量.并将它们与Eiffel进行比较,后者在语言中原生支持它们. (2认同)