Liskov Substitution Principle (LSP) 里氏替換原則。
LSP的定義如下:
Let Φ(x) be a property provable about objects of x of type T. Then Φ(y) should be provable for objects y of type S where S is a subtype of T.
其意思為「程式中父類物件都可由子類物件替代且行為不變」。
從上可看出LSP關注物件導向程式(OOP)繼承的問題,重點在於子類替代父類時的行為不變。
繼承(inheritance)為OOP三大特性之一,當一個類別繼承另一個類別時,繼承類別可擁有被繼承類別的特性,這些特性包括屬性與方法。
Java程式用extends
關鍵字表示兩類別的繼承關係如下,Child
繼承Parent
。
Parent 被繼承類/父類
class Parent {
}
Child 繼承類/子類
class Child extends Parent {
}
繼承希望增加程式的重用性。子類可直接引用、擴增或覆寫父類的特性來增強功能。但Java沒有限制子類繼承父類後該如何擴增及覆寫父類的行為,任意繼承反而令程式僵固、無法預期且難以維護。所以LSP說明了設計繼承時子類替代父類時的行為必須不變的原則。
下面以常見的矩形(Rectangle)與方形(Square)來說明未遵守LSP引發的問題。
一個矩形類別Rectangle
。getArea()
方法用長乘寬取得面積。
Rectangle
public class Rectangle {
private int length;
private int width;
public int getArea() {
return this.length * width;
}
public void setLength(int length) {
this.length = length;
}
public void setWidth(int width) {
this.width = width;
}
}
在客戶端類Client
的calculateArea()
計算傳入的長寬及矩形取得面積如下。
Client
public class Client {
public int calculateArea(int length, int width, Rectangle r) {
r.setLength(length);
r.setWidth(width);
return r.getArea();
}
}
測試長等於2,寬等於3及矩形物件得到的面積為6。
@Test
public void calculateArea_rectangleWithLength2AndWidth3() {
Client client = new Client();
int length = 2;
int width = 3;
int area = client.calculateArea(length, width, new Rectangle());
Assertions.assertEquals(6, area); // pass
}
幾何上直觀地認為方形是一種矩形,因為都有一對長寬且每個邊互為直角,因此Square
繼承Rectangle
,由於方形長寬相等,所以覆寫setLength()
及setWidth()
使無論設定長或寬皆令長寬相等。
Square
public class Square extends Rectangle {
@Override
public void setLength(int length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(int width) {
super.setLength(width);
super.setWidth(width);
}
}
測試客戶端若傳入方形物件卻導致非預期的結果,這就是設計Square
繼承Rectangle
時違反LSP的結果,子類的行為改變(覆寫)了父類原本的行為,造成子類無法替代父類。
@Test
public void calculateArea_squareWidth3() {
Client client = new Client();
int length = 2;
int width = 3;
int area = client.calculateArea(length, width, new Square());
Assertions.assertEquals(6, area); // fail
}
Square
行為改變的地方在覆寫setLength()
及setWidth()
,超出Rectangle
預期單純設定長寬的行為,為了達到長寬相等的條件而更改了行為。
從以上來看Square
繼承Rectangle
是否恰當?雖然幾何上方形是矩形,但方形有比矩形更嚴格長寬相等的特性,兩者共有的特性為四個邊與彼此互為直角,但Rectangle
目前並無此相關屬性的設計,也就是說Square
繼承Rectangle
毫無意義。設計繼承關係時應看子類是否會改變父類的行為,不能僅從常識或其他領域的定義來決定。
所以子類要能替代父類則不應覆寫父類的方法,因為覆寫等同於改變行為;換句話說子類繼承父類只能擴充新的特性,不應覆寫父類的特性,否則就違反了LSP。若要覆寫就必須符合原來預期的行為。
沒有留言:
張貼留言