目錄

 

正文

 

回到頂部

1. 突破思維——不要將思維限定在面向?qū)ο蠓椒ㄉ?/h2>

你正在制作一個視頻游戲,你正在為游戲中的人物設計一個類繼承體系。你的游戲處在農(nóng)耕時代,人類很容易受傷或者說健康度降低。因此你決定為其提供一個成員函數(shù),healthValue,返回一個整型值來表明一個人物的健康度。因為不同的人物會用不同的方式來計算健康度,將healthValue聲明為虛函數(shù)看上去是一個比較明顯的設計方式:

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

1 class GameCharacter {2 public:3 4 virtual int healthValue() const; // return character’s health rating;5 6 ...                                               // derived classes may redefine this7 8 };

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

                   

healthValue沒有被聲明為純虛函數(shù)的事實表明了會有一個默認的算法來計算健康度(Item 34)。

這的確是設計這個類的一個明顯的方式,在某種意義上來說,這也是它的弱點。因為這個設計是如此明顯,你可能不會對其他的設計方法有足夠的考慮。為了讓你逃離面向?qū)ο笤O計之路的車轍,讓我們考慮一些處理這個問題的其它方法。

回到頂部

2. 替換虛函數(shù)的四種設計方法

2.1 通過使用非虛接口(non-virtual interface(NVI))的模板方法模式

一個很有意思的學派認為虛函數(shù)幾乎應該總是private的。這個學派的信徒建議一個更好的設計方法是仍然將healthValue聲明成public成員函數(shù)但是使其變?yōu)榉翘摵瘮?shù),然后讓它調(diào)用一個做實際工作的private虛函數(shù),也就是,doHealthValue:

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 1 class GameCharacter { 2 public: 3 int healthValue() const // derived classes do not redefine 4 { // this — see Item 36 5  6 ...                                       // do “before” stuff — see below 7  8 int retVal = doHealthValue(); // do the real work 9 10 ...                                       // do “after” stuff — see below11 12 return retVal;                  
13 14 15 }16 ...17 private:18 virtual int doHealthValue() const // derived classes may redefine this19 {20 ... // default algorithm for calculating21 } // character’s health22 };

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 

在上面的代碼中(這個條款中剩余的代碼也如此),我在類定義中展示了成員函數(shù)體。正如Item30中所解釋的,將其隱式的聲明為inline。我使用這種方式的目的只是使你更加容易的看到接下來會發(fā)生什么。我所描述的設計和inline之間是獨立的,所以不要認為在類內(nèi)部定義成員函數(shù)是有特定意義的,不是如此。、

客戶通過public非虛成員函數(shù)調(diào)用private虛函數(shù)的基本設計方法被稱作非虛接口(non-virtual interface(NVI))用法。它是更一般的設計模式——模板方法模式(這個設計模式和C++模板沒有任何關系)的一個特定表現(xiàn)。我把非虛函數(shù)(healthValue)叫做虛函數(shù)的一個包裝。

NVI用法的一個優(yōu)點可以從代碼注釋中看出來,也就是“do before stuff”和“do after stuff”。這些注釋指出了在做真正工作的虛函數(shù)之前或之后保證要被執(zhí)行的代碼。這意味著這個包裝函數(shù)在一個虛函數(shù)被調(diào)用之前,確保了合適的上下文的創(chuàng)建,在這個函數(shù)調(diào)用結(jié)束后,確保了上下文被清除。舉個例子,“before”工作可以包括lock a mutex,記錄log,驗證類變量或者檢查函數(shù)先驗條件是否滿足要求,等等?!盿fter”工作可能包含unlocking a mutex,驗證函數(shù)的后驗條件是否滿足要求,重新驗證類變量等等。如果你讓客戶直接調(diào)用虛函數(shù),那么沒有什么好的方法來做到這些。

你可能意識到NVI用法涉及到在派生類中重新定義private虛函數(shù)——重新定義它們不能調(diào)用的函數(shù)!這在設計上并不矛盾。重新定義一個虛函數(shù)指定如何做某事,而調(diào)用一個虛函數(shù)指定何時做某事。這些概念是相互獨立的。NVI用法允許派生類重新定義一個虛函數(shù),這使他們可以對如何實現(xiàn)一個功能進行控制,但是基類保有何時調(diào)用這個函數(shù)的權(quán)利。初次看起來很奇怪,但是C++中的派生類可以重新定義繼承而來的private虛函數(shù)的規(guī)則是非常明智的。

對于NVI用法,虛函數(shù)并沒有嚴格限定必須為private的。在一些類的繼承體系中,一個虛函數(shù)的派生類實現(xiàn)需要能夠觸發(fā)基類中對應的部分,如果使得這種調(diào)用是合法的,虛函數(shù)就必須為protected,而不是private的。有時一個虛函數(shù)甚至必須是public的(例如,多態(tài)基類中的析構(gòu)函數(shù)——Item7),但是這種情況下,NVI用法就不能夠被使用了。

2.2 通過函數(shù)指針實現(xiàn)的策略模式

NVI用法是public虛函數(shù)的一個很有意思的替換者,但是從設計的角度來說,有一點弄虛作假的嫌疑。畢竟,我們?nèi)匀皇褂昧颂摵瘮?shù)計算每個人物的健康度。一個更加引人注目的設計方法是將計算一個人物的健康度同這個人物的類型獨立開來——這種計算不必作為這個人物的一部分。舉個例子,我們可以使用每個人物的構(gòu)造函數(shù)來為健康計算函數(shù)傳遞一個函數(shù)指針,然后在函數(shù)指針所指的函數(shù)中進行實際的運算:

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 1 class GameCharacter;                                                                            // forward declaration 2  3 // function for the default health calculation algorithm                        4  5 int defaultHealthCalc(const GameCharacter& gc);                               
 6  7 class GameCharacter {                                                                         
 8  9 public:                                                                                                 
10 11 typedef int (*HealthCalcFunc)(const GameCharacter&);                     
12 13 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)       
14 15 : healthFunc(hcf )                                                                                
16 17 {}                                                                                                         
18 19 int healthValue() const                                                                        20 21 { return healthFunc(*this); }                                                                
22 23 ...                                                                                                          
24 25 private:                                                                                                
26 27 HealthCalcFunc healthFunc;                                                                
28 29 };

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 

這個方法是另外一種普通設計模式的簡單應用,也就是策略模式。同在GameCharacter繼承體系中基于虛函數(shù)的方法進行對比,它能提供了一些有意思的靈活性: 

  • 相同人物類型的不同實例能夠擁有不同的健康度計算函數(shù)。舉個例子:

    • Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

       1 class EvilBadGuy: public GameCharacter { 2  3 public: 4  5 explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) 6  7 : GameCharacter(hcf ) 8  9 { ... }10 11 ...12 13 };14 15 16 int loseHealthQuickly(const GameCharacter&); // health calculation17 int loseHealthSlowly(const GameCharacter&); // funcs with different18 // behavior19 EvilBadGuy ebg1(loseHealthQuickly); // same-type charac20 EvilBadGuy ebg2(loseHealthSlowly); // ters with different21 // health-related22 // behavior

      Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

       

  • 特定人物的健康度計算函數(shù)能夠在運行時發(fā)生變化。舉個例子,GameCharacter可能提供一個成員函數(shù),setHealthCalculator,它可以對當前的健康度計算函數(shù)進行替換。

此外,健康度計算函數(shù)不再是GameCharacter繼承體系中的成員函數(shù)的事實意味著它不能對正在計算健康度的對象的內(nèi)部數(shù)據(jù)進行特殊訪問。例如,defaultHealthCalc對EvilBadGuy的非public部分沒有訪問權(quán)。如果一個人物的健康度計算僅僅依賴于人物的public接口,這并沒有問題,但是如果精確的健康計算需要非public信息,在任何時候當你用類外的非成員非友元函數(shù)或者另外一個類的非友元函數(shù)來替換類內(nèi)部的某個功能時,這都會是一個潛在的問題。這個問題在此條款接下來的部分會一直存在,因為我們將要考慮的所有其他的設計方法都涉及到對GameCharacter繼承體系外部函數(shù)的使用。

 

作為通用的方法,非成員函數(shù)能夠?qū)︻惖姆莗ublic部分進行訪問的唯一方法就是降低類的封裝性。例如,類可以將非成員函數(shù)聲明為友元函數(shù),或者對隱藏起來的部分提供public訪問函數(shù)。使用函數(shù)指針來替換虛函數(shù)的優(yōu)點是否抵消了可能造成的GameCharacter的封裝性的降低是你在每個設計中要需要確定的。

 

2.3 通過tr1::function實現(xiàn)的策略模式

 

一旦你適應了模板以及它們所使用的隱式(implicit)接口(Item 41),基于函數(shù)指針的方法看起來就非常死板了。為什么健康計算器必須是一個函數(shù)而不能用行為同函數(shù)類似的一些東西來代替(例如,一個函數(shù)對象)?如果它必須是一個函數(shù),為什么不能是一個成員函數(shù)?為什么必須返回一個int類型而不是能夠轉(zhuǎn)換成Int的任意類型呢?

 

如果我們使用tr1::funciton對象來替換函數(shù)指針的使用,這些限制就會消失。正如Item54所解釋的,這些對象可以持有任何可調(diào)用實體(也就是函數(shù)指針,函數(shù)對象,或者成員函數(shù)指針),只要它們的簽名同客戶所需要的相互兼容。這是我們剛剛看到的設計,這次我們使用tr1::function:

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 1 class GameCharacter; // as before 2 int defaultHealthCalc(const GameCharacter& gc); // as before 3 class GameCharacter { 4 public: 5 // HealthCalcFunc is any callable entity that can be called with 6 // anything compatible with a GameCharacter and that returns anything 7 // compatible with an int; see below for details 8 typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc; 9 10 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)11 : healthFunc(hcf )12 {}13 int healthValue() const14 { return healthFunc(*this); }15 ...16 private:17 HealthCalcFunc healthFunc;18 };

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 

正如你所看到的,HealthCalcFunc是對一個實例化tr1::function的typedef。這意味著它的行為像一個泛化函數(shù)指針類型。看看HealthCalcFunc對什么進行了typedef:

1 std::tr1::function<int (const GameCharacter&)>

 

這里我對這個tr1::function實例的“目標簽名”(target signature)做了字體加亮。這個目標簽名是“函數(shù)帶了一個const GameCharacter&參數(shù),并且返回一個int類型”。這個tr1::function類型的對象可以持有任何同這個目標簽名相兼容的可調(diào)用實體。相兼容的意思意味著實體的參數(shù)要么是const GameCharacter&,要么可以轉(zhuǎn)換成這個類型,實體的返回值要么是int,要么可以隱式轉(zhuǎn)換成int。

 

同上一個設計相比我們看到(GameCharacter持有一個函數(shù)指針),這個設計基本上是相同的。唯一的不同是GameCharacter現(xiàn)在持有一個tr1::function對象——一個指向函數(shù)的泛化指針。這個改動是小的,但是結(jié)果是客戶現(xiàn)在在指定健康計算函數(shù)上有了更大的靈活性:

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 1 short calcHealth(const GameCharacter&);     // health calculation 2 // function; note 3 // non-int return type 4  5 struct HealthCalculator {                     // class for health 6  7  8 int operator()(const GameCharacter&) const // calculation function 9 { ... } // objects10 };11 class GameLevel {12 public:13 14 float health(const GameCharacter&) const;    // health calculation15 16 ...                                                                 // mem function; note17 18 };                                                                 // non-int return type19 20  21 22 class EvilBadGuy: public GameCharacter { // as before23 24 ...                                                                
25 26 };27 28 class EyeCandyCharacter: public GameCharacter { // another character29 ... // type; assume same30 31 };                                                               // constructor as32 // EvilBadGuy33 34 EvilBadGuy ebg1(calcHealth);                   // character using a35 // health calculation36 // function37 38 EyeCandyCharacter ecc1(HealthCalculator());          // character using a39 // health calculation40 // function object41 42 GameLevel currentLevel;43 ...44 EvilBadGuy ebg2(                                     // character using a45 46 47 std::tr1::bind(&GameLevel::health, // health calculation48 49 currentLevel,               // member function;50 51 _1)                               // see below for details52 53 54 );

Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

 

你會因為tr1::function的使用而感到吃驚。它一直讓我很興奮。如果你不感到興奮,可能是因為剛開始接觸ebg2的定義,并且想知道對tr1::bind的調(diào)用會發(fā)生什么??聪旅娴慕忉專?/p>

 

我想說為了計算ebg2的健康度,應該使用GameLevel類中的健康成員函數(shù)?,F(xiàn)在,GameLevel::health是一個帶有一個參數(shù)的函數(shù)(指向GameCharacter的引用),但是它實際上有兩個參數(shù),因為它同時還有一個隱含的GameLevel參數(shù)——由this所指向的。然而GameCharacters的健康計算函數(shù)卻只有一個參數(shù):也就是需要計算健康度的GameCharacter。如果我們對ebg2的健康計算使用GameLevel::health,我們必須做一些“適配”工作,以達到只帶一個參數(shù)(GameCharacter)而不是兩個參數(shù)(GameCharacter和GameLevel)的目的。在這個例子中,我們想使用GameLevel對象currentLevel來為ebg2計算健康度,所以我們每次使用”bind”到currentLevel的GameLevel::health函數(shù)來計算ebg2的健康度。這也是調(diào)用tr1::bind所能做到的:它指定了ebg2的健康計算函數(shù)應該總是使用currentLevel作為GameLevel對象。

 

我跳過了tr1::bind調(diào)用的很多細節(jié),因為這樣的細節(jié)不會有很多啟發(fā)意義,并且會分散我要強調(diào)的基本觀點:通過使用tr1::function而不是一個函數(shù)指針,當計算一個人物的健康度時我們可以允許客戶使用任何兼容的可調(diào)用實體。這是不是很酷。

 

2.4 “典型的”策略模式

 如果你對設計模式比上面的C++之酷更有興趣,策略模式的一個更加方便的方法是將健康計算函數(shù)聲明為一個獨立健康計算繼承體系中的虛成員函數(shù)。最后的繼承體系設計會是下面的樣子:

 Android培訓,安卓培訓,手機開發(fā)培訓,移動開發(fā)培訓,云培訓培訓

如果你對UML符號不熟悉,上面的UML圖說明的意思是GameCharacter是繼承體系中的root類,EvilBadGuy和EyeCandyCharacter是派生類;HealthCalcFunc是root類,SlowHealthLoser和FastHealthLoser是派生類;每個GameCharacter類型都包含了一個指向HealthCalcFunc繼承體系對象的指針。

 


作者: HarlanC 

博客地址: http://www.cnblogs.com/harlanc/ 
個人博客: http://www.harlancn.me/ 
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 原文鏈接 

如果覺的博主寫的可以,收到您的贊會是很大的動力,如果您覺的不好,您可以投反對票,但麻煩您留言寫下問題在哪里,這樣才能共同進步。謝謝! 

http://www.cnblogs.com/harlanc/p/6607535.html