多態(Polymorphism)在一些編程教程中被弄得很神秘,而在另外一些教程中則被忽略,其實它不過是C++語言所支持的一個簡單而有用的概念。按照C++標准所言,"多態類型(Polymorphic type)”就是帶有虛函數的類類型。從設計的角度來看,"多態對象(Polymorphic object)"就是一個具有不止一種類型的對象,而"多態基類(Polymorphic base class)"則是一個為滿足多態對象的使用需求而設計的基類。
讓我們來看一個金融期權的類型AmOption,如下面的代碼所示:
class Deal
{
};
class Priceable
{
};
class Option: public Deal, public Priceable
{
};
class AmOption: public Option
{
};
class EurOption: public Option
{
};
AmOption對象同時具有4個類型:AmOption, Option, Deal以及Priceable。由於一個類型是一組操作,因此,AmOption對象可以通過其4個接口中的任何一個進行操縱。這意味著從一個AmOption對象可以被針對Deal, Priceable, Option接口編寫的代碼所操縱,從而允許AmOption的實現利用或復用所有那些代碼。對於AmOption這樣的多態類型,從基類繼承的最重要的東西就是它們的接口,而不是它們的實現。事實上,一個基類僅僅由接口組成不但常見,而且通常正是我們所希望的。
當然,這裡有一個需要注意的地方。如果讓這種優勢能夠發揮出來,一個良好設計的多態類對於它的每個基類而言必須是可替換的。換句話說,如果針對Option接口編寫的通用代碼接受的是一個AmOption對象,那麼該對象的行為最好就像一個Option對象!
這並不是說AmOption對象應該和Option對象的行為完全一致(首先可能是因為Option基類的許多操作是不帶任何實現的純虛函數)。實際上,將一個多態基類(如Option)想象成一份契約更好理解一些。這個基類對其接口的用戶做了某些承諾,這些承諾包括鄭重的語法承諾,即特定的成員函數可以通過一些特定類型的實參進行調用,以及不太容易驗證的語義上的承諾,即當一個特定的成員函數被調用時將會發生什麼實際情況。像AmOption和EurOption這樣的具體派生類被稱為"轉包類",它們實現Option與其客戶簽訂的契約。
舉個例子,如果Option具有一個純虛成員函數price,其作用是給出Option的當前值,那麼AmOption和EurOption都必須實現這個函數。我們顯然不會為這兩種類型的Option實現完全一致的行為,但它們都應該計算並返回一個價格(price),而不應該去撥打一個電話或打印一個文件。
另一方面,如果我要去訪問同一個對象的兩種不同接口的price函數,那麼我應該得到相同的結果。就本質而言,每一個調用都應該綁定到同一個函數:
AmOption *d = new AmOption;
Option *b = d;
d->price();// 如果這一個調用的是AmOption::price()
b->price();// 那麼這一個也應該如此
這是有意義的。假如我問你“那個美國期權的當前值是什麼?”,我期望得到以下簡短提問方式相同的答案:“那個期權的當前值是什麼?”
當然,同樣的推理也適用於對象的非虛函數:
b->update(); //如果這一個調用的是Option::update
d->update(); //那麼這一個也是如此!
正是基類提供的契約允許針對基類接口編寫的“多態”代碼對特定的期權起作用,同時有助於對派生類的存在保持“健康的不知情”。換句話說,多態代碼可能正在操縱AmOption和EurOption對象,但除非特別關心它們到底是什麼對象,否則均被視作Option對象。各種各樣“具體的”Option類型可以被添加或刪除而不會影響到只關心基類Option的通用代碼。比如說,如果在某一個地方出現一個AsianOption對象,那麼只知道Option的多態代碼也能夠操作它。
出於同樣的原因,像AmOption和EurOption這樣具體的期權類型只需要知道基類就可以了(它們實現了基類的契約),改變通用代碼對它們毫無影響。原則上,基類可以不知道除自身以外的任何事物。從實踐的角度看,對其接口的設計要考慮預期用戶的需求,並且應該以這樣的方式進行設計:派生類可以很容易地推知並實現其契約。然而,基類應該對其派生類的具體細節全然不知,因為知道這些會不可避免地致使在類層次結構上添加或刪除派生類變得困難。