Liskov替换原则,前置条件和抽象方法

4

Liskov替换原则(LSP)认为:

子类型中的前置条件不能被加强。

在C#中,我可以如下违反整个原则:

public class A 
{
      public virtual void DoStuff(string text)
      {
            Contract.Requires(!string.IsNullOrEmpty(text));
      }
}

public class B : A
{
      public override void DoStuff(string text)
      {
            Contract.Requires(!string.IsNullOrEmpty(text) && text.Length > 10);
      }
}

但是,如果A.DoStuff是一个抽象方法,会发生什么:

public class A 
{
      public abstract void DoStuff(string text);
}

public class B : A
{
      public override void DoStuff(string text)
      {
            Contract.Requires(!string.IsNullOrEmpty(text));
      }
}

现在,A.DoStuff是没有契约的。或者说它的契约只是允许一切
那么,B.DoStuff违反了Liskov替换原则的前提条件吗?

@Lee 讨论的问题可能是,一个抽象方法作为仅仅是“元数据”,是否可以被认为不提供前置条件,因为它没有行为,它不提供前置条件并不意味着它没有前置条件。我认为这是一个简单的情况,但可能需要进行复杂的分析才能确定它是否真正违反了LSP... - Matías Fidemraizer
@InBetween 为什么你删除了你的回答? - Matías Fidemraizer
1
@zerkms - 抽象方法可以像其他方法一样具有前/后置条件。 - Lee
因为我越想越清楚,我越来越确信这是一个LSP违规,并且我的答案可能是错误的。我仍在思考中...但我会取消删除答案,开启另一种观点。 - InBetween
@zerkms,您曾经在评论中为相反的观点辩护,认为接口方法的实现可能会破坏LSP,现在您声称在不违反LSP的情况下,将常规、抽象和接口方法视为同等对待。我打开了这个问答,因为我感到这并不是100%清楚的问题,我们在论证中可能会犯错误,这是关于得到令人信服的答案来回答我的(甚至是我们的)担忧。 - Matías Fidemraizer
显示剩余12条评论
3个回答

2
这取决于“定义合同”的内容。LSP是一个理论构造,它不依赖于特定的语言或实现,例如C#的“代码合同”功能。合同可以由以下方式定义:方法名称、方法参数名称、方法注释、返回类型和方法参数类型、“显式”合同,如Contract.Requires。最后两个将由编译器进行验证。然而,前三个也可以成为合同的一部分!请考虑以下示例:
public interface StuffContainer
{
    void Add(string text);

    // Removes a string that has previously been added.
    void Remove(string text);
}

Remove 方法的名称和文档定义了明确的前提条件。在实现中验证要删除的字符串之前是否已添加不会违反 LSP。验证该字符串至少具有 5 个字符将违反 LSP。


请注意,我想解释关于C#和代码合同的整个问题,只是基于一个实际示例讨论这个主题。 - Matías Fidemraizer
无论如何,您的结论是我在问题中解释的整个情况不一定违反LSP? - Matías Fidemraizer
@MatíasFidemraizer:如果你的方法真的叫做DoStuff并且没有更多的文档说明,那么是的,它违反了LSP(无论是你的第一个还是第二个例子)。如果你的方法实际上叫做DoStuffOnLongString并且你在某个地方定义了“长字符串”是一个具有超过10个字符的字符串,那么LSP就没有被违反(无论是在你的第一个还是第二个例子中)。 - Heinzi
我的问题源自这个其他的问答:http://stackoverflow.com/a/40957842/411632,请查看我的回答。@usr在那里提出了一个问题:它违反了LSP原则。我并不是说它不违反LSP原则,但我发现将其作为一个新的问答来记录和分析抽象方法的特定情况会很有趣。 - Matías Fidemraizer

1

是的,您可以很容易地违反这个原则,不仅在C#中。

它只是说明:

子类型要求:让phi(x)成为一个可证明的属性 关于类型T的对象x。然后 phi(y)应该为真 类型为T的子类型S的对象y。

在您的示例中,类型B未满足提供适用于短文本的DoStuff方法的属性,尽管其超类型A已满足该属性。 因此,违反了该原则。

程序员需要遵守这个原则。 属性也可以是“它做正确的事情”,您可以通过具有错误实现方法的子类型轻松违反该属性。


我说C#是因为我想介绍一个使用C#的代码片段! :) - Matías Fidemraizer
嗯,当你谈论我的例子时,你是在考虑抽象情况吗? - Matías Fidemraizer
我认为对于抽象类而言,不存在对象。因此,该原则不适用。 - fafl
那么你和 @InBetween 在同一行。 - Matías Fidemraizer
另一方面,我们可以说,在派生A的第一类之后,层次结构的下一个级别是应该应用LSP的类。实际上,从A开始的任何层次结构都可以这样工作。 - Matías Fidemraizer

0
我认为这并不是这样的。按照定义,抽象方法没有前置条件,因为它没有实现。这就好比争论实现接口是否会破坏LSP。
A.DoSomething() 是无约束的是一个不正确的前提。 A.DoSomehing() 是未定义的,因此它不能有进一步的约束,除了其签名。

我相信你的回答引入了一个好的观点,当你比较抽象和接口方法时,可以帮助我们找到正确的方向。 - Matías Fidemraizer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接