我对Liskov替换原则的理解是,对于派生类来说,基类的某些属性是真的,或者一些基类的实现行为也是真的。
我想这意味着当一个方法在一个基类中定义时,它不应该在派生类中被重写,因为替换基类而不是派生类会产生不同的结果。我想这也意味着,拥有(非纯)虚拟方法是一件坏事?
我想我可能对这一原则的理解有误。如果我不这样做,我不明白为什么这个原则是好的实践。有人能给我解释一下吗?谢谢
- 感谢所有回答的人。我想你们都对我理解这是如何工作做出了很大贡献。我给了每个人一张赞成票,我不知道我如何确定正确的答案(每个人的答案都帮助了我!D)
Liskov替换原则完全允许基类中的子类重写方法。
这可能简化得太多了,但我记得它是"一个子类不需要更多,承诺更少"。
如果客户使用方法something(int i)使用一个超类ABC,那么客户应该能够毫无问题地替换ABC的任何子类。与其用变量类型来考虑这个问题,不如用先决条件和后条件来考虑它。
如果上述ABC基类中的something()方法具有允许任何整数的宽松前提,那么ABC的所有子类也必须允许任何整数。不允许子类GreenABC向要求参数为正整数的something()方法添加额外的前提条件。这将违反里斯科夫替代原则(即需要更多)。因此,如果客户机正在使用BlueABC子类并向something()传递负整数,那么如果我们需要切换到GreenABC,客户机将不会中断。
相反,如果基ABC类something()方法具有后置条件(例如保证它永远不会返回零值),则所有子类也必须遵守相同的后置条件,否则它们违反了liskov替换原则(即承诺较少)。
我希望这有帮助。
- 嘿,如果用greenabc代替abc并调用something返回不同的值,这是否违反了原则?
- 如果greenabc返回的值对abc提供的规范有效,那么不,它不违反原则。
- @格伦德弗莱克:对不起,如果我不明白的话。因此,派生类不能更改基类的行为,只能添加到它上面吗?也就是说,我不能重写派生类中的方法,使其具有与基类不同的实现。
- 不,一个子类肯定可以有不同的实现,它只是不能改变它的行为规则。我想我们需要拿出一个具体的例子…让我想想……
- PizzaSharingService接口或抽象类(base)具有将返回切片列表的share(pizza,numSlices)方法。前提条件是,numSlices参数可以是任何数字0-12(包括0-12),如果数字为零,则返回一个空列表。)因此,第一个团队创建了一个RoundPizzaSharingService,它将披萨切成三角形切片,并遵循切片数为零的规则。第二个团队创建一个SquarePizzaSharingService,但是决定如果切片数为零,它将抛出一个异常或返回一个空值。这违反了LSP。
- @除尘器,很好的例子。IMO更多示例==好…假设你有一个Rectangle类,它有setWidth()和setHeight()。Square类将Rectangle子类化,并重写行为,以便当维度发生更改时,类将注意保持方形。如果有人引用Rectangle并调用setWidth(10); setHeight(5);,那么矩形的合同规定尺寸现在应该是10x5,但是如果引用实际上是Square,那么尺寸将被修改为方形。Square类是违反lsp的一个例子。
- +1-对合同设计的出色描述。
- 我将在第一条语句中添加一个简短的附录:子类应该对代码作出不低于[超类]的承诺,代码可以接受对超类或其任何子类的任意实例的引用。让Subclass对调用new Subclass()的代码提供的承诺比Superclass对调用new Superclass()的代码提供的承诺少,没有什么错,因为调用new Subclass()的代码不能期望接收超类实例。
有一个很流行的例子说,如果它像鸭子一样游泳,江湖骗子像鸭子但需要电池,那么它就打破了里斯科夫的替代原则。
简单地说,您有一个被某人使用的基本duck类。然后通过引入plasticduck来添加层次结构,它与鸭子的行为(如游泳、咯咯叫等)相同,但需要电池来模拟这些行为。这实质上意味着您正在为子类的行为引入一个额外的前提条件,要求电池执行与基本duck类之前在没有电池的情况下执行的相同行为。这可能会让您的duck类的消费者感到惊讶,并可能破坏围绕基本duck类的预期行为构建的功能。
下面是一个很好的链接-http://lassala.net/2010/11/04/a-good-example-of-liskov-substitution-principle/
不,它告诉您应该能够使用派生类,方法与它的基类相同。有很多方法可以在不破坏方法的情况下重写方法。一个简单的例子,c中的getHashCode()是所有类的基础,但它们仍然可以用作计算哈希代码的"对象"。据我所知,打破规则的一个经典例子是从矩形中派生出正方形,因为正方形不能同时具有宽度和高度-因为设置一个会改变另一个,因此它不再符合矩形规则。但是,您仍然可以使用.getSize()使用基形状,因为所有形状都可以这样做,因此任何派生形状都可以被替换并用作形状。
- 嘿,你是说,对于C gethashcode()中的任何对象,都可以调用,并且可以强制转换为对象,但gethashcode()仍然会返回相同的值。还是我误解了?我猜gethashcode是在对象中实现的,派生对象不会重写这个。我看不出这与正方形/矩形的例子是如何对应的。你能解释一下吗?
- 我认为queen3意味着许多类需要重写GetHashCode(),但是调用它的客户机(如哈希表)不会知道他们没有调用为对象定义的方法。由于合同仍然满足,因此没有发生违反。但是,对于方形/矩形示例,客户机可能能够告诉他们所传递的实例的类是什么,或者可能以某种方式失败,因此发生了冲突。
- 不,不是相同的值,而是满足关于这个值的基类契约的一些值。至于形状和散列,请参见,如果对于派生的someObject,您只能在toString()之后调用getHashCode()(出于某种原因),则会破坏约定-现在调用方必须知道它是对象还是someObject。现在不能将某个对象传递给需要该对象的调用方。与形状相同-不能将正方形传递给需要矩形的调用方,因为它们将尝试设置宽度,而不会期望它更改高度。你可以用一个班代替另一个班。因此,LSP被破坏。
如果更改由基方法定义的任何行为,则重写将破坏liskov替换原则。这意味着:
最薄弱的先决条件子方法不应更强而不是基本方法。
子方法的后置条件表示的后置条件父方法。哪里有后置条件形成于:a)所有侧面方法执行和b)造成的影响返回表达式的类型和值。
从这两个需求中,您可以暗示子方法中的任何新功能,如果不影响对超级方法的期望值,则不会违反该原则。这些条件允许您在需要超类实例的情况下使用子类实例。
如果不遵守这些规则,类将违反LSP。一个经典的例子是如下的层次结构:类Point(x,y)、类ColoredPoint(x,y,color),它扩展了Point(x,y),并在ColoredPoint中重写了方法equals(obj),反映了颜色的相等性。现在,如果有一个Set的实例,他可以假设在这个集合中具有相同坐标的两个点是相等的。而对于被重写的方法equals则不是这种情况,而且一般来说,在不破坏lsp的情况下,无法扩展可实例化类并添加equals方法中使用的方面。
因此,每当您违反这个原则时,都会隐式地引入一个潜在的bug,它揭示了代码所期望的父类的不变量何时不满足。然而,在现实世界中,通常没有明显的设计解决方案不违反LSP,因此可以使用@ViolatesLSP类注释来警告客户,在多态集或任何其他依赖Liskov替换原则的情况下使用类实例是不安全的。
- 如果x和y是同一个实例,那么指定x.equals(y)必须返回true是什么错误?对于任何一对完全构造的实例x和y,必须始终返回相同的值;如果x和y表现为不同的对象实例,则必须返回false;如果x和y的任何引用组合可以表示为在不影响程序行为的情况下添加y或反之亦然。可以肯定的是,许多.NET实现的equals并不是这样工作的,但是这样的equals定义是有用的,并且完全符合LSP。
- 问题是,lsp并不意味着equals must return false if X and Y will ever behave as different object instances,而是意味着后代必须表现得像其父代。例如,如果您将超级类的一个成员放在哈希表中作为键,然后使用子类实例查找它,您将找不到它,因为在您的情况下,它们将不相等。
- 让类定义一个比object.equals应该隐含的内容更具体的等价关系通常很有用。这种等价关系只能用于从某种类型派生的对象。如果定义了一个等价关系,指定两个同名动物应被视为相等,那么对名为"fred"的暹罗猫而言,不将其与名为"fred"的猫或名为"fred"的土豚进行比较将违反LSP。除非其中一个明确指定了等价关系,尽管…
- …默认等价关系一般不应将任何对象视为等同于另一个类的任何对象,不管类关系如何。仅当一个对象可以安全地替换另一个对象时才将其视为相等的等价关系可以有效地应用(例如用于缓存或截取),即使一个人对所讨论的对象一无所知。其他类型的等价关系(例如"名称"字段上的匹配)只能在具有特定语义意义的上下文中使用。
我认为你在描述原则的方式上是完全正确的,只有重写纯虚拟或抽象的方法才能确保你不会违反它。
但是,如果您从客户机的角度来看这个原则,也就是说,一个引用基类的方法。如果这个方法不能分辨(当然也不会尝试也不需要找出)传入的任何实例的类,那么您也不会违反这个原则。因此,重写基类方法可能无关紧要(某些修饰符可能会这样做,在进程中调用基类方法)。
如果客户机似乎需要找出传入的实例的类,那么您将陷入维护噩梦,因为您实际上应该只是添加新类作为维护工作的一部分,而不是修改现有的例程。(也见OCP)
- 因此,在常用的正方形和矩形示例中,从矩形继承正方形并不是违反规则的。如果矩形的setWidth属性试图标识它的实数类型(如果它是传递给接收矩形的函数的平方),并尝试为该情况实现特殊逻辑,则是违反规则的。这是正确的吗?
- @aip.cd.aish:如果客户不知道,这不是违规行为;如果客户必须使用特殊的逻辑来解决问题,这是违规行为。如果类的方法或者不关心它们的类型,或者可以安全地假定它们的类型是最初定义方法的类型,那就更好了。
- @Quamrana:根据你对我对queen3答案的评论的回复,派生类不能改变基类的行为,只能添加到它上面吗?也就是说,我不能重写派生类中的方法,使其具有与基类不同的实现。这是原则所说的吗(除了关于方法的部分之外,不应该试图确定它实际上是什么类)?
- @aip.cd.aish:如果你从实现的角度考虑这个问题,你就不会轻易地解决这个问题。最好考虑客户的期望。在一个实现中,您可以做很多不违背客户期望的事情,特别是在客户期望值很低或没有的情况下。简单的例子是GetHasCode()或Shape::GetArea(),客户很难检测到故障。一个更困难的例子是正方形和矩形,在这里你可能只有矩形有setHeight和setWidth,只有正方形有setSize。