l

2017年2月13日 星期一

從合約判斷類別行為是否相容

Feb. 13 12:08~12:52

屏幕截图 2017-02-13 12.47.38

 

Liskov Substitution Principle(LSP,里氏替換原則)和Design By Contract(DBC,依合約設計)所說的Subcontracting(子合約)兩者是一樣的東西。上周六在上「敏捷開發懶人包:物件導向技能」課程的時候Teddy引用了《Agile Software Development》書中LSP的例子:

▼假設某人做了一個物件導向設計,讓Square(正方形)繼承Rectangle(長方形)。

屏幕截图 2017-02-09 14.26.59

 

▼Rectangle的程式碼如下。

屏幕截图 2017-02-09 14.29.18

 

▼以下是Square的程式碼,因為正方形的四邊相等,所以Square類別覆寫(override)Rectangle的setWidth與setHeight這兩個method,無論是設定正方型的寬或高,需要同時把高或寬設成一樣。

屏幕截图 2017-02-13 12.19.46

 

▼上述設計到目前為止看起來沒什麼問題,接下來看兩個測試案例。testRectangleArea()設定長方形的高和寬分別是5,4,所以得到面積為20。依照Liskov Substitution Principle的定義:「系統中基本型別(base types)出現的地方,都可以用子型別( subtypes)來取代,而不會破壞程式原有的行為。」所以在testSquareArea()測試案例中,第23行把 r 設定為 Square的instance,因為Square是Rectangle的子類別,如果符合LSP原則,那麼Square不會破壞原有程式的行為,也就是說第27行計算面積的結果應該還是20。

屏幕截图 2017-02-13 12.28.49

 

▼但鄉民們一看就知道第二個測試案例會失敗,因為正方形的面積應該是16而不是20。也就是說在這個例子中,基本型別(Rectangle)出現的地方用子型別( Square)來取代的結果破壞程式原有的行為,因此違反了LSP。

屏幕截图 2017-02-13 12.32.17

***

因為Teddy上課的時候是從Design by Contract的角度來解釋LSP,因此有學員問:「這個例子要如何從合約的角度來判斷?」很簡單:

Rectangle的setHeight的postcondition:

this.height = height;

this.width = old width;

 

Square的setHeight的postcondition:

this.height = height;

this.width = height;

 

▼依據DBC的Subcontracting原則,子類別的postcondition需要與父類別的postcondition進行AND運算

屏幕截图 2017-02-13 12.40.44

 

▼所以Square的setHeight的postcondition實際上應該是:

this.height = height;

this.width = old width;

AND

this.height = height;

this.width = height;

很顯然this.width = old width;this.width = height;這兩條合約無法同時被滿足,所以Square的行為無法與其父類別相容,因此違反了Subcontracting原則。

***

友藏內心獨白:合約可以寫在程式碼中,也可寫在測試案例中。

2 則留言:

  1. 所以,應該是讓Rectangle(長方形)繼承Square(正方形)??

    回覆刪除
  2. 兩者行為不相容,所以也許可以另外建一個Shape類別,讓Rectangle和Square都繼承自Shape。

    回覆刪除