2013-05-29 63 views
6

我读过一些关于使Square成为Rectangle类的继承类的做法的文章,这是一种不好的做法,称它违反了LSP(Liskov替代原则)。我仍然不明白,我在Ruby中做了一个示例代码:Square和Rectangle继承有什么问题?

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 

有人能告诉我它有什么问题吗?

+0

块状太空公主? http://www.youtube.com/watch?v=pJTrD3R5cj0 – paxdiablo

+0

哇,这很有趣,但我不太明白 – mko

+1

yozloy,道歉,我只是提出一点,你可能想解释你的意思LSP,以便那些不知道它的人不必搜索。 – paxdiablo

回答

4

我并不总是热衷于Liskov,因为它似乎限制了基于行为而不是“本质”的继承。在我看来,继承总是意味着一种“是一种”的关系,而不是“完全一样”。

话虽如此,the wikipedia article进入细节为为什么这被认为是由一些不好的,使用您的具体例子:

违反LSP一个典型的例子是一个正方形类,它从矩形派生类,假设getter和setter方法都存在宽度和高度。

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

这个问题不容易修复:如果我们可以在Square类中修改setter方法,以便它们保留Square不变量(即保持尺寸相等),那么这些方法将削弱(违反)后置条件矩形设置器,其中声明尺寸可以独立修改。

所以,看你的代码旁边的等效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的类的代码实际预期的后置条件或不变量。

换句话说,只要使用类代码理解的行为,不存在问题。

底线,方形是长方形的真子集,为矩形的活够清晰:-)

+0

感谢您的详细解释。我没有从'矩形'代码得到的是'r = Rectangle.new(100)'这一行,你的意思是'r = Rectangle.new(100,100)'? – mko

+0

@yozloy,是的,道歉,这是一个cut'n'paste错误,虽然我可以声称它是一个单一的参数矩形构造函数,它使一个正方形:-)现在修复。 – paxdiablo

+0

@paxdiable明白了!谈论's.height'和'r.height'的输出,我认为'50','100'是正确的输出,我对这一点是否正确? – mko

2

什么地方错了,从里氏替换原则(LSP)的观点是,你Rectangle小号和Square是可变的。这意味着你必须显式地重新实现子类中的setter,并失去继承的好处。如果你使得Rectangle是不变的,即如果你想要一个不同的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而不是制定者,因此不变性。通过这个实施,RectanglesSquares提供对heightwidth的可见度,对于一个正方形,这些将永远是相同的,并且区域的概念是一致的。

+0

呃!我可以看到你来自哪里,这是一个很好的解释。但看起来这使得对象重用更加困难。为了调整对象的大小,你必须创建一个具有修改属性的全新对象,然后销毁旧对象。这并不能使你的答案变得毫无意义,只会降低LSP在我眼中的实用性。 – paxdiablo

+0

@pjs为什么mutable会失去继承的好处?我认为只需重新实现setter方法,'Square'类可以重用'Rectangle'类中定义的方法,这是一种好处吗? – mko

+0

@paxdiablo:我不主张LSP,只是试图解释(我的理解)它。但是,我倾向于支持不可变对象。不同尺寸的矩形是不同的矩形!这在很多情况下使事情更安全。例如,考虑将一堆矩形放在按其区域排序的二叉搜索树中。现在如果你改变了其中一个维度,那么这棵树就会在未来的访问中神秘地失败 - 其中一个元素突然违反了树的基本排序属性。像这样的错误可能很难追查。 – pjs

0

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

ReadableRectangle从,一个可以定义具体类型ImmutableRectangle(这需要在其构造的高度和宽度,并保证了HeightWidth属性将始终返回相同的值),和MutableRectangle。人们还可以定义具体类型MutableRectangle,它允许随时设置高度和宽度。

在事物的“正方形”一侧,ImmutableSquare应该可替代ImmutableRectangleReadableSquare。但是,MutableSquare仅可替代ReadableSquare [可替代ReadableRectangle。]此外,尽管ImmutableSquare的行为可替代ImmutableRectangle,但通过继承具体的ImmutableRectangle类型获得的值将受到限制。如果ImmutableRectangle是抽象类型或接口,那么ImmutableSquare类只需要使用一个字段而不是两个来保存其维度(对于具有两个字段的类,保存一个并不是什么大问题,但不难想象带有很多领域,其中节省可能很大)。但是,如果ImmutableRectangle是一个具体类型,那么任何派生类型都必须具有其基数的所有字段。

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