【问题标题】:What's wrong with the Square and Rectangle inheritance?Square 和 Rectangle 继承有什么问题?
【发布时间】:2013-05-24 04:16:15
【问题描述】:

我读过一些关于将 Square 作为 Rectangle 类的继承类是一种不好的做法的文章,说它违反了 LSP(Liskov 替换原则)。我还是不明白,我用 Ruby 做了一个示例代码:

class Rectangle
    attr_accessor :width, :height
    def initialize(width, height)
        @width = width
        @height = height
    end
end

class Square < Rectangle
    def initialize(length)
        super(length, length)
    end
    def width=(number)
        super(number)
        @height = number
    end

    def height=(number)
        super(number)
        @width = number
    end
end


s = Square.new(100)

s.width = 50

puts s.height

谁能告诉我这是怎么回事?

【问题讨论】:

  • 块状太空公主? youtube.com/watch?v=pJTrD3R5cj0
  • 哇,这很有趣,但我不太明白
  • yozloy,抱歉,我只是想说明您可能想解释一下 LSP 的含义,以便那些不了解它的人不必搜索。
  • 请不要链接到外部代码托管服务作为您的问题内容。您的问题需要独立且可回答,无需依赖任何外部链接。
  • @maerics,我不认为亚当在评论外部链接问题,我怀疑他是在评论实际问题,因为“通过规范”似乎与外部链接无关以及与“如果代码有效,请使用它”有关的一切。

标签: ruby oop liskov-substitution-principle


【解决方案1】:

考虑抽象基类或接口(无论是接口还是抽象类都是与LSP无关的实现细节)ReadableRectangle;它具有只读属性WidthHeight。可以从中派生出 ReadableSquare 类型,它具有相同的属性,但合同保证 WidthHeight 始终相等。

ReadableRectangle,可以定义具体类型ImmutableRectangle(在其构造函数中采用高度和宽度,并保证HeightWidth 属性将始终返回相同的值)和@987654331 @。也可以定义具体类型MutableRectangle,允许随时设置高度和宽度。

在事物的“正方形”方面,ImmutableSquare 应该可以替代ImmutableRectangleReadableSquare。然而,MutableSquare 只能替代ReadableSquare [后者又可以替代ReadableRectangle。] 此外,虽然ImmutableSquare 的行为可以替代ImmutableRectangle,但获得的值由继承具体的ImmutableRectangle 类型将受到限制。如果ImmutableRectangle 是一个抽象类型或接口,ImmutableSquare 类只需要使用一个字段而不是两个来保存它的维度(对于具有两个字段的类,保存一个没什么大不了的,但不难想象一下具有更多字段的类,其中的节省可能很大)。但是,如果 ImmutableRectangle 是一个具体类型,那么任何派生类型都必须具有其基类的所有字段。

某些类型的正方形可以替代相应类型的矩形,但可变正方形不能替代可变矩形。

【讨论】:

    【解决方案2】:

    我并不总是热衷于 Liskov,因为它似乎限制了您可以根据行为而不是“本质”对继承进行的操作。在我看来,继承总是意味着一种“是”的关系,而不是“行为完全像”的关系。

    话虽如此,the wikipedia article 使用您的确切示例详细说明了为什么这被某些人认为是不好的:

    违反 LSP 的典型示例是从 Rectangle 类派生的 Square 类,假设宽度和高度都存在 getter 和 setter 方法。

    Square 类始终假定宽度与高度相等。如果在需要 Rectangle 的上下文中使用 Square 对象,则可能会发生意外行为,因为 Square 的尺寸不能(或者说不应该)独立修改。

    这个问题不容易解决:如果我们可以修改 Square 类中的 setter 方法,使它们保持 Square 不变量(即保持尺寸相等),那么这些方法将削弱(违反)Rectangle 的后置条件setter,声明维度可以独立修改。

    因此,查看您的代码以及等效的 Rectangle 代码:

    s = Square.new(100)            r = Rectangle.new(100,100)
    s.width = 50                   r.width = 50
    puts s.height                  puts r.height
    

    左边的输出是 50,右边是 100。

    但是,在我看来,这个是这篇文章的重要部分:

    像这样的 LSP 违反,可能会或可能不会在实践中成为问题,这取决于使用违反 LSP 的类的代码实际预期的后置条件或不变量。

    换句话说,只要代码使用类理解行为,就没有问题。

    底线,正方形是矩形的真子集,对于矩形的足够松散的定义:-)

    【讨论】:

    • 感谢您的详细解释。我没有从你的Rectangle 代码中得到一件事是r = Rectangle.new(100) 行,你的意思是r = Rectangle.new(100, 100)
    • @yozloy,是的,很抱歉,这是一个剪切粘贴错误,尽管我可以声称它是一个单参数 Rectangle 构造函数,它构成了一个正方形 :-) 现在已修复。
    • @paxdiable 明白了!谈到s.heightr.height 的输出,我认为50100 是正确的输出,我对这一点是否正确?
    • @paxdiable 为什么将正方形和矩形作为 Quadrilateral 的子类不违反 LSP?
    • @yozloy,实际上你是对的,这也是违规行为。我已经删除了那段,但我更多地认为 LSP 在现实世界中的用途有限。
    【解决方案3】:

    从 Liskov 替换原则 (LSP) 的角度来看,它的问题在于您的 Rectangles 和 Squares 是可变的。这意味着您必须在子类中显式地重新实现 setter,并失去继承的好处。如果您使Rectangles 不可变,即如果您想要一个不同的Rectangle,您创建一个新的而不是更改现有的测量值,那么违反 LSP 没有问题。

    class Rectangle
      attr_reader :width, :height
    
      def initialize(width, height)
        @width = width
        @height = height
      end
    
      def area
        @width * @height
      end
    end
    
    class Square < Rectangle
      def initialize(length)
        super(length, length)
      end
    end
    

    使用attr_reader 提供getter 但不提供setter,因此具有不变性。通过此实现,RectanglesSquares 都提供了对 heightwidth 的可见性,对于正方形,它们将始终相同,并且区域的概念是一致的。

    【讨论】:

    • 呃!我可以看到你来自哪里,这是一个很好的解释。但这似乎使对象重用变得更加困难。为了调整对象的大小,您必须创建一个具有修改属性的全新对象,然后销毁旧对象。这并没有降低您的答案的有效性,只是降低了 LSP 在我眼中的实用性。
    • @pjs 为什么 mutable 失去了继承的好处?我觉得只要重新实现setter方法,Square类就可以复用Rectangle类中定义的方法,是不是一种好处?
    • @paxdiablo:我不提倡 LSP,只是试图解释(我的理解)它。但是,我确实倾向于支持不可变对象。不同尺寸的长方形就是不同的长方形!在许多情况下,这使事情变得更加安全。例如,考虑将一堆矩形放入按区域排序的二叉搜索树中。现在,如果您更改其中一个的维度,则该树将开始神秘地失败以供将来访问——其中一个元素突然违反了树的基本排序属性。像这样的错误很难追踪。
    • @yozloy:你越需要重写方法以使它们在子类中工作,你就越是说子类不是其父类的真正扩展,它是一个单独的东西。您必须在子类中放入的代码越多,您对继承的利用就越少。请注意,在不可变矩形中,除了初始化程序之外,我无需添加或覆盖任何内容。不要误会,我并不是要为 LSP 辩护为优于或低于其他观点,只是指出一些设计考虑因素。
    • @yozloy 不完全——我的意思是如果有更好的架构,在子类中重写方法是不好的。如果您要重写很多方法,那么接口可能是比继承更好的设计选择。当具有通用名称的抽象(例如 area)对不同的类(例如梯形、三角形和圆形)具有完全不同的实现时,我会使用接口。一个接口告诉我所有的几何图形都有一个区域的概念,但是这个概念的实现是类特定的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-07
    • 2017-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-05
    相关资源
    最近更新 更多