2

几天来,我一直在尝试理解 Liskov 替换原则,在使用非常典型的 Rectangle/Square 示例进行一些代码测试时,我创建了下面的代码,并提出了 2 个关于它的问题。

问题 1:如果我们有超类/子类关系,为什么我们要将一个实例声明为超类型,但将其实例化(新建)为子类型?

我理解为什么,如果我们通过接口进行多态性,我们希望以这种方式声明和实例化变量:

IAnimal dog = new Dog();

但是,现在我在旧的编程类和一些博客示例中回忆起它,当通过继承使用多态性时,我仍然会看到一些示例,其中一些代码会以这种方式声明变量

Animal dog = new Dog();

在我下面的代码中,Square 继承自 Rectangle,所以当我以这种方式创建一个新的 Square 实例时:

Square sq = new Square();

它仍然可以被视为一个矩形,或者添加到一个通用的矩形列表中,那么为什么有人仍然想将它声明为 Rectangle = new Square() 呢?是否有我没有看到的好处,或者需要这样做的场景?就像我说的,我下面的代码工作得很好。

namespace ConsoleApp
{
class Program
{
    static void Main(string[] args)
    {
        var rect = new Rectangle(300, 150);
        var sq = new Square(100);
        Rectangle liskov = new Square(50);

        var list = new List<Rectangle> {rect, sq, liskov};

        foreach(Rectangle r in list)
        {
            r.SetWidth(90);
            r.SetHeight(80);

            r.PrintSize();
            r.PrintMyType();

            Console.WriteLine("-----");
        }


        Console.ReadLine();
    }

    public class Rectangle
    {
        protected int _width;
        protected int _height;

        public Rectangle(int width, int height)
        {
            _width = width;
            _height = height;
        }

        public void PrintMyType()
        {
            Console.WriteLine(this.GetType());
        }

        public void PrintSize()
        {
            Console.WriteLine(string.Format("Width: {0}, Height: {1}", _width, _height));
        }

        public virtual void SetWidth(int value)
        {
            _width = value;
        }

        public virtual void SetHeight(int value)
        {
            _height = value;
        }

        public int Width { get { return _width; } }
        public int Height { get { return _height; } }
    }

    public class Square : Rectangle
    {
        public Square(int size) : base(size, size) {}

        public override void SetWidth(int value)
        {
            base.SetWidth(value);
            base.SetHeight(value);
        }

        public override void SetHeight(int value)
        {
            base.SetHeight(value);
            base.SetWidth(value);
        }
    }
}

}

尽管这应该违反 Liskov 替换原则,但我得到以下输出:

"宽度:90,高度:80

ConsoleApp.Program+矩形

宽度:80,高度:80

ConsoleApp.Program+Square

宽度:80,高度:80 ConsoleApp.Program+Square

问题 2:那么,此代码示例为何或如何破坏 LSP?仅仅是因为所有边相等的 Square 不变量打破了 Rectangle 不变量,边可以独立修改吗?如果是这个原因,那么 LSP 违规只是理论上的吗?或者,在代码中,我怎么能看到这个代码违反了原则?

编辑:在我正在阅读的一篇 LSP 博客文章中提出了第三个问题,但没有答案,所以就是这个

问题 3:开闭原则指出我们应该通过新的类(继承或接口)引入新的行为/功能。因此,例如,如果我在基类中有一个 WriteLog 方法,它没有先决条件,但是我引入了一个新的子类,它覆盖了该方法,但只有在事件非常关键时才实际写入日志....如果这是新的预期功能(对子类型进行强化的前提条件),这仍然会破坏 LSP 吗?在这种情况下,这两个原则似乎相互矛盾。

提前致谢。

4

2 回答 2

3

问题 1:如果我们有超类/子类关系,为什么我们要将一个实例声明为超类型,但将其实例化(新建)为子类型?

使用超类型执行此操作的原因与使用接口执行此操作的原因相同。您列出的为什么将变量声明为其特定子类型而不是超类型的所有原因同样适用于为什么将变量声明为其特定子类型而不是子类型实现的接口。

abstract class Car { ... }
public abstract class ToyotaCamery2011 extends Car ( ... )

class Garage {
    private Car car = new ToyotaCamery2011();
    public Car getCar() { return car; }
    ....
}

class Garage {
    private ToyotaCamery2011 toyotaCamery2011 = new ToyotaCamery2011();
    public Car getCar() { return toyotaCamery2011; }
    ....
}

只要所有的方法Garage都只使用 的方法Car,而公共接口Garage只显示Car而没有特定于Prius2011,那么这两个类实际上是等价的。哪个更容易理解,例如哪个更接近真实世界?这确保我不会意外使用 Prius 专用方法,即建造一个 Prius 专用车库?如果我决定买一辆新车,哪一个更容易维护?代码是否使用特定子类型以任何方式改进?


问题 2:那么,此代码示例为何或如何破坏 LSP?是否只是因为所有边相等的 Square 不变量打破了边可以独立修改的 Rectangle 不变量?如果是这个原因,那么 LSP 违规只是理论上的吗?或者,在代码中,我怎么能看到这个代码违反了原则?

不谈论承诺/合同就很难谈论 LSP。但是是的,如果Rectangle承诺边可以独立修改(更正式地说,如果调用的后置条件Rectangle.setWidth()包括 Rectangle.getHeight() 应该不受影响),那么从中断 LSPSquare派生。Rectangle

你的程序不依赖于这个属性,所以它很好。但是,采用一个试图满足周长值或面积值的程序。这样的程序可能依赖于Rectangle具有独立方面的想法。

任何接受 aRectangle作为输入并依赖于该属性/行为的类在Rectangle将 a 作为输入时都可能会中断Square。像这样的程序可以跳过箍来寻找和禁止 a Square(这是子类的知识),或者它可以改变Rectangle关于独立大小的合同。然后所有使用的程序Rectangle可以在每次调用setWidth()或 setLength() to see whether the adjacent side also changed and react accordingly. If it does the latter, thanSquare deriving frmoRectangle` 后检查不再违反 LSP。

它不仅仅是理论上的,它可以对软件产生真正的影响,但在实践中经常会受到影响。不幸的是,您经常在 Java 中看到这一点。Java 的Iterator类提供了一个remove()可选的方法。使用迭代器的类必须了解实现类和/或其子类,以了解其使用是否安全Iterator.remove()。这违反了 LSP,但它在 Java 中被接受。它使编写和维护软件更复杂,更容易受到错误的影响。


问题 3:开闭原则指出我们应该通过新的类(继承或接口)引入新的行为/功能。因此,例如,如果我在基类中有一个 WriteLog 方法,它没有先决条件,但是我引入了一个新的子类,它覆盖了该方法,但只有在事件非常关键时才实际写入日志....如果这是新的预期功能(对子类型进行强化的前提条件),这仍然会破坏 LSP 吗?在这种情况下,这两个原则似乎相互矛盾。

当您说先决条件时,我认为您的意思是后置条件-您正在描述该方法承诺实现的内容。如果是这样,那么我看不到 LSP 违规 - 如果方法超类什么都不承诺,那么子类可以做它喜欢的事情并且仍然可以完全替代。子类对它所写的内容更具选择性(“仅实际写入”)这一事实是新功能,特别是考虑到超类什么都不承诺的事实。

于 2011-02-02T08:16:25.610 回答
0

为什么你认为这应该打破 Liskov 替换原则?

LSP 的真正意义在于,如果传递的对象是子类型而不是原始类型的对象,则方法应该做正确的事情。这意味着如果您可以证明该方法在传递该类型的对象时“做正确的事情”,那么如果您将这些对象替换为子类型的对象,它将“做正确的事情”。

但是在您的情况下,您没有方法调用,因此 LSP 有点无关紧要。

于 2011-02-02T07:43:28.527 回答