【问题标题】:How to comply with Liskov's Substitution Principle (LSP) and still benefit from polymorphism?如何遵守 Liskov 替换原则 (LSP) 并仍然受益于多态性?
【发布时间】:2013-01-18 16:52:39
【问题描述】:

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

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

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

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

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

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

从这里查看示例:http://www.oodesign.com/liskov-s-substitution-principle.html

【问题讨论】:

  • "LSP 说“派生类型不能改变基类型的行为””——它不是这么说的。
  • 您是否研究过 [SOLID](en.wikipedia.org/wiki/Solid_(object-oriented_design) 设计模式?
  • LSP 声明 “程序中的对象应该可以替换为其子类型的实例,而不会改变该程序的正确性”。你的来源是什么?
  • LSP 说对象的任何子类型都应该能够替换基本类型,而无需更改程序。例如,如果我对数据访问进行了抽象,我应该能够将数据库实现与文件系统实现交换出来,而无需修改程序
  • @TheLight:恐怕你被误导了; LSP 不是关于改变行为,而是关于改变正确性/接口/合同。

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


【解决方案1】:

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

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

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

【讨论】:

  • 这是有道理的。所以它是关于行为而不是合同。
  • 不正确是不是得到了预期的结果?我的意思是......如果你有一个计算器,正确性就是得到没有错误的结果。正确性将由用例定义......
【解决方案2】:

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

作为示例,假设您声明了一个记录器,使您可以将消息记录到控制台

logger.WriteLine("hello");

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


如果你需要编写如下代码,那么就会违反 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 关键字并且方法必须是虚拟的(除非您正在实现接口)。

【讨论】:

  • 如果 FileLogger 继承自 ConsoleLogger 那么在什么情况下程序将无法接受 FileLogger(派生类)?!你能用一些属性或方法做一个例子吗?
  • 这是违反 OCP 的示例。
  • 如果文件记录器需要以特定方式格式化消息(我提到了行号作为示例)而不是控制台记录器,这将违反 LSP,因为使用记录器的代码最可能必须更改才能使用新的记录器。
  • 不,OCP 处理软件实体的可扩展性,而 LSP 处理派生类型的兼容性。
  • 不,您解释的是 OCP 而不是 LSP。 LSP 是关于行为是否对于基的所有子类型仍然正确。如果您正在检查特定的派生类型,然后有不同的实现,那么您就违反了 OCP,因为稍后对于 DatabaseLogger,您可能希望在示例中使用不同的格式。
【解决方案3】:

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

【讨论】:

  • 它违反 LSP 不是因为代码错误或在运行时有任何问题。它违反了 LSP,因为它不是应有的正确行为。
  • 对不起...我不明白。什么意思?
  • 我的意思是谁定义了什么是“正确的”,什么是不正确的?相同的代码在一个应用程序中可能是正确的并且不违反 LSP,但在另一个应用程序中却违反了 LSP,因为该应用程序中正确性的定义会有所不同。
  • 不……正确性有一个绝对定义,可以普遍定义。这就像说“我开车”是正确的,只是因为您认为这对您是正确的。有共同的语法规则已经制定并被所有“最终用户”广泛接受。编程语言也是如此。
  • 我不同意这个代码示例违反了 LSP。首先,矩形不一定有Width != Height。其次,如果您可以更改 Rectangle 的各个尺寸,您应该能够在 Square 中一起更改它们。 Square 所做的只是调用 Rectangle 的公共属性,因此它与 Rectangle 的行为并不矛盾。如果 Rectangle 有一个方法 SetDimensions(w, h) 那么 Square 会破坏 LSP。
【解决方案4】:

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

因此,无论您在子类中进行什么更改,只要此更改不会强制您修改父类中的代码,它都不会违反 Liskov 替换原则 (LSP)。

【讨论】:

    【解决方案5】:

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

    在联系人方面。

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

    Link

    【讨论】:

    • 你能扩展一下吗?我不确定我是否遵循最后一句话。
    【解决方案6】:

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

    【讨论】:

    • “在派生类型中引入[ing]方法”不一定违反父类型定义的约定。
    • 但是有一个方法只能在派生类型中使用,从而在可以访问该行为之前强制转换,这不是违反该原则吗?调用类型需要知道它在不关心时使用的是哪个派生类型。显然这与多态性相反,但它会破坏 LSP 吗?
    • 关键是在需要 ParentType 的地方,任何 ChildType 都应该足够,而不会破坏任何东西。如果 ChildType 需要一个函数,只有它定义为被调用,那么是的,这将违反 LSP。附加功能本身不会导致违反 LSP。请参阅 StreamFileStream 类。 FileStream 具有附加功能,但仍可用于任何只需要 Stream 的事物。
    猜你喜欢
    • 1970-01-01
    • 2015-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-03
    • 2016-08-20
    • 1970-01-01
    相关资源
    最近更新 更多