### 一、什么是里氏替換原則
里氏替換原則的嚴格表達是:
如果對每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都替換成o2時,程序P的行為沒有變化,那么類型T2是類型T1的子類型。
換言之,一個軟件實體如果使用的是一個基類的話,那么一定適用于其子類,而且它根本不能察覺出基類對象和子類對象的區別。
比如,假設有兩個類,一個是Base類,另一個是Child類,并且Child類是Base的子類。那么一個方法如果可以接受一個基類對象b的話:method1(Base b)那么它必然可以接受一個子類的對象method1(Child c).
里氏替換原則是繼承復用的基石。只有當衍生類可以替換掉基類,軟件單位的功能不會受到影響時,基類才能真正的被復用,而衍生類也才能夠在基類的基礎上增加新的行為。
但是需要注意的是,反過來的代換是不能成立的,如果一個軟件實體使用的是一個子類的話,那么它不一定適用于基類。如果一個方法method2接受子類對象為參數的話method2(Child c),那么一般而言不可以有method2(b).
### 二、墨子的智慧
《墨子:小取》中說,“白馬,馬也;乘白馬,乘馬也。驪馬,馬也;乘驪馬,乘馬也”。文中的驪馬是黑的馬。意思就是白馬和黑馬都是馬,乘白馬或者乘黑馬就是乘馬。在面向對象中我們可以這樣理解,馬是一個父類,白馬和黑馬都是馬的子類,我們說乘馬是沒有問題的,那么我們把父類換成具體的子類,也就是乘白馬和乘黑馬也是沒有問題的,這就是我們上邊說的里氏替換原則。
墨子同時還指出了反過來是不能成立的。《墨子:小取》中說:“娣,美人也,愛娣,非愛美人也”。娣是指妹妹,也就是說我的妹妹是沒人,我愛我的妹妹(出于兄妹感情),但是不等于我愛美人。在面向對象里就是,美人是一個父類,妹妹是美人的一個子類。哥哥作為一個類有“喜愛()”方法,可以接受妹妹作為參量。那么這個“喜愛()”不能接受美人類的實例,這也就說明了反過來是不能成立的。
### 三、正方形是不是長方形
上過數學課的人都知道,正方形是一種特殊的長方形,只不過是它的長和寬是一樣的,也就是說我們在面向對象里我們應當將長方形設計成父類,將正方形設計成長方形的子類,但是我可以很負責的告訴你,**這樣做是錯誤的,是不符合里氏替換原則的。**
~~~
package com.designphilsophy.lsp.version1;
/**
* 定義一個長方形類,只有標準的get和set方法
*
* @author xingjiarong
*
*/
public class Rectangle {
protected long width;
protected long height;
public void setWidth(long width) {
this.width = width;
}
public long getWidth() {
return this.width;
}
public void setHeight(long height) {
this.height = height;
}
public long getHeight() {
return this.height;
}
}
~~~
~~~
package com.designphilsophy.lsp.version1;
/**
* 定義一個正方形類繼承自長方形類,只有一個side
*
* @author xingjiarong
*
*/
public class Square extends Rectangle {
public void setWidth(long width) {
this.height = width;
this.width = width;
}
public long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
this.width = height;
}
public long getHeight() {
return height;
}
}
~~~
~~~
package com.designphilsophy.lsp.version1;
public class SmartTest
{
/**
* 長方形的長不短的增加直到超過寬
* @param r
*/
public void resize(Rectangle r)
{
while (r.getHeight() <= r.getWidth() )
{
r.setHeight(r.getHeight() + 1);
}
}
}
~~~
在上邊的代碼中我們定義了一個長方形和一個繼承自長方形的正方形,看著是非常符合邏輯的,但是當我們調用SmartTest類中的resize方法時,長方形是可以的,但是正方形就會一直增大,一直long溢出。但是我們按照我們的里氏替換原則,父類可以的地方,換成子類一定也可以,所以上邊的這個例子是不符合里氏替換原則的。
問題由來:有一功能P1,由類A完成。現需要將功能P1進行擴展,擴展后的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。
解決方案:當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。

剛才我們寫的代碼的結構就是上邊那樣的,對于這樣不符合里氏替換原則原則的關系,我們在代碼重構的時候一般采用下面的方法。

我們再定義一個他們共同的父類,然后讓正方形和長方形都繼承自這個父類。
具體的代碼如下:
~~~
package com.designphilsophy.lsp.version2;
/**
* 定義一個四邊形類,只有get方法沒有set方法
* @author xingjiarong
*
*/
public abstract class Quadrangle {
protected abstract long getWidth();
protected abstract long getHeight();
}
~~~
~~~
package com.designphilsophy.lsp.version2;
/**
* 自己聲明height和width
* @author xingjiarong
*
*/
public class Rectangle extends Quadrangle {
private long width;
private long height;
public void setWidth(long width) {
this.width = width;
}
public long getWidth() {
return this.width;
}
public void setHeight(long height) {
this.height = height;
}
public long getHeight() {
return this.height;
}
}
~~~
~~~
package com.designphilsophy.lsp.version2;
/**
* 自己聲明height和width
* @author xingjiarong
*
*/
public class Square extends Quadrangle
{
private long width;
private long height;
public void setWidth(long width) {
this.height = width;
this.width = width;
}
public long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
this.width = height;
}
public long getHeight() {
return height;
}
}
~~~
在基類Quadrange類中沒有賦值方法,因此類似于SamrtTest的resize()方法不可能適用于Quadrangle類型,而只能適用于不同的具體子類Rectangle和Aquare,因此里氏替換原則不可能被破壞了。
### 四、為什么要符合里氏替換原則
里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。它包含以下4層含義:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類中可以增加自己特有的方法。
- 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
- 當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
看上去很不可思議,因為我們會發現在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什么后果?來看一個例子。
~~~
package com.designphilsophy.lsp.version3;
public class A{
public int func1(int a, int b){
return a-b;
}
}
~~~
~~~
package com.designphilsophy.lsp.version3;
public class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
~~~
~~~
package com.designphilsophy.lsp.version3;
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
~~~
輸入結果:
100-50=150
100-80=180
100+20+100=220
我們發現原本運行正常的相減功能發生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法,造成所有運行相減功能的代碼全部調用了類B重寫后的方法,造成原本運行正常的功能出現了錯誤。在本例中,引用基類A完成的功能,換成子類B之后,發生了異常。在實際編程中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的幾率非常大。
源碼下載:[http://download.csdn.net/detail/xingjiarong/9308063](http://download.csdn.net/detail/xingjiarong/9308063)
- 前言
- 設計原則(一)&quot;開-閉&quot;原則(OCP)
- 設計原則(二)里氏替換原則(LSP)
- 設計原則(三)組合復用原則
- 設計原則(四)依賴倒置原則(DIP)
- 設計模式(一)簡單工廠模式
- 設計模式(二)工廠方法模式
- 設計模式(三)抽象工廠模式
- 設計模式(四)單例模式
- 設計模式(五)創建者模式(Builder)
- 設計模式(六)原型模式
- 設計模式(七)門面模式(Facade Pattern 外觀模式)
- 設計模式(八)橋梁模式(Bridge)
- 設計模式(九)裝飾模式(Decorator)
- 設計模式(十)適配器模式
- 設計模式(十一)策略模式
- 設計模式(十二)責任鏈模式
- 設計模式之UML(一)類圖以及類間關系(泛化 、實現、依賴、關聯、聚合、組合)
- 設計模式之橋梁模式和策略模式的區別