【问题标题】:Is it possible to violate Liskov Substitution Principle in a constructor?是否有可能在构造函数中违反 Liskov 替换原则?
【发布时间】:2016-06-19 08:48:45
【问题描述】:

我刚刚安装了 Microsoft 代码合同。它是 .NET Framework 和 Visual Studio 插件的一部分。它提供对已定义合约的运行时检查和静态检查。

该工具有四个警告级别,所以我设置了最高。

我已经声明了旨在违反 Liskov 替换原则的类。

public class Person
{
    protected int Age { get; set; }

    public Person(int age)
    {
        Contract.Requires(age > 0);
        Contract.Requires(age < 130);
        this.Age = age;
    }
}

public class Child : Person
{
    public Child(int age) : base(age)
    {
        Contract.Requires(age > 0); 
        Contract.Requires(age < Consts.AgeOfMajority);
        Contract.Requires(age < 130);
        this.Age = age;
    }
}

public static class Consts
{
    public readonly static int AgeOfMajority = 18;
}

LSP 状态:

如果 S 是 T 的子类型,则 T 类型的对象可以替换为 类型 S 的对象,而不改变任何所需的属性 那个程序

在我的示例中,违规将是这个分配:Person person = new Child(23);。我们应该能够做到这一点,但我们做不到,因为孩子的年龄不能超过 person 类要求的年龄。

然而,分析结果令人惊讶CodeContracts: Checked 11 assertions: 11 correct。我的示例是错误的还是代码合同没有检测到此类事情?

【问题讨论】:

  • 我不认为这违反了 LSP 的现状。您的课程没有任何行为,并且它们具有相同的 api。唯一的区别是它们的构造规则不同,但我解释 LSP 的方式是从使用该类的客户端的角度来看的。任何与Persons 打交道的人都不应该知道关于孩子最大年龄的规则。在这种情况下,他们不必这样做。
  • 请注意,setter 是不受限制的。修复它,你应该得到一个错误。
  • @Kevin 我已经实现了 full age 属性并添加了 requires 语句。仍然没有警告。

标签: c# code-contracts solid-principles liskov-substitution-principle


【解决方案1】:

有一个著名的鸭子违反 LSP 的例子:

但是我们不能在构造函数中违反它。假设我们有 Duck 和 WildDuck 类:

public abstract class Duck
{
    public abstract string Quack();
    public double Weight { get; set; }

    public Duck(double weight)
    {
        Contract.Requires(weight > 0);
        this.Weight = weight;
    }
}

public class WildDuck : Duck
{
    public WildDuck(double weight)
        : base(weight)
    {
        Contract.Requires(weight > 0);
        this.Weight = weight;
    }

    public override string Quack()
    {
        return "wild quack";
    }
}

现在让我们介绍一下 ElectricDuck:

public class ElectricDuck : Duck
{
    public Battery Battery { get; set; }

    public override string Quack()
    {
        return "electric quack";
    }

    public ElectricDuck(double weight, Battery battery)
        : base(weight)
    {
        Contract.Requires(weight > 0);
        Contract.Requires(battery != null);
        this.Weight = weight;
        this.Battery = battery;
    }
}

public class Battery
{
    public bool IsEmpty { get; set; }
}

乍一看,它似乎违反了 LSP,因为 ElectricDuck 需要的不仅仅是 WildDuck 或抽象 Duck。但只要 ElectricDuck 提供 Quack 方法而不需要额外的要求,它就不是真的。

如果 ElectricDuck 需要电池来发光 - 从 LSP 的角度来看这是完全正确的:

public void Glow()
{
    Contract.Requires(!this.Battery.IsEmpty);
}

向继承的方法添加需求时违反了LSP:

public override string Quack()
{
    Contract.Requires(!this.Battery.IsEmpty);
    return "electric quack";
}

而且这种修改会导致 CodeContracts 显示警告。

【讨论】:

    【解决方案2】:

    虽然 LSP 指定子类型确实不能对方法设置更多限制性前提条件,但这不适用于构造函数,因为您不以多态方式使用构造函数。

    合约违反将是new Child(23);,在分配给Person之前发生。

    所以示例违规是错误的,它没有创建子类型 S 的实例,更不用说将它替换为 T。

    【讨论】:

    • 你能提到任何文章说 LSP 不适用于构造函数吗?如果 Child 构造函数可能不同,我不会感到惊讶,例如需要一些额外的变量,但是限制继承的变量,嗯,对我来说这是一种代码味道。
    • 合乎逻辑。 LSP 谈论使用子类型的实例。如果您无法创建子类型的实例,您甚至无法进入 LSP 域。
    • 能够在不关心的情况下将类型替换为更多派生类型的全部意义在于,您可以多态地使用它们。您认为使用 Person 的代码中可能会出现什么问题,因为年龄不低于 18 岁?我认为,如果有的话,这里的设计味道是类的接口存在/遭受原始的痴迷。
    • 现在的气味是您没有充分的理由使用 Child 类。它只存在于此预检查。
    • @sara,我认为您不替换类型,而是替换 objects 以使讨论有意义。
    【解决方案3】:

    我会说 liskov 替换控制类的构造实例的行为。因此,正确构造的 Child 实例可以毫无问题地替换 Person。

    你对如何构造一个 Child 有限制。我没有看到框架没有将此标记为问题。

    【讨论】:

      猜你喜欢
      • 2015-01-01
      • 1970-01-01
      • 2011-09-09
      • 2012-01-12
      • 1970-01-01
      • 2017-07-04
      • 1970-01-01
      • 2021-11-17
      • 2020-02-03
      相关资源
      最近更新 更多