2013-05-29 62 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是一個具體類型,那麼任何派生類型都必須具有其基數的所有字段。

某些類型的方形可替代相應類型的矩形,但可變方形不可替代可變矩形。