为什么削弱前提条件不违反里氏替换原则
Why weakening a precondition does not violate Liskov substitution principle
我正在详细学习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;
}
}
在这里我可以清楚地看到对基数 class 有效的东西对其导数无效。换句话说,我无法在不改变行为的情况下用它的导数替换基数 class。
现在以下方法被认为是合法的,因为它削弱了前提条件:
public class LSPLegalSubType : SuperType
{
public override string FormatName(string name)
{
if (name == null)
throw new ArgumentNullException("name");
return name;
}
}
引用站点:这是完全合法的,因为超类型的任何有效参数在子类型中也将有效。
嗯,但是无效参数呢?如果我有一个调用带有无效参数(例如空名称)的 SuperType 的代码,它会失败。如果我用子类型替换它,相同的调用将不会因为条件较弱而失败。所以从这个意义上说,我不能用子类型替换超类型,因为它也会改变行为。我很纳闷。
如果你弱化一个先决条件,子类型仍然与期望超类型的地方兼容。它可能不会在基 class 正常执行的情况下抛出异常,但这没关系,因为抛出较少的异常不应该破坏使用代码。如果调用代码是围绕在某些地方抛出异常的假设构建的,并将其用于应用程序的主要控制流,则可能应该重写使用代码。
此外,我认为您的第二个代码示例是错误的。
如果基 class 的先决条件真的必须一直执行,更好的实现是创建一个封装这些规则的数据类型,并将其作为参数传递。这样它就不在 subclasses 的手中,它是新 class 的构造函数的一部分。
例如:
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
}
}
一个带有前置条件和后置条件的方法声明,当调用者满足前置条件时,它保证在退出时满足后置条件。但是,合同没有说明如果先决条件 不 满足会发生什么 - 该方法仍然允许成功完成。因此,子类型可以削弱前提条件,因为如果调用方无法满足子类型的前提条件,则无法对方法的行为做出任何假设。
我正在详细学习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;
}
}
在这里我可以清楚地看到对基数 class 有效的东西对其导数无效。换句话说,我无法在不改变行为的情况下用它的导数替换基数 class。
现在以下方法被认为是合法的,因为它削弱了前提条件:
public class LSPLegalSubType : SuperType
{
public override string FormatName(string name)
{
if (name == null)
throw new ArgumentNullException("name");
return name;
}
}
引用站点:这是完全合法的,因为超类型的任何有效参数在子类型中也将有效。
嗯,但是无效参数呢?如果我有一个调用带有无效参数(例如空名称)的 SuperType 的代码,它会失败。如果我用子类型替换它,相同的调用将不会因为条件较弱而失败。所以从这个意义上说,我不能用子类型替换超类型,因为它也会改变行为。我很纳闷。
如果你弱化一个先决条件,子类型仍然与期望超类型的地方兼容。它可能不会在基 class 正常执行的情况下抛出异常,但这没关系,因为抛出较少的异常不应该破坏使用代码。如果调用代码是围绕在某些地方抛出异常的假设构建的,并将其用于应用程序的主要控制流,则可能应该重写使用代码。
此外,我认为您的第二个代码示例是错误的。
如果基 class 的先决条件真的必须一直执行,更好的实现是创建一个封装这些规则的数据类型,并将其作为参数传递。这样它就不在 subclasses 的手中,它是新 class 的构造函数的一部分。
例如:
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
}
}
一个带有前置条件和后置条件的方法声明,当调用者满足前置条件时,它保证在退出时满足后置条件。但是,合同没有说明如果先决条件 不 满足会发生什么 - 该方法仍然允许成功完成。因此,子类型可以削弱前提条件,因为如果调用方无法满足子类型的前提条件,则无法对方法的行为做出任何假设。