如何遵守Liskov替换原则(LSP)并仍然受益于多态?

时间:2013-01-18 16:52:39

标签: c# polymorphism solid-principles liskov-substitution-principle

LSP说“派生类型不能改变基类型的行为”,换言之,“派生类型必须完全可替换它们的基类型”。

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

此外,如果我们使用new关键字在驱动方法中隐藏方法,那么我们又违反了这个原则。

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

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

那么,当你需要继承并且你想从多态中受益时,如何设计符合这个原则的应用程序?我很困惑!

请参阅此处的示例:http://www.oodesign.com/liskov-s-substitution-principle.html

6 个答案:

答案 0 :(得分:6)

Barbara Liskov有一篇非常好的文章Data Abstraction and Hierarchy,她特别涉及多态行为和虚拟软件构造。阅读本文后,您可以看到,她深入描述了软件组件如何通过简单的多态调用实现灵活性和模块化。

LSP说明了实现细节,而不是抽象。具体来说,如果您使用T类型的某些接口或抽象,您应该通过T的所有子类型而不是观察 意外 行为或程序崩溃。

此处的关键字是意外,因为它可以描述程序的任何属性(正确性,执行的任务,返回的语义,临时等)。因此,制作方法virtual并不意味着违反LSP

答案 1 :(得分:2)

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

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

答案 2 :(得分:2)

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

答案 3 :(得分:2)

“派生类型不得更改基类型的行为”意味着必须可以使用派生类型,就像使用基类型一样。例如,如果您能够拨打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关键字,并且该方法必须是虚拟的(除非您正在实现接口)。

答案 4 :(得分:0)

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

就联系而言。

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

Link

答案 5 :(得分:-1)

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