69

我对 Liskov 替换原则的理解是,基类的某些属性是真的,或者基类的某些实现的行为,对于派生类也应该是真的。

我想这意味着当一个方法在基类中定义时,它不应该在派生类中被覆盖——因为这样替换基类而不是派生类会产生不同的结果。我想这也意味着,拥有(非纯)虚拟方法是一件坏事?

我想我可能对原理有错误的理解。如果我不这样做,我不明白为什么这个原则是好的做法。谁可以给我解释一下这个?谢谢

4

6 回答 6

67

Liskov 替换原则完全允许子类覆盖基类中的方法。

这可能过于简化了,但我记得它是“一个子类应该不需要更多,也不需要更少承诺”

如果客户端使用ABC带有方法的超类something(int i),那么客户端应该能够ABC毫无问题地替换任何子类。与其从变量类型的角度考虑这一点,不如从前置条件和后置条件的角度来考虑。

如果我们something()在上面基类中的方法有一个允许任何ABC整数的宽松前提条件,那么 的所有子类也必须允许任何整数。不允许子类向要求参数为整数的方法添加额外的前提条件。这将违反 Liskov 替代原则(即要求更多)。因此,如果客户端正在使用子类并将负整数传递给客户端,如果我们需要切换到.ABC GreenABCsomething()BlueABCsomething()GreenABC

相反,如果基ABCsomething()方法有一个后置条件——比如保证它永远不会返回零值——那么所有子类也必须遵守相同的后置条件,否则它们就违反了里氏替换原则(即承诺更少)。

我希望这有帮助。

于 2009-11-14T19:35:35.000 回答
14

有一个流行的例子说,如果它像鸭子一样游泳,嘎嘎喜欢鸭子但需要电池,那么它就违反了 Liskov 替换原则。

简单地说,你有一个被某人使用的基础 Duck 类。然后通过引入 PlasticDuck 来添加层次结构,它具有与 Duck 相同的覆盖行为(如游泳、嘎嘎声等),但需要电池来模拟这些行为。这实质上意味着您为 Sub Class 的行为引入了一个额外的前提条件,以要求电池执行与之前不带电池的 Base Duck 类相同的行为。这可能会让 Duck 类的消费者感到意外,并可能破坏围绕 Base Duck 类的预期行为构建的功能。

这是一个很好的链接 - http://lassala.net/2010/11/04/a-good-example-of-liskov-substitution-principle/

于 2013-10-16T04:42:55.567 回答
7

不,它告诉您应该能够以与基类相同的方式使用派生类。有很多方法可以覆盖一个方法而不会破坏它。一个简单的例子,C# 中的 GetHashCode() 是所有类的基础,并且仍然可以将它们中的所有类用作“对象”来计算哈希码。据我所知,打破规则的一个经典例子是从 Rectangle 派生 Square,因为 Square 不能同时具有 Width 和 Height - 因为设置一个会改变另一个,因此它不再符合 Rectangle 规则。但是,您仍然可以使用带有 .GetSize() 的基本形状,因为所有形状都可以做到这一点 - 因此任何派生形状都可以替换并用作形状。

于 2009-11-14T18:38:47.640 回答
6

如果您更改由基本方法定义的任何行为,则覆盖会破坏 Liskov 替换原则。意思就是:

  1. 子方法的最弱前提条件不应强于基方法。
  2. 子方法的后置条件意味着父方法的后置条件。后置条件由以下各项构成:a)方法执行引起的所有副作用和b) 返回表达式的类型和值。

从这两个要求中,您可以暗示子方法中的任何不影响超级方法预期内容的新功能都不会违反原则。这些条件允许您在需要超类实例的情况下使用子类实例。

如果不遵守这些规则,则一个类违反了 LSP。一个经典的例子是下面的层次结构: class Point(x,y)ColoredPoint(x,y,color)扩展Point(x,y)和覆盖方法equals(obj)的类,ColoredPoint通过颜色反映平等。现在,如果一个人有一个实例,Set<Point>他可以假设在这个集合中具有相同坐标的两个点是相等的。重写的方法不是这种情况equals,一般来说,没有办法在equals不破坏 LSP 的情况下扩展可实例化的类并添加方法中使用的方面。

因此,每次您违反此原则时,您都会隐含地引入一个潜在的错误,该错误会揭示代码预期的父类的不变量何时不满足。然而,在现实世界中,通常没有明显的设计解决方案不违反 LSP,因此可以使用例如@ViolatesLSP类注释来警告客户在多态集或任何其他类型中使用类实例是不安全的依赖 Liskov 替代原则的案例。

于 2011-04-27T21:24:48.253 回答
2

我认为您描述原则的方式确实是正确的,只有覆盖纯虚拟或抽象方法才能确保您不会违反它。

但是,如果从客户端的角度来看原理,也就是一个方法,它需要对基类的引用。如果这个方法不能告诉(当然不会试图也不需要找出)传入的任何实例的类,那么你也没有违反原则。因此,重写基类方法可能并不重要(某些装饰器可能会这样做,在过程中调用基类方法)。

如果客户似乎需要找出传入的实例的类,那么您将面临维护的噩梦,因为您实际上应该只是添加新类作为维护工作的一部分,而不是修改现有例程。(另见OCP

于 2009-11-14T22:55:07.630 回答
1

原原理:

“这里想要的是类似于下面的替换属性:如果对于每个 S 类型的对象 o1 都有一个 T 类型的对象 o2,那么对于所有以 T 定义的程序 P,o1 为替换为 o2 则 S 是 T 的子类型。”。

Barbara Liskov, 1987

这个词就是行为。“前置条件和后置条件”的理解对于良好的设计很有用,但与 LSP 无关。

让我们检查一下“前置条件和后置条件”理论的总结:

  • 不要对输入参数实施比父类实施更严格的验证规则。
  • 至少对父类应用的所有输出参数应用相同的规则。

一个表明它与 LSP 无关的迹象是:VOID 方法呢?VOID 没有 OUTPUT 参数。这条规则如何应用于 VOID 方法?根据这个规则,我们如何保证在 VOID 方法中遵守 LSP?

LSP 指行为。当一个子类继承自一个超类并且您必须使用一些技巧来完成这项工作时,结果会改变您正在破坏 LSP 的程序的行为。

LSP 是关于行为的,Square x Rectangle 的经典例子帮助我们理解。事实上是鲍勃叔叔使用的例子。您从 Rectangle 继承 Square 并覆盖 SetHeight 和 SetWidth 以强制 Square 充当 Square,即使它是矩形(通过继承)。当用户调用 SetHeight 时,不要期望 Width 改变.... 但会改变,这会改变预期的行为并破坏 LSP。

这是 Virtuals x LSP 的问题

于 2021-08-27T09:05:31.257 回答