網頁

2021/1/2

Java SOLID Liskov Substitution Principle 里氏替換原則

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引發的問題。

一個矩形類別RectanglegetArea()方法用長乘寬取得面積。

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;
    }

}

在客戶端類ClientcalculateArea()計算傳入的長寬及矩形取得面積如下。

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。若要覆寫就必須符合原來預期的行為。


沒有留言:

張貼留言