(點擊此處,接上篇)
The Strategy Pattern via tr1::function(經由 tr1::function 實現的策略模式)
一旦你習慣了 templates(模板)和 implicit interfaces(隱式介面)(參見 Item 41)的應用,function-pointer-based(基於函數指標)的方法看上去就有些死板了。健康值的計算為什麼必須是一個 function(函數),而不能是某種簡單的行為類似 function(函數)的東西(例如,一個 function object(函數對象))?如果它必須是一個 function(函數),為什麼不能是一個 member function(成員函數)?為什麼它必須返回一個 int,而不是某種能夠轉型為 int 的類型?
如果我們用一個 tr1::function 類型的對象代替一個 function pointer(函數指標)(諸如 healthFunc),這些約束就會消失。就像 Item 54 中的解釋,這樣的對象可以持有 any callable entity(任何可調用實體)(例如,function pointer(函數指標),function object(函數對象),或 member function pointer(成員函數指標)),這些實體的標誌性特徵就是相容於它所期待的東西。我們馬上就會看到這樣的設計,這次使用了 tr1::function:
class GameCharacter; // as before
int defaultHealthCalc(const GameCharacter& gc); // as before
class GameCharacter {
public:
// HealthCalcFunc is any callable entity that can be called with
// anything compatible with a GameCharacter and that returns anything
// compatible with an int; see below for details
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
就像你看到的,HealthCalcFunc 是一個 tr1::function instantiation(執行個體化)的 typedef。這意味著它的行為類似一個普通的 function pointer(函數指標)類型。我們近距離看看 HealthCalcFunc 究竟是一個什麼東西的 typedef:
std::tr1::function<int (const GameCharacter&)>
這裡我突出了這個 tr1::function instantiation(執行個體化)的“target signature(目標識別特徵)”。這個 target signature(目標識別特徵)是“取得一個引向 const GameCharacter 的 reference(引用),並返回一個 int 的函數”。這個 tr1::function 類型的(例如,HealthCalcFunc 類型的)對象可以持有相容於這個 target signature(目標識別特徵)的 any callable entity(任何可調用實體)。相容意味著這個實體的參數能夠隱式地轉型為一個 const GameCharacter&,而它的傳回型別能夠隱式地轉型為一個 int。
與我們看到的最近一個設計(在那裡 GameCharacter 持有一個指向一個函數的指標)相比,這個設計幾乎相同。僅有的區別是目前的 GameCharacter 持有一個 tr1::function 對象——指向一個函數的 generalized(泛型化)指標。除了達到“clients(客戶)在指定健康值計算函數時有更大的靈活性”的效果之外,這個變化是如此之小,以至於我寧願對它視而不見:
short calcHealth(const GameCharacter&); // health calculation
// function; note
// non-int return type
struct HealthCalculator { // class for health
int operator()(const GameCharacter&) const // calculation function
{ ... } // objects
};
class GameLevel {
public:
float health(const GameCharacter&) const; // health calculation
... // mem function; note
}; // non-int return type
class EvilBadGuy: public GameCharacter { // as before
...
};
class EyeCandyCharacter: public GameCharacter { // another character
... // type; assume same
}; // constructor as
// EvilBadGuy
EvilBadGuy ebg1(calcHealth); // character using a
// health calculation
// function
EyeCandyCharacter ecc1(HealthCalculator()); // character using a
// health calculation
// function object
GameLevel currentLevel;
...
EvilBadGuy ebg2( // character using a
std::tr1::bind(&GameLevel::health, // health calculation
currentLevel, // member function;
_1) // see below for details
);
就個人感覺而言:我發現 tr1::function 能讓你做的事情是如此讓人驚喜,它令我渾身興奮異常。如果你沒有感到興奮,那可能是因為你正目不轉睛地盯著 ebg2 的定義並對 tr1::bind 的調用會發生什麼迷惑不解。請耐心地聽我解釋。
比方說我們要計算 ebg2 的健康等級,應該使用 GameLevel class(類)中的 health member function(成員函數)。現在,GameLevel::health 是一個被聲明為取得一個參數(一個引向 GameCharacter 的引用)的函數,但是它實際上取得了兩個參數,因為它同時得到一個隱式的 GameLevel 參數——指向 this。然而,GameCharacters 的健康值計算函數只取得單一的參數:將被計算健康值的 GameCharacter。如果我們要使用 GameLevel::health 計算 ebg2 的健康值,我們必須以某種方式“改造”它,以使它適應只取得唯一的參數(一個 GameCharacter),而不是兩個(一個 GameCharacter 和一個 GameLevel)。在本例中,我們總是要使用 currentLevel 作為 GameLevel 對象來計算 ebg2 的健康值,所以每次調用 GameLevel::health 計算 ebg2 的健康值時,我們就要 "bind"(凝固)currentLevel 來作為 GameLevel 的對象來使用。這就是 tr1::bind 的調用所做的事情:它指定 ebg2 的健康值計算函數應該總是使用 currentLevel 作為 GameLevel 對象。
我們跳過一大堆的細節,諸如為什麼 "_1" 意味著“當為了 ebg2 調用 GameLevel::health 時使用 currentLevel 作為 GameLevel 對象”。這樣的細節並沒有什麼啟發性,而且它們將轉移我所關注的基本點:在計算一個角色的健康值時,通過使用 tr1::function 代替一個 function pointer(函數指標),我們將允許客戶使用 any compatible callable entity(任何相容的可調用實體)。很酷是不是?
The "Classic" Strategy Pattern(“經典的”策略模式)
如果你比 C++ 更加深入地進入 design patterns(設計模式),一個 Strategy 的更加習以為常的做法是將 health-calculation function(健康值計算函數)做成一個獨立的 health-calculation hierarchy(健康值計算繼承體系)的 virtual member function(虛擬成員函數)。做成的 hierarchy(繼承體系)設計看起來就像這樣:
如果你不熟悉 UML 記法,這不過是在表示當把 EvilBadGuy 和 EyeCandyCharacter 作為 derived classes(衍生類別)時,GameCharacter 是這個 inheritance hierarchy(繼承體系)的根;HealthCalcFunc 是另一個帶有 derived classes(衍生類別)SlowHealthLoser 和 FastHealthLoser 的 inheritance hierarchy(繼承體系)的根;而每一個 GameCharacter 類型的對象包含一個指向“從 HealthCalcFunc 派生的對象”的指標。
這就是相應的架構代碼:
class GameCharacter; // forward declaration
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
: pHealthCalc(phcf)
{}
int healthValue() const
{ return pHealthCalc->calc(*this);}
...
private:
HealthCalcFunc *pHealthCalc;
};
這個方法的吸引力在於對於熟悉“標準的”Strategy pattern(策略模式)實現的人可以很快地識別出來,再加上它提供了通過在 HealthCalcFunc hierarchy(繼承體系)中增加一個 derived class(衍生類別)而微調已存在的健康值計算演算法的可能性。
Summary(概要)
這個 Item 的基本建議是當你為嘗試解決的問題尋求一個設計時,你應該考慮可選的 virtual functions(虛擬函數)的替代方法。以下是對我們考察過的可選方法的一個簡略的回顧:
- 使用 non-virtual interface idiom (NVI idiom)(非虛擬介面慣用法),這是用 public non-virtual member functions(公有非虛擬成員函數)封裝可存取權限較小的 virtual functions(虛擬函數)的 Template Method design pattern(模板方法模式)的一種形式。
- 用 function pointer data members(函數指標資料成員)代替 virtual functions(虛擬函數),一種 Strategy design pattern(策略模式)的顯而易見的形式。
- 用 tr1::function data members(資料成員)代替 virtual functions(虛擬函數),這樣就允許使用相容於你所需要的東西的 any callable entity(任何可調用實體)。這也是 Strategy design pattern(策略模式)的一種形式。
- 用 virtual functions in another hierarchy(另外一個繼承體系中的虛擬函數)代替 virtual functions in one hierarchy(單獨一個繼承體系中的虛擬函數)。這是 Strategy design pattern(策略模式)的習以為常的實現。
這不是一個可選的 virtual functions(虛擬函數)的替代設計的詳盡無遺的列表,但是它足以使你確信這些是可選的方法。此外,它們之間互為比較的優劣應該使你考慮它們時更為明確。
為了避免陷入 object-oriented design(物件導向設計)的習慣性道路,時不時地給車輪一些有益的顛簸。有很多其它的道路。值得花一些時間去考慮它們。
Things to Remember
- 可選的 virtual functions(虛擬函數)的替代方法包括 NVI 慣用法和 Strategy design pattern(策略模式)的各種變化形式。NVI 慣用法本身是 Template Method design pattern(模板方法模式)的一個執行個體。
- 將一個機能從一個 member function(成員函數)中移到 class(類)之外的某個函數中的一個危害是 non-member function(非成員函數)沒有訪問類的 non-public members(非公有成員)的途徑。
- tr1::function 對象的行為類似 generalized function pointers(泛型化的函數指標)。這樣的對象支援所有相容於一個給定的目標特徵的 callable entities(可調用實體)。