替換原則
Liskov替換原則:子類型必須能夠替換它們的基類型
表述
1. 如果每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程式P在所有的對象o1都代換為o2時,程式P的行為沒有變化,那么類型T2是類型T1的子類型。
2. 換言之,一個軟體實體如果使用的是一個基類的話,那么一定適用於其子類,而且它根本不能察覺出基類對象和子類對象的區別。只有衍生類替換基類的同時軟體實體的功能沒有發生變化,基類才能真正被復用。
3. 里氏代換原則由Barbar Liskov(芭芭拉.里氏)提出,是繼承復用的基石。
4. 一個繼承是否符合里氏代換原則,可以判斷該繼承是否合理(是否隱藏有缺陷)。
理解
(1) 應當儘量從抽象類繼承,而不從具體類繼承。
一般而言,如果有兩個具體類A、B有繼承關係,那么一個最簡單的修改方案是建立一個抽象類C,然後讓類A和B成為抽象類C的子類。即如果有一個由繼承關係形成的等級結構的話,那么在等級結構的樹形圖上面所有的樹葉節點都應當是具體類,而所有的樹枝節點都應當是抽象類或者接口。
總結
1. 為了保持LSP,所有子類必須符合使用基類的client所期望的行為。
2. 一個子類型不得具有比基類型(base type)更多的限制,可能這對於基類型來說是合法的,但是可能會因為違背子類型的其中一個額外限制,從而違背了LSP!
3. LSP保證一個子類總是能夠被用在其基類可以出現的地方!
備註
LSP講的是基類和子類的關係。只有當這種關係存在時,里氏代換關係才存在。如果兩個具體的類A,B之間的關係違反了LSP的設計,(假設是從B到A的繼承關係)那么根據具體的情況可以在下面的兩種重構方案中選擇一種。 創建一個新的抽象類C,作為兩個具體類的超類,將A,B的共同行為移動到C中來解決問題。 從B到A的繼承關係改為委派關係。
在進行設計的時候,我們儘量從抽象類繼承,而不是從具體類繼承。如果從繼承等級樹來看,所有葉子節點應當是具體類,而所有的樹枝節點應當是抽象類或者接口。當然這個只是一個一般性的指導原則,使用的時候還要具體情況具體分析。
舉例
Composite模式,Proxy模式,Strategy模式
里氏代換原則(Liskov Substitution Principle)
里氏代換原則是由麻省理工學院(MIT)計算機科學實驗室的Liskov女士,在1987年的OOPSLA大會上發表的一篇文章《Data Abstraction and Hierarchy》裡面提出來的,主要闡述了有關繼承的一些原則,也就是什麼時候應該使用繼承,什麼時候不應該使用繼承,以及其中的蘊涵的原理。2002年,軟體工程大師Robert C. Martin,出版了一本《Agile Software Development Principles Patterns and Practices》,在文中他把里氏代換原則最終簡化為一句話:"Subtypes must be substitutable for their base types",也就是說,子類必須能夠替換成它們的基類。
我們把里氏代換原則解釋得更完整一些:在一個軟體系統中,子類應該可以替換任何基類能夠出現的地方,並且經過替換以後,代碼還能正常工作。子類也能夠在基類的基礎上增加新的行為。
里氏代換原則是對開閉原則的補充,它講的是基類和子類的關係。只有當這種關係存在時,里氏代換關係才存在。
"正方形是長方形"是一個理解里氏代換原則的最經典的例子。在數學領域裡,正方形毫無疑問是長方形,它是一個長寬相等的長方形。所以,應該讓正方形繼承自長方形。
長方形類如程式10-1所示。
程式10-1 長方形類Rectangle.java
package principle.liskovsubstitution; public class Rectangle { private int height; private int width; public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } } 繼承了長方形的正方形類如程式10-2所示。
程式10-2 正方形類Square.java
package principle.liskovsubstitution; public class Square extends Rectangle { public void setWidth(int width) { super.setWidth(width); super.setHeight(width); } public void setHeight(int height) { super.setWidth(height); super.setHeight(height); } } 由於正方形的長度和寬度必須相等,所以在方法setLength()和setWidth()中,對長度和寬度賦值相同。程式10-3所示的測試類中的函式zoom()用來增加長方形的長和寬。
程式10-3 測試類TestRectangle.java
package principle.liskovsubstitution; public class TestRectangle { public void zoom(Rectangle rectangle, int width, int height) { rectangle.setWidth(rectangle.getWidth() + width); rectangle.setHeight(rectangle.getHeight() + height); } } 顯然,當增加的長度和寬度不同時,不能夠將其中的長方形換成其子類正方形。這就違反了里氏代換原則。
為了符合里氏代換原則,我們可以為長方形和正方形創建一個父類Base,並在其中定義好共有的屬性,並定義一個zoom()抽象函式,如程式10-4所示。
程式10-4 父類Base.java
package principle.liskovsubstitution; public abstract class Base { private int height; private int width; public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public abstract void zoom(int width, int height); } 長方形類繼承自該父類,並編寫自己的zoom()實現函式,如程式10-5所示。
程式10-5 修改後的長方形類BaseRectangle.java
package principle.liskovsubstitution; public class BaseRectangle extends Base { public void zoom(int width, int height) { setWidth(getWidth() + width); setHeight(getHeight() + height); } } 正方形類也繼承自該父類,並編寫自己的zoom()實現函式,如程式10-6所示。
程式10-6 修改後的正方形類BaseSquare.java
package principle.liskovsubstitution; public class BaseSquare extends Base { public void setWidth(int width) { super.setWidth(width); super.setHeight(width); } public void setHeight(int height) { super.setWidth(height); super.setHeight(height); } public void zoom(int width, int height) { int length = (width + height) /2; setWidth(getWidth() + length); setHeight(getHeight() + length); } } 編寫測試函式如程式10-7所示。
程式10-7 修改後的測試類BastTest.java
package principle.liskovsubstitution; public class BastTest { public void zoom(Base base, int width, int height) { base.zoom(width, height); } } 此時的Base類可以被它的子類Rectangle和Square所替代,而不用改變測試代碼。這就是符合里氏代換原則的編寫方式。
由此可見,在進行設計的時候,我們儘量從抽象類繼承,而不是從具體類繼承。如果從繼承等級樹來看,所有葉子節點應當是具體類,而所有的樹枝節點應當是抽象類或者接口。當然這只是一個一般性的指導原則,使用的時候還要具體情況具體分析。