为啥弱化先决条件不违反里氏替换原则

Posted

技术标签:

【中文标题】为啥弱化先决条件不违反里氏替换原则【英文标题】: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!你是对的 【参考方案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 名称调用基类中的方法,我会得到异常。如果我用子类型替换它,它将起作用。我无法理解为什么可以?也许是因为在提供子类型时,我应该期望它可能接受比超类型更多的东西?

以上是关于为啥弱化先决条件不违反里氏替换原则的主要内容,如果未能解决你的问题,请参考以下文章

里氏替换原则|SOLID as a rock

面向对象原则之一 里氏替换原则

设计模式--六大原则

里氏替换原则的七大原则

面向对象设计原则三:里氏替换原则(LSP)

六大设计原则LSP里氏替换原则