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