省略 super() and/or *弱化* 前提条件是否违反里氏替换原则?

Does omitting super() and/or *weakening* preconditions violate the Liskov Substitution Principle?

我最近一直在研究一些 SOLID 设计原则,我从一个来源获得的一些信息最初对我来说很有意义,但基于我能够为 thr LSP 找到的严格定义,似乎该信息可能不正确。信息具体是:

1) 不在重写的方法上回调 super() 违反了 LSP(或者至少让你容易违反)因为基础 class 的行为可能会在某些时候改变,你的子class 可能会以导致子 class 不再可替代 parent 的方式缺少该行为。这对我来说似乎很有意义,但如果有人可以详细说明 that/give 一些关于什么时候不回调 super 是合适的信息,那就太好了。

2) Subclasses 的限制不应低于 parent。示例是:如果您有一个只接受正整数的 parent class,那么您创建一个接受正整数和负整数的 subclass。所以 child 应该可以很好地代替 parent,但是 child 在这种情况下不能委托回 super。

我认为这是有道理的,但 LSP 上的信息似乎恰恰相反:child 不能 加强 先决条件。两者对我来说似乎都有意义,但 Liskov 只声明前置条件不能加强,后置条件不能削弱。有人可以帮我解释一下吗?

1) 案例,什么时候不回调 super

通常(但不总是)这种情况意味着 class 层次结构设计有问题。

如果子方法接受正确的输入类型和 returns 正确的输出类型,则不调用超级 class 实现不会违反 LSP。 仅表示问题的可能性。

这是一个当你不调用 super 方法时绝对有效的例子:

class Animal

   void eat(Food food)
       // Eat the food

class Cat extends Animal

   void meow()
       // Say meow


class AnimalOwner

   Animal findAPet() 
       return new Animal()

class CatOwner

   // we can return the subclass of Animal here
   Cat findAPet() 
       return new Cat() // We don't need to use the parent implementation

这里是 CatOwner::findAPet() returns a Cat (an Animal subclass),这在 LSP 方面是有效的,我们不调用父实现。

请注意,调用父实现并不能保证您不会陷入与不调用时相同的问题。

考虑这个例子:

 class Child

    Output doSomething()
        parent = super::doSomething()
        if parent->isNotGood():
           return new OutputSubclass()  // We called super, but we return 
                                        // something different, might be not safe
        else:
           return parent                // We called super and return 
                                        // the same value, safe

2) "Preconditions cannot be strengthened in a subtype"。这也意味着先决条件(与输入参数相关的期望)可以保持不变或被削弱。 在你说的例子中,前置条件实际上被弱化了,所以不存在冲突:

Parent::doSomething(PositiveInteger value)  // Only positive integers

Child::doSomething(Integer value)           // Positive or negative integers, 
                                            // pre-condition is weaker 
                                            // (covers wider area of values)

不完全正确的是第一句:"Subclasses should not be less restrictive than the parent"。 当我们谈论前置条件(输入参数)时,Subclasses 可以减少限制,这就是示例中显示的内容。

Liskov 替换原则要求您可以在需要基类型时使用子类型。为此,您必须遵守基本类型的约定。对于具有方法 f、前置条件 Pre、后置条件 Post 和不变量 I 的基础 class B,这意味着

  • 调用者代码仅保证 Pre 在可能使用您的对象时保持不变。如果 Pre 成立,您的先决条件也必须成立。逻辑上 Pre 表示派生方法的前提条件。就是说可以放宽前置条件因为Pre -> (Pre and condition),只要不矛盾
  • 调用方代码期望 Post 在方法完成时保持不变。这意味着你可以增加更多的保证,但你不能削弱Post条件。
  • 你的子class的不变量必须与基class的不变量兼容。

我会考虑显式调用 base-class 实现代码味道(构造函数除外)。使用 te,plate method pattern(or non-virtual interface in C++) 来执行 base-class 合约要好得多。在 Python 中,这看起来像:

class Base:
   def publicMethod(self, x):
       // do something
       self.templateMethod(x)
       // do something else

   def templateMethod(self, x):
       // has to be overriden by sub-classes
       // it provides extension hooks, but
       // publicMethod ensures that the overall
       // functionality is correct