如何在遵守liskov替换原则(LSP)的同时,从多态中获益

本文关键字:多态 LSP liskov 原则 替换 | 更新日期: 2023-09-27 17:54:29

LSP说"派生类型不能改变基类型的行为",换句话说"派生类型必须可以完全替代它们的基类型"。

这意味着如果在基类中定义虚方法,就违反了这一原则。

同样,如果我们使用new关键字在drive方法中隐藏一个方法,那么我们又违反了这个原则。

换句话说,如果我们使用多态性,我们就违反了LSP!

在许多应用程序中,我在基类中使用了Virtual方法,现在我意识到它违反了LSP。另外,如果你使用模板方法模式,你就违反了这个原则,我已经用过很多次了。

那么,当您需要继承并希望从多态性中获益时,如何设计符合此原则的应用程序呢?我糊涂了!

查看此处的示例:http://www.oodesign.com/liskov-s-substitution-principle.html

如何在遵守liskov替换原则(LSP)的同时,从多态中获益

Barbara Liskov有一篇非常好的文章《数据抽象和层次》,其中她特别提到了多态行为和虚拟软件结构。在阅读完这篇文章之后,您可以看到,她深入地描述了软件组件如何从简单的多态调用中实现灵活性和模块化。

LSP描述的是实现细节,而不是抽象。具体来说,如果您使用T类型的一些接口或抽象,您应该期望传递T的所有子类型,并且不会观察到意外的行为或程序崩溃。

这里的关键字是unexpected,因为它可以描述程序的任何属性(正确性、执行的任务、返回的语义、临时的等等)。因此,使您的方法virtual本身并不意味着违反LSP

"派生类型必须不改变基类型的行为"意味着必须可以像使用基类型一样使用派生类型。例如,如果您能够调用x = baseObj.DoSomeThing(123),那么您也必须能够调用x = derivedObj.DoSomeThing(123)。如果基方法没有抛出异常,派生方法不应该抛出异常。使用基类的代码也应该能够很好地与派生类一起工作。它不应该"看到"它正在使用另一种类型。这并不意味着派生类必须做完全相同的事情;那将毫无意义。换句话说,使用派生类型不应该破坏使用基类型平稳运行的代码。

作为一个例子,让我们假设您声明了一个记录器,使您能够将消息记录到控制台

logger.WriteLine("hello");

可以在需要生成日志的类中使用构造函数注入。现在,您传递给它的不是控制台日志记录器,而是从控制台日志记录器派生的文件日志记录器。如果文件记录器抛出一个异常,说"你必须在消息字符串中包含行号",这将破坏LSP。但是,日志记录到文件而不是控制台也不是问题。也就是说,如果记录器向调用者显示相同的行为,则一切正常。


如果需要编写如下代码,则会违反LSP:

if (logger is FileLogger) {
    logger.Write("10 hello"); // FileLogger requires a line number
    // This throws an exception!
    logger.Write("hello");
} else {
    logger.Write("hello");
}

顺便说一下:new关键字不影响多态性,相反,它声明了一个全新的方法,恰好与基类型中的方法同名,但与之无关。特别是,不可能通过基类型调用它。要使多态性起作用,必须使用override关键字,并且方法必须是虚拟的(除非要实现接口)。

LSP说你必须能够像使用它的父类那样使用派生类:"程序中的对象应该可以用它们的子类型的实例来替换,而不会改变程序的正确性"。打破这一规则的经典继承是从Rectangle类派生出Square类,因为前者必须具有Height = Width,而后者可以具有Height != Width

public class Rectangle
{
    public virtual Int32 Height { get; set; }
    public virtual Int32 Width { get; set; }
}
public class Square : Rectangle
{
    public override Int32 Height
    {
        get { return base.Height; }
        set { SetDimensions(value); }
    }
    public override Int32 Width
    {
        get { return base.Width; }
        set { SetDimensions(value); }
    }
    private void SetDimensions(Int32 value)
    {
        base.Height = value;
        base.Width = value;
    }
}

在这种情况下,Width和Height属性的行为改变了,这违反了该规则。让我们看一下输出,看看为什么行为改变了:

private static void Main()
{
    Rectangle rectangle = new Square();
    rectangle.Height = 2;
    rectangle.Width = 3;
    Console.WriteLine("{0} x {1}", rectangle.Width, rectangle.Height);
}
// Output: 3 x 2

我认为Liskov的替换原则(LSP)主要是关于移动可能不同于子类的函数的实现,并使父类尽可能通用。

所以无论你在子类中做了什么改变,只要这个改变不强迫你修改父类中的代码,它就不会破坏Liskov的替换原则(LSP)。

子类型必须被基类型替换。

在接触方面。

派生类可以替换基类相同或更弱的前置条件和相同或更大的后置条件。

链接

要使多态性生效,LSP必须被遵守。打破它的一个好方法是在派生类型中引入不在基类型中的方法。在这种情况下,多态性不能工作,因为这些方法在基类型中不可用。您可以有一个方法的不同子类型实现,同时坚持多态性和LSP。