里氏替換原則(LSP)


一、定義

(1)、所有使用基類的地方必須能夠使用子類進行替換,而程序的行為不會發生任何變化(替換為子類之后不會產生錯誤或者異常)。

只有這樣,父類才能真正被復用,子類能夠在父類的基礎上增減新的屬性和行為。才能真正的實現多態行為。

(2)、當子類繼承父類的時候,子類就擁有了父類的屬性和行為。(注意:只是類型而已) 但是如果子類覆蓋父類的某些方法,那么

原來使用父類的地方就可能出現錯誤。(如何理解呢?表面上看是調用的是父類的方法,實際運行的時候子類方法覆蓋了父類的方

法,注意父類方法其實是存在的,通過作用域限定符可以訪問到,兩個方法的實現可能不一樣,這樣不符合LSP里氏替換原則。)

(3)、里氏替換原則是實現開閉原則的重要方式之一。由於使用基類對象的地方可以使用子類對象,因此程序中盡量使用基類類型進

行定義,而在運行的時候確定子類類型,子類對象替換父類對象。 (有點面向接口編程的味道,對外提供接口,而不是實現類)。

或者可以實現公共父類(父類中公共屬性和行為)。

編程實驗:長方形和正方形的駁論

1、正方形是一種特殊的長方形(is-a關系):類圖:

正方形類繼承於長方形類。

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

 

2、改進的繼承關系---符合LSP原則(面向接口編程)
類圖:

 

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

 

3、鴕鳥不是鳥類

 1 //面向對象設計原則:LSP里氏替換原則
 2 //鴕鳥不是鳥的測試程序
 3 
 4 #include <stdio.h>
 5 
 6 //鳥類
 7 class Bird
 8 {
 9 private:
10 double velocity; //速度
11 public:
12 virtual void fly() {printf("I can fly!\n");}
13 virtual void setVelocity(double v){velocity = v;}
14 virtual double getVelocity(){return velocity;}
15 };
16 
17 //鴕鳥類Ostrich
18 class Ostrich : public Bird
19 {
20 public:
21 void fly(){printf("I can\'t fly!");}
22 void setVelocity(double v){Bird::setVelocity(0);}
23 double getVelocity(){return Bird::getVelocity();}
24 };
25 
26 //測試函數
27 void calcFlyTime(Bird& bird) //參數是引用 父類引用子類的時候,會有多態的行為
28 {
29 try
30 {
31 double riverWidth = 3000;
32 
33 if(bird.getVelocity()==0) throw 0;
34 
35 printf("Velocity = %f\n", bird.getVelocity());
36 printf("Fly time = %f\n", riverWidth /bird.getVelocity());
37 }
38 catch(int) //異常處理
39 {
40 printf("An error occured!") ;
41 }
42 }
43 
44 int main()
45 {
46 //遵守LSP原則時,父類對象出現的地方,可用子類替換
47 Bird b; //用子類Ostrich替換Bird
48 
49 b.setVelocity(100); //替換之后,會直接調用子類的方法
50 
51 calcFlyTime(b); //父類測試時是正常的,子類時會拋出異常,違反LSP
52 
53 return 0;
54 }

二、歷史替換原則的4層含義(良好的繼承定義規范,主要包括4層含義)

1、子類必須實現父類中聲明的所有方法。

 

 

 

java里面的接口可以直接定

義接口對象。

(1)、步槍、手槍和機關槍都繼承於AbstractGun接口類,都必須實現shoot(射擊)的功能。

(2)、玩具槍不能直接繼承AbstractGun。因為玩具槍不能實現父類的shoot功能(即子類不能完全實現父類的方法,違反LSP原則)。

按照繼承原則,上面的玩具槍繼承AbstractGun是沒有問題的,玩具槍也是槍,但是在具體的應用場景中就要考慮這個問題了:子類

是否能夠完整的實現父類的業務,否則就會出現拿槍殺敵人時是把玩具槍的笑話。

因此,ToyGun不能繼承於AbstractGun,而是繼承AbstracToy,然后仿真槍的行為。因為士兵類要求傳入的參數AbstractGun類的對

象,所以不能使用玩具槍殺人。

 

 

 

感覺用C++表示這種關系比較牽強。

(3)、如果子類不能完整的實現父類的方法,或者父類的某些方法在子類中已經發生"畸變",則建議斷開父子繼承關系,采用依賴、

聚合、組合等關系代替繼承。

2、子類可以擴展功能,但不能改變父類原有的功能(理解:不能出現方法覆蓋的情況,多態可以)

(1)、子類可以有自己的屬性和方法。因此,里氏替換原則只能正着用,父類出現的地方可以用子類替換,但是不能反過來用。即子

類出現的地方,父類未必可以替換。例如:Snipper類的killEnemy方法中不能傳入Rifle類的對象,因為父類中沒有子類的zoomOut

方法。

 

(2)、父類向下轉換是不安全的,可能會調用只有在子類中出現的方法造成異常。

java里面的接口其實就是C++里面的抽象類,而java里面的抽象類其實就是C++里面的普通的父類(可以有成員變量和方法)。

多繼承的實現:單繼承+多接口

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

注意:父類抽象方法(多態),一般不要覆蓋非抽象方法(子類中公有的父類成分)

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

格。

(1)、子類只能使用相等或者更寬松(表示使用的是父類類型)的前置條件來替換父類的前置條件。相等時表示覆蓋,不同時表示的是重載(java中)。

為什么是放大?因為父類方法的參數類型相對較小,所以當傳入父類方法的參數類型,重載的時候優先匹配父類的方法,而子類的重載方法不會匹

配,因此仍保證執行父類的方法(子類繼承的時候其實操作的是子類中的父類成分),所以業務邏輯不會改變(C++中,父子類的同名函數發生隱藏而不

是重載,因為父類的函數被隱藏,當用子類替換父類時,永遠不會調用父類的函數,LSP將無法遵守)。若是覆蓋時,子類的方法會被執行。

(2)、只能使用相等或更強的后置條件來替換父類的后置條件。即返回值應該是父類方法返回值的子類或更小。

如果是重載,由於前置條件的要求,會調用到父類的函數,因此子函數不會被調用。

如果是覆蓋,則調用子類的函數,這時子類的返回值比父類要求的小。因為父類調用函數的時候,返回值的類型是父類的類型,而子類的返回值更小,

賦值合法。

Father F = ClassF.Func();//;用子類替換時Father F = ClassC.Func()是合法的 子類賦值父類轉是合法的,父類賦值給子類是不合法的

利用設計模式之禪上面的例子更能詳細的說明這點:

實驗:(實驗也是網上的,但是能用設計模式之禪上面的例子更好)

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 //定義兩個空類型用於實驗
 6 class Shape
 7 {
 8 };
 9 
10 class Rectangle : public Shape
11 {
12 
13 };
14 //C++中的抽象類就相當於java中的接口實現
15 //C++中普通的父類(帶有虛函數的,抽象方法)相當於java中的抽象類
16 class Father
17 {
18 public:
19 virtual void drawShape(Shape s) //
20 {
21 printf("Father:drawShape(Shape s)\n");
22 }
23 
24 virtual void showShape(Rectangle r) //
25 {
26 printf("Father:ShowShape(Rectangle r)\n");
27 }
28 
29 Shape CreateShape()
30 {
31 Shape s;
32 printf("Father: Shape CreateShape()");
33 return s;
34 }
35 };
36 
37 class Son : public Father
38 {
39 public:
40 
41 //對於C++而言,重載只能發生在同一作用域。顯示Son和Father是不同作用域
42 //下面發生的是管下列函數中的形參是否比父類更嚴格,只要同名,父類virtual一律被隱藏。
43 
44 //子類的形參類型比父類更嚴格,
45 void drawShape(Rectangle r)
46 {
47 printf("Son:drawShape(Rectangle r)\n");
48 }
49 
50 //子類的形參類型比父類嚴寬松:表示的是父類
51 void showShape(Shape s)
52 {
53 printf("Son:showShape(Shape s)\n");
54 }
55 
56 //返回值類型比父類嚴格
57 Rectangle CreateShape()
58 {
59 Rectangle r;
60 printf("Son: Rectangle CreateShape()");
61 
62 return r;
63 }
64 };
65 
66 int main()
67 {
68 //當遵循LSP原則時,使用父類地方都可以用子類替換
69 
70 //Father* f = new Father(); //該行可用子類替換
71 Son* f = new Son(); //用子類替換父類出現的地方
72 
73 Rectangle r;
74 
75 //子類形參類型更嚴格時,下一行輸出結果會發生變化,不符合LSP原則
76 f->drawShape(r); //Father類型的f時,調用父類的drawShape(Shape s)
77 //Son類型的f時,發生隱藏,會匹配子類的drawShape
78 
79 //子類形參類型更寬松時,對於C++而言,會因發生隱藏而不符合LSP原則。但Java發生重載,會符合LSP
80 f->showShape(r); //Father類型的f時,直接匹配父類的showShape(Rectangle r)
81 //Son類型的f時,因發生隱藏,會匹配子類的showShape(Shape s)
82 
83 //子類的返回值類型更嚴格
84 Shape s = f->CreateShape(); //替換為子類時,返回值為Rectangle,比Shape類型小,這種賦值是合法的
85 
86 delete f;
87 cin.get();
88 return 0;
89 }

 

覺得不錯,轉自https://blog.csdn.net/li_101357/article/details/52902600

 



 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM