【问题标题】:Why weakening a precondition does not violate Liskov substitution principle为什么弱化先决条件不违反里氏替换原则
【发布时间】:2018-06-27 23:51:26
【问题描述】:

我正在详细了解 LSP,我确实明白为什么强化前置条件会违反原则(使用来自 http://www.ckode.dk/programming/solid-principles-part-3-liskovs-substitution-principle/#contravariance 的示例):

public class SuperType  
{  
    public virtual string FormatName(string name)  
    {  
        if (string.IsNullOrEmpty(name))  
            throw new ArgumentException("name cannot be null or empty", "name");  
        return name;  
    }  
}  

//VIOLATING ONE
public class LSPIllegalSubType : SuperType  
{  
    public override string FormatName(string name)  
    {  
        if (string.IsNullOrEmpty(name) || name.Length < 4)  
            throw new ArgumentException("name must be at least 4 characters long", "name");  
        return name;  
    }  
}  

在这里,我清楚地看到,对于基类有效的东西对于它的派生类会失败。换句话说,我不能在不改变行为的情况下用它的派生类替换基类。

现在以下方法被认为是合法的,因为它削弱了先决条件:

    public class LSPLegalSubType : SuperType  
{  
    public override string FormatName(string name)  
    {  
        if (name == null)  
            throw new ArgumentNullException("name");  
        return name;  
    }  
}  

引用网站:这是完全合法的,因为超类型的任何有效参数在子类型中也有效。

好吧,但是无效的参数呢?如果我有一个使用无效参数(例如空名称)调用 SuperType 的代码,它将失败。如果我将其替换为子类型,则相同的调用不会失败,因为条件较弱。所以从这个意义上说,我不能用子类型替换超类型,因为它也会改变行为。我很困惑。

【问题讨论】:

  • 你的第二个例子强化了前提条件。
  • 啊,对不起,错误的代码 sn-p!你是对的

标签: oop solid-principles liskov-substitution-principle


【解决方案1】:

具有前置条件和后置条件的方法声明当调用者满足前置条件时,它保证后置条件将在退出时得到满足。但是,合同没有说明如果先决条件满足会发生什么 - 仍然允许该方法成功完成。因此,子类型可以削弱前提条件,因为如果调用者不能满足子类型的前提条件,则调用者无法对方法的行为做出任何假设。

【讨论】:

  • 什么意思?如果不满足前提条件,则不应该执行。
  • @user970696 - 这通常会发生,但从调用者的角度来看是不可靠的。调用者负责满足基本类型的先决条件,如果子类型避免加强它们,他们将始终这样做。
【解决方案2】:

如果你弱化一个前置条件,子类型仍然与需要超类型的地方兼容。它可能不会在基类正常执行的情况下抛出异常,但这没关系,因为抛出较少的异常不应破坏使用代码。如果调用代码是围绕在某些地方抛出异常的假设构建的,并将其用于应用程序的主要控制流,则可能应该重写消费代码。

另外,我认为您的第二个代码示例是错误的。

如果确实必须始终强制执行基类的先决条件,更好的实现是创建一个封装这些规则的数据类型,并将其作为参数传递。这样它就不是在子类的手中,它是新类的构造函数的一部分。

例如:

public class UserName 
{
    public string Value { get; }

    public UserName(string value)
    {
        if (string.IsNullOrWhitespace(value) || value.Length < 4)
            throw new ArgumentNullException(nameof(value));

        Value = value;
    }
}

public class BaseClass 
{
    public virtual void Foo(UserName username) 
    { 
        //No precondition checks required here 
    }
}

public class DerivedClass : BaseClass
{
    public override void Foo(UserName username) 
    {
        //No precondition checks required here
    }
}

【讨论】:

  • 你说得对,我在示例中放置了正确的代码 sn-p。但我仍然不明白 - 如果我用它的派生类替换基类并且行为发生变化(如示例中所示),那还可以吗?就像在示例中一样 - 基类不允许空字符串,但对于派生类来说可以,这会将其弱化为不为空的字符串。
  • 这是 .NET 和类似系统中一般异常抛出模式的问题。抛出的异常不是类型的公共接口的一部分,因此这些协定很难在编译时强制执行。在SuperType 类之外,没有明确的方法可以知道它有什么先决条件。通过创建Username 类型,您可以使类型系统强制执行这些协定。 LSP 并没有真正考虑打破弱化先决条件的情况,但它是可能的。也许 LSP 的另一面是不要设计你的类,这样子类就可以破坏它们。
  • 好吧,我认为它与异常无关。假设基类在 A,B,C 为真时执行一个函数,而当只有 A 为真时它的派生类执行相同的功能。
  • 非常正确。这与例外无关。 SOLID 的很大一部分通常是,在使用一个类时,不要对任何不属于其公共接口的任何内容做出任何假设。这包括抛出的异常、通过查看私有成员知道会发生的副作用等。假装在使用该类时看不到该类的源代码。
  • 所以我还是一头雾水。如果我用 null 名称调用基类中的方法,我会得到异常。如果我用子类型替换它,它将起作用。我无法理解为什么可以?也许是因为在提供子类型时,我应该期望它可能接受比超类型更多的东西?
猜你喜欢
  • 2017-05-14
  • 1970-01-01
  • 1970-01-01
  • 2015-02-15
  • 1970-01-01
  • 1970-01-01
  • 2017-07-26
  • 1970-01-01
  • 2020-04-05
相关资源
最近更新 更多