1. 程式人生 > >第2章 面向物件的設計原則(SOLID):2_里氏替換原則(LSP)

第2章 面向物件的設計原則(SOLID):2_里氏替換原則(LSP)

2. 里氏替換原則(Liskov Substitution Principle,LSP)

2.1 定義

(1)所有使用基類的地方必須能透明地使用子類替換,而程式的行為沒有任何變化(不會產生執行結果錯誤或異常)。只有這樣,父類才能被真正複用,而且子類也能夠在父類的基礎上增加新的行為。也只有這樣才能正確的實現多型

(2)當一個類繼承了另一個類時,子類就擁有了父類中可以繼承下來的屬性和操作。但如果子類覆蓋了父類的某些方法,那麼原來使用父類的地方就可能會出現錯誤,因為表面上看,它呼叫了父類的方法,但實際執行時卻呼叫了被子類覆蓋的方法,而這兩個方法的實現可能不一樣,這就不符合LSP原則。(見後面的解決方案)

(3)里氏代換原則是實現開閉原則的重要方式之一,由於使用基類物件的地方都可以使用子類物件,因此在程式中儘量使用基類型別來對物件進行定義,而在執行時再確定其子類型別,用子類物件來替換父類物件。

【程式設計實驗】正方形與長形的駁論

 //1、正方形是一種特殊的長方形(is - a關係)?

#include <stdio.h>

//長方形類
class Rectangle
{
protected:
    long width;
    long height;
public:
    void setWidth(long width){this->width = width;}
    long getWidth(){return this->width;}
    
    void setHeight(long height){this->height = height;}
    long getHeight(){return this->height;}
    
    long getArea(){return width * height;}
};

//正方形類(如果繼承自長方形類)
class Square : public Rectangle
{
public:
    void setWidth(long width)
    {
        this->width = width;
        this->height = width;
    }
    
    long getWidth(){return this->width;}
    
    void setHeight(long height)
    {
        this->width = height;
        this->height = height;       
    }
    
    long getHeight(){return this->height;}      
};

int main()
{
    //LSP原則:父類出現的地方必須能用子類替換
    Rectangle* r = new Rectangle();//Square *r = new Square();
    
    r->setWidth(5);
    r->setHeight(4);
    
    printf("Area = %d\n",r->getArea()); //當用子類時,結果是16。使用者就不
                                       //明白為什麼長5,寬4的結果不是20,而是16.
                                       //所以正方形不能代替長方形。即正方形不能
                                       //繼承自長方形的子類
    return 0;
}

 //2. 改進的繼承關係——符合LSP原則

#include <stdio.h>

//抽象的四方形類
class QuadRangle
{   
public:
    //將四方形抽象出公共部分出來
    virtual long getArea() = 0;     //面積
    virtual long getPerimeter() = 0;//周長
};

//長方形類(繼承自抽象的四方形類)
class Rectangle : public QuadRangle
{
private:
    long width;
    long height;
public:
    Rectangle(long width, long heigth)
    {
        this->width = width;
        this->height = heigth;
    }
    
    void setWidth(long width){this->width = width;}
    long getWidth(){return this->width;}
    
    void setHeight(long height){this->height = height;}
    long getHeight(){return this->height;}
    
    long getArea(){return width * height;}
    long getPerimeter(){return (width + height) * 2;}
};

//正方形類(繼承自抽象的四方形類)
class Square : public QuadRangle
{
    long side;
public:
    Square(long side) {this->side = side;}
    
    void setSide(long side);
    long getSide(){return this->side;}
    long getPerimeter(){return 4 * side;}
    long getArea(){return side * side;}
};

int main()
{
    //LSP原則:父類出現的地方必須能用子類替換
    QuadRangle* q = new Rectangle(5, 4); //Rectangle* q = new Rectangle(5, 4);或Square *q = new Square(5);
       
    printf("Area = %d, Perimeter = %d\n",q->getArea(), q->getPerimeter()); 
    
    return 0;
}

2.2 LSP原則的4層含義

(1)子類必須實現父類中宣告的所有方法。

  ①步槍、手槍和機關槍都繼承於AbstractGun,因此都實現了shoot(射擊)的功能。

  ②玩具槍不能直接繼承於AbstractGun。因為玩具槍不能去實現父類的shoot功能(即子類不能完全實現父類的方法,違反LSP原則),否則這樣的武器拿給士兵去殺敵會鬧笑話。因此,ToyGun不能繼承於AbstractGun,而是繼承於AbstracToy,然後去模擬槍的行為。這樣對於士兵類來講,因要求傳入的是AbstactGun類的物件,所以不能使用玩具手槍殺人。

(2)子類可以擴充套件功能,但不能改變父類原有的功能

  ①子類可以有自己的屬性和操作。因此,里氏替換原則只能正著用,不能返過來用。即子類出現的地方,父類未必就可以替換。如Snipper類的killEnemy方法中不能傳入Rifle類的物件,因為Rifle類中沒有zoomOut的方法。

  ②父類向下轉換是不安全的,可能會呼叫到只有在子類中出現的方法而造出異常。

(3)子類可以實現父類的抽象方法,但一般不要覆蓋父類的非抽象方法。

4)如果覆蓋或實現父類方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。方法的後置條件(即方法的返回值)要比父類更嚴格

  ①子類只能使用相等或更寬鬆的前置條件來替換父類的前置條件。當相等時表示覆蓋,不同時表示過載。

  為什麼只能放大?因為父類方法的引數型別相對較小,所以當傳入父類方法的引數型別(或更窄型別)時,過載時將優先匹配父類的方法,而子類的過載方法不會匹配,因此保證了仍執行父類的方法,所以業務邏輯不變(對於C++而言,父子類之間的同名函式發生隱藏而不是過載,因父類的函式被隱藏,當用子類替換父類時,永遠呼叫不到父類的函式,LSP將無法被遵守)。若是覆蓋時,必須清楚其邏輯要義,因為覆蓋時子類的方法會被執行)

  ②只能使用相等或更強的後置條件來替換父類的後置條件。即返回值應該是父類返回值的子類或更小

  如果是過載,由於前置條件的要求,會呼叫到父類的函式,因此子類函式不會被呼叫

  如果是覆蓋,則呼叫子類的函式,這時子類的返回值(S型別)比父類要求的小(T型別),這是被允許的,因為父類呼叫函式的時候,返回值至少是T型別,而子類的返回值S(型別小),給T型別的變數賦值是合法的。

  Father F = ClassF.Func();//;用子類替換時Father F = ClassC.Func()是合法的

【程式設計實驗】前置條件和後置條件

#include <stdio.h>

class Shape
{  
};

class Rectangle : public Shape
{
    
};

class Father
{
public:
    virtual void drawShape(Shape s) //
    {
        printf("Father:drawShape(Shape s)\n");
    }
    
    virtual void showShape(Rectangle r) //
    {
        printf("Father:ShowShape(Rectangle r)\n");       
    }
    
    Shape CreateShape()
    {
        Shape s;
        printf("Father: Shape CreateShape()");
        return s;
    }
};

class Son : public Father
{
public:

    //對於C++而言,過載只能發生在同一作用域。顯示Son和Father是不同作用域
    //所以,下面發生的是隱藏,而不是過載!因此,當使用子類時,不管下列
    //函式中的形參是否比父類更嚴格,只要同名,父類virtual一律被隱藏。

    //子類的形參型別比父類更嚴格
    virtual void drawShape(Rectangle r)  
    {
        printf("Son:drawShape(Rectangle r)\n");        
    } 
    
    //子類的形參型別比父類嚴寬鬆
    virtual void showShape(Shape s)
    {
        printf("Son:showShape(Shape s)\n");        
    }   

    //返回值型別比父類嚴格
    Rectangle CreateShape()
    {
        Rectangle r;
        printf("Son: Rectangle CreateShape()");
        
        return r;
    } 
};

int main()
{
    //當遵循LSP原則時,使用父類地方都可以用子類替換

    //Father* f = new Father(); //該行可用子類替換    
    Son* f = new Son(); //用子類替換父類出現的地方

    Rectangle r;
    
    //子類形參型別更嚴格時,下一行輸出結果會發生變化,不符合LSP原則
    f->drawShape(r); //Father型別的f時,呼叫父類的drawShape(Shape s)
                     //Son型別的f時,發生隱藏,會匹配子類的drawShape
    
    //子類形參型別更寬鬆時,對於C++而言,會因發生隱藏而不符合LSP原則。但Java發生過載,會符合LSP
    f->showShape(r); //Father型別的f時,直接匹配父類的showShape(Rectangle r)
                     //Son型別的f時,因發生隱藏,會匹配子類的showShape(Shape s)
                  
    //子類的返回值型別更嚴格
    Shape s = f->CreateShape(); //替換為子類時,返回值為Rectangle,比Shape型別小,這種賦值是合法的
    
    delete f;
    
    return 0;
}