【问题标题】:Does overloading (not overriding) break the Liskov subsitution principle?重载(而不是覆盖)是否会破坏 Liskov 替换原则?
【发布时间】:2020-05-22 07:51:41
【问题描述】:

有大量关于 LSP 的讨论,但所有讨论似乎都过于模糊。
AFAIK,LSP 声明要正确覆盖(而不是重载)子类中的超类方法,应确保子类方法:

  1. 不会产生父方法在任何情况下都不会引发的新型异常
  2. 与父方法具有相同的签名(在强类型语言的情况下)
  3. 与签名具有相同的语义意义
  4. 返回相同类型的值
  5. 返回相同语义的值

semantic meaning 我的意思是,如果基类方法暗示它返回 int 而这个 int 意味着,比如说,美元或欧元,那么重载的方法也应该暗示返回值是美元或欧元,并且即使在一种情况下返回 'RUB' 也会违反 LSP。

但是如果我的基类看起来像这样(示例是在 Python 中):

class A:

    func(x: int) -> int
        return x*2

class B(A):

    func(x: int, y: string) -> int
        return x*y

所以我的问题中有两个子问题:

  1. 术语contract 更实际的含义是什么?是 interface 的同义词吗?
  2. 使用基类中不存在的签名(作为参数列表的一部分)进行重载是否违反 LSP?

【问题讨论】:

  • 合约同接口。它只是更适合同一事物的另一个词。
  • 您是在专门谈论 Python,还是这种语言不可知论?我看到您最初没有添加语言标签,但是当@SergeyBerezovskiy 添加了python标签时并没有恢复。

标签: python oop design-patterns liskov-substitution-principle


【解决方案1】:

答案取决于语言。

在Java、C++、C#等典型的stronly-typed OO语言中,当你编写像a.func(b,c)这样的方法调用时,实际调用的方法是根据方法名和接收类型确定的(在此示例中为 a 的类型),以及参数的数量和类型。

在这样的语言中,具有不同数量参数或不同类型参数的方法是完全不同的方法。拥有不同数量的参数就像拥有不同的名称一样。当你“重载”一个方法时,就像创建一个不同名称的方法一样,所以你不会因为从基类中重载一个方法而违反 LSP。

不过,您的问题似乎与 python 有关,并且在典型的动态类型语言(如 python、JavaScript 等)中,当您编写像 a.func(b,c) 这样的方法调用时,仅按名称查找要调用的方法(看起来在与接收对象关联的表中)。在这样的语言中,没有方法或函数的重载

在您的示例中,您已覆盖具有两个参数函数的 func 的单参数定义。这意味着您的派生类的消费者不能再使用一个参数调用func,这确实违反了 LSP。

【讨论】:

    【解决方案2】:

    在现实生活中,合同是甲方和乙方对彼此的期望,即他们提供的保证。编程也一样:

    • 此方法接受什么范围的参数?
    • 您可以从中获得什么行为?
    • 什么时候抛出错误?

    它是方法的接口和文档的组合。

    当我们说一个孩子应该遵循相同的契约时,我们的意思是,如果我们用另一个 ChildC 替换实现,那么就会遵循与父级中描述的相同的约束,相同的行为。

    例如在 Java 中,我们有 Map 的概念(类似于 Python 中的 Dict),并且有 2 种不同的实现:HashMap 和 LinkedHashMap。一个不保留加法的顺序,另一个 - 保留。但是在合同中(在界面及其文档中)没有人说应该保留订单。因此,如果我们的代码使用了 HashMap,然后我们将其替换为 LinkedHashMap - 它仍然可以完成父文档和签名所要求的一切。

    还有另一个实现:ConcurrentHashMap。现在,它违反了 LSP。因为 HashMap,LinkedHashMap 可以有空键,而 ConcurrentHashMap 不能。因此,我们不能只用另一种实现替换一个实现——如果客户端实际上将 null 放在那里,它可能会破坏事情。

    Map 文档说某些实现可能不接受空值,但这更像是一种 hack。

    至于重载 - 您正在创建一个具有相同名称的单独方法,它不必遵循第一个合同。这些方法可能根本没有共同之处。尽管如其他答案中所述 - Python 中没有方法重载,但您只是用新方法替换旧方法,因此在子类中完成时它是一种覆盖。

    PS:如果您使用策略或命令 - 实现会完全不同。所以 LSP 在这里不适用。但他们仍然需要遵循父级中描述的通用合同。

    【讨论】:

      【解决方案3】:

      要了解 LSP 的含义,“要求不多,承诺不少”的口号很有帮助。覆盖方法不能要求更多参数或更具体类型的参数(即它们必须是逆变的)。它必须承诺返回与被覆盖方法相同或更具体类型的值(即,它必须是协变的)。

      语义约束是覆盖方法必须在子类中和在超类中做“同样的事情”。这种做同样事情的概念不能很容易地形式化,并且取决于类本身的含义。但是,如果覆盖方法调用了被覆盖的方法,这是语义一致性的强烈标志。

      重载与 LSP 无关。允许不同的方法具有相同的名称只是一个编译时技巧。

      【讨论】:

      • 为什么是-1?
      • 感谢您的解释,我很感激?但是,我写的内容也适用于动态语言。如果您在 Python 中覆盖单参数方法,并且在子类方法中期望该参数是更专业的类型,那么您将创建一个场景,其中子类方法可以使用超类方法期望的更通用的参数类型运行。这可能导致运行时错误。与 Java 这样的语言的不同之处在于它的静态类型系统会标记编译时错误,因此不会发生这种类型的运行时错误。
      猜你喜欢
      • 2015-01-01
      • 2010-12-16
      • 2013-06-07
      • 2011-09-09
      • 2012-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-03
      相关资源
      最近更新 更多