Liskov替换原则 - 没有覆盖/虚拟方法?

Ais*_*war 57 liskov-substitution-principle design-principles solid-principles

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

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

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

dus*_*ine 57

Liskov Substituion Principle完全允许基类中的子类重写方法.

这可能会过多地简化它,但我记得它是"一个子类应该只需要更多,并且承诺不会更少"

如果客户端正在ABC使用带有方法的超类something(int i),则客户端应该能够替换任何子类而ABC不会出现问题.而不是根据变量类型来考虑这一点,或许可以根据前提条件和后置条件来考虑它.

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

相反,如果基ABCsomething()方法具有后置条件 - 例如保证它永远不会返回零值 - 那么所有子类也必须遵守相同的后置条件或者它们违反Liskov替换原则(即,承诺更少).

我希望这有帮助.

  • 一个`PizzaSharingService`接口或抽象类(base)有一个`share(pizza,numSlices)`方法,它将返回一个切片列表.前提条件是`numSlices`参数可以是任意数字0-12(包括,如果数字为零,则返回空列表.)因此第一个团队创建一个`RoundPizzaSharingService`,将比萨饼切成三角形切片,遵守关于切片数量为零的规则.第二个团队创建一个`SquarePizzaSharingService`,但决定如果切片数为零,则抛出异常或返回null.这违反了LSP. (6认同)
  • @dustmachine,很好的例子.IMO更多例子==好......假设你有一个`Rectangle`类,它有`setWidth()`和`setHeight()`.一个`Square`类子类化`Rectangle`并覆盖行为,这样当一个维度改变时,该类就会保持它的方形形状.如果有人引用了`Rectangle`并调用`setWidth(10); setHeight(5);`矩形的合约表明尺寸现在应该是10x5,但如果参考实际上是'Square`,则尺寸将被修改为使其成为方形.`Square`类是违反LSP的一个例子. (5认同)
  • 如果GreenABC返回的值对ABC提供的规范有效,那么不,它不违反原则. (2认同)

vib*_*bhu 10

有一个流行的例子说,如果它像鸭子一样游泳,嘎嘎喜欢鸭子,但需要电池,那么它打破了Liskov替代原则.

简单地说,你有一个基础鸭类,正在被某人使用.然后你通过介绍PlasticDuck添加层次结构与鸭子相同的重写行为(如游泳,嘎嘎等),但需要电池来模拟这些行为.这实际上意味着您要为Sub Class的行为引入一个额外的前提条件,要求电池执行与之前没有电池的Base Duck类所做的相同的行为.这可能会让您的Duck类的消费者感到意外,并且可能会破坏围绕Base Duck类的预期行为构建的功能.

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


que*_*en3 7

不,它告诉您应该能够以与其基础相同的方式使用派生类.有很多方法可以覆盖方法而不会破坏它.一个简单的例子,C#中的GetHashCode()是所有类的基础,并且它们中的所有类都可以用作"对象"来计算哈希码.据我所知,破坏规则的一个典型例子是从矩形中导出Square,因为Square不能同时具有宽度和高度 - 因为设置一个会改变另一个,因此它不再符合Rectangle规则.但是,您仍然可以使用.GetSize(),因为所有形状都可以执行此操作 - 因此任何派生的形状都可以替换并用作Shape.

  • 不,不是相同的值,而是满足关于此值的基类契约的某些值.至于形状和散列,请参阅,如果对于派生的SomeObject,您只能调用GetHashCode()_ after_ ToString()(由于某种原因),则会破坏契约 - 现在调用者必须知道它是对象还是SomeObject.现在,您无法将SomeObject传递给期望对象的调用者.与形状相同 - 您不能将Square传递给期望Rectangle的调用者,因为他们将尝试设置Width并且不会期望它更改Height.你可以用一个班代替另一个班.因此,LSP被破坏了. (4认同)
  • 我认为queen3意味着许多类需要覆盖`GetHashCode()`,但调用它的客户端(如哈希表)将不会知道它们没有调用为Object定义的方法.由于合同仍然符合,因此没有发生违规行为.但是,使用Square/Rectangle示例,客户端可能能够告诉它们已经传递的实例的类,或者可能以某种方式失败,因此发生了违规*. (2认同)

Vit*_*nko 5

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

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

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

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

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

  • 问题是 LSP 并不意味着“如果 X 和 Y 将作为不同的对象实例,则等于必须返回 false”,而只是意味着后代必须像其父对象一样。例如,如果您将超类的一个成员放在哈希表中作为键,然后使用子类实例查找它,您将找不到它,因为在您的情况下它们将不相等。 (2认同)