物件導向方法的困境--正方形不能繼承自長方形?
轉摘:http://blog.csdn.net/javayuan/article/details/1191751
物件導向理論誕生之初,由於沒有最佳實務的指導,往往導致繼承的濫用。一個很著名的例子就是java類庫中的堆棧類Stack繼承自向量類Vector。
public class Stack extends Vector ...{
public Object push(Object item) ...{
addElement(item);
return item;
}
public synchronized Object pop() ...{
//...
}
}
僅僅是為了重用Vector的管理元素的方法,就讓Stack繼承自Vector,導致了一個蹩腳的設計。人們後來總結出一條規律:優先使用組合而不是繼承,只有當類A確實"is a"類B的時候,才使用繼承。Stack不是Vector,所以這個時候應該使用組合。範例程式碼如下:
public class Stack...{
private Vector vector;
public Object push(Object item) ...{
vector.addElement(item);
return item;
}
//...
}
只有符合"is a"關係時才能使用繼承,這條規律是符合我們的直覺的。物件導向技術就是用軟體裡面的對象類比現實中的對象。如果狗不是貓,當然不能讓狗繼承自貓。但是實際的軟體開發中,人們很快發現有些情況下即使類A確實"is a"類B,也不能使用繼承。最著名的一個例子就是正方形不能繼承自長方形了,很多講物件導向設計的書裡都有。這是一個明顯違反直覺的例子,範例程式碼如下:
public class Rectangle ...{
private double width;
private double height;
public Rectangle(double width,double height)...{
this.width=width;this.height=height;
}
public double getHeight() ...{
return height;
}
public void setHeight(double height) ...{
this.height = height;
}
public double getWidth() ...{
return width;
}
public void setWidth(double width) ...{
this.width = width;
}
//...
}
public class Square extends Rectangle...{
public void setHeight(double height) ...{
super.setHeight(height);
super.setWidth(height);
}
public void setWidth(double width) ...{
super.setHeight(width);
super.setWidth(width);
}
//...
}
因為Rectangle類中的setHeight和setWidth方法對Square類不合適,為了保證正方形的長和寬相等,改寫了這兩個方法。上面的代碼初看上去很合理,其實有問題。這個代碼的缺陷我就不詳細討論了,很多書都對其做了詳細的分析。我只是從理論上指出其不合理之處。根據契約式設計,有下面的原則:
子類的前置條件不能強於父類的前置條件。
子類的後置條件不能弱於父類的後置條件。
子類的類不變式不能弱於父類的類不變式。
這個原則也可以由Liskov替換原則得來。比方說要能夠把父類替換成子類,那麼父類所接受的任何參數,子類也必須能接受,也就是子類的前置條件不能強於父類的前置條件。
對於長方形類的setHeight方法,其前置條件是height>0,其後置條件是this.height==height&&this.width==old.width。對於正方形類的setHeight方法,其前置條件也是height>0,不強於父類的前置條件。但是其後置條件變成this.height==height&&this.width==height。它違背了父類的後置條件。子類的後置條件不能弱於父類的後置條件的意思是子類必需遵守父類的後置條件,同時還可以加上自己的更強的後置條件。
對於上面這種反常的情況,人們提出了子類型的概念。上面的代碼中Square類是Rectangle類的子類(subclass),但是不是子類型(subtype)。因為它違反了Liskov替換原則。這個概念的引入應該說是有很大的混淆性。
那麼到底為什麼會出現這種反常的情況,正方形就是長方形,使用繼承是合情合理的。如果一個設計方法會出現太多的例外情況,那它肯定不是一種好的設計方法。
老師:同學們請看!這是一個正方形,現在我讓它的垂直邊高度增加,現在成為什麼形狀了?
學生:報告老師,是長方形。
老師:回答正確。
我們看到在現實中完全可以單獨改變正方形的邊長,這時改變以後的形狀不再是正方形了。通過和現實情況對比,我們受到了啟發。那就是正方形和長方形應該是不可變類。當它的邊長改變以後,就變成了一個新的長方形。
public class Rectangle ...{
private double width;
private double height;
public Rectangle(double width,double height)...{
this.width=width;this.height=height;
}
public double getHeight() ...{
return height;
}
public double getWidth() ...{
return width;
}
public Rectangle changeHeight(double height) ...{
return new Rectangle(this.width,height);
}
public Rectangle changeWidth(double width) ...{
return new Rectangle(width,this.height);
}
}
public class Square extends Rectangle ...{
public Square(double dege) ...{
super(dege, dege);
}
public Square changeEdge(double edge)...{
return new Square(edge);
}
}
通過這種思路,我們就得到了一個完全和現實對象相吻合的設計。所以說並不是正方形不能繼承自長方形。