當冒號邁著不變的步伐出現在教室時,手上有了一點變化:左手仍拎著筆記本包,右手卻多了一樣東西。大家定睛一看,原來是個電腦主板,不由得暗自納悶:難道軟件課改成了硬件課?
冒號照例直入主題:“上節課我們對繼承的利弊作了詳細的分析,其中最重要的觀點是:繼承的主要用途不是代碼重用,而是代碼被重用。這依賴于兩個前提,一個是在語義上遵循里氏代換原則,另一個是在語法上支持多態(polymorphism)機制。因此不妨說,對于靜態類型語言來說,繼承是多態的基礎,多態是繼承的目的。”
問號忍不住問:“為什么要強調靜態類型呢?”
“還記得鴨子類型[1]嗎?那就是一種不依賴于繼承的多態類型,也是動態類型語言一大優劣參半的特色。”冒號提醒道,“靜態類型語言中的多態是動靜結合的產物,將靜態類型的安全性和動態類型的靈活性融為一體。它一般有兩種實現方式:一種利用GP(泛型編程)中的參數多態(parametric polymorphism),一種利用OOP中的包含多態(inclusion polymorphism)或稱子類型多態(subtyping polymorphism)。從實現機制上看,二者的不同之處在于何時將一個變量與其實際類型所定義的行為掛鉤。前者在編譯期,屬于早綁定 (early binding)或靜態綁定(static binding)[2];后者在運行期,屬于遲綁定 (late binding)或動態綁定(dynamic binding)。從應用形式上看,前者是發散式的,讓相同的實現代碼應用于不同的場合;后者是收斂式的,讓不同的實現代碼應用于相同的場合。從思維方式上看,前者是泛型式編程風格,看重的是算法的普適性;后者是對象式編程風格,看重的是接口與實現的分離度。盡管二者從范式到語法、語義都大相徑庭,但都是為著同一個目的:在保證必要的類型安全的前提下,突破編譯期間過于嚴苛的類型限制。對于既是靜態類型語言又是靜態語言、既支持OOP又支持GP的C++、Java和C#而言,多態機制是保證代碼的靈活性、可維護性和可重用性的終極武器。為了說明問題,我們看一個簡單而實用的例子:編寫一個類,讓它能儲存用戶名和密碼,以作今后驗證之用。”
嘆號一愣:“這題是不是太簡單了?還有別的要求嗎?”
冒號搖搖頭。
引號卻認為:“要求太少反而不好做。比如是把數據放在內存、還是文件或者數據庫?密碼以明文還是密文的形式存儲?”
句號提出:“無論是數據的存放方式還是密碼的加密方式,都不應該硬編碼。”
“循此思路,我們就來編寫一個可重用的抽象類。”冒號投放了一段Java代碼——
/** 一個可以驗證用戶名和密碼的類 */
abstract class Authenticator
{
/** 保存用戶名和密碼 */
final public void save(String user, String password)
{
if (password == null)
password = "";
store(user, encrypt(password));
}
/** 驗證用戶名和密碼 */
final public boolean authenticate(String user, String password)
{
String storedPassword = retrieve(user);
if (storedPassword == null) return false; // 無此用戶
if (password == null)
password = "";
return storedPassword.equals(encrypt(password));
}
/** 保存用戶名和加密過的密碼 */
protected abstract void store(String user, String encryptedPassword);
/** 從用戶名獲取相應的加密過的密碼 */
protected abstract String retrieve(String user);
/** 給明文單向(one-way)加密,默認不加密 */
protected String encrypt(String text) { return text; }
}
冒號解說道:“該抽象類有兩個public接口,一個用來保存,一個用來驗證。它們用final修飾符來禁止子類覆蓋,因為真正的擴展點是三個protected方法。其中store和retrieve是抽象的,encrypt有一個平凡實現。以此為基礎,再根據實際需要來編寫子類,具體實現這三個方法。”
幻燈片轉到下一頁——
import java.util.Map;
import java.util.HashMap;
/** 一個簡單的驗證類,數據放在內存,密碼保持明文 */
class SimpleAuthenticator extends Authenticator
{
private Map<String, String> usrPwd = new HashMap<String, String>();
@Override protected void store(String user, String encryptedPassword)
{
usrPwd.put(user, encryptedPassword);
}
@Override protected String retrieve(String user)
{
return usrPwd.get(user);
}
}
“我們利用HashMap來儲存數據,密碼保持明文。這大概是最簡單的一種子類了。”冒號仿佛在輕輕地把玩著一件小物什,“為安全起見,最好還是將密碼加密。于是我們設計了稍微復雜一點的子類——”
import java.security.MessageDigest;
/** 一個安全的驗證類,數據放在內存,密碼經過SHA-1加密 */
class Sha1Authenticator extends SimpleAuthenticator
{
private static final String ALGORITHM = "SHA-1"; // SHA-1算法
private static final String CHARSET = "UTF-8"; // 避免依賴平臺
@Override protected String encrypt(String plainText)
{
try
{
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(plainText.getBytes(CHARSET));
byte digest[] = md.digest();
// BASE64編碼比十六進制編碼節省空間
//為簡便起見用到了非標準的API,因此以下代碼有警告
return (new sun.misc.BASE64Encoder()).encode(digest);
}
catch (java.security.NoSuchAlgorithmException e)
{
throw new InternalError(e.getMessage()); // 不可能發生
}
catch (java.io.UnsupportedEncodingException e)
{
throw new InternalError(e.getMessage()); // 不可能發生
}
}
}
逗號質疑道:“不是具體類不宜被繼承的嗎?怎么Sha1Authenticator類卻繼承了具體類SimpleAuthenticator?”
冒號略表贊許:“很高興你沒有忘記這個原則。不過考慮到Sha1Authenticator類需要覆蓋父類的encrypt方法,這么做也是情有可原的。當然最好選擇讓該類直接繼承抽象類Authenticator,但作為示例代碼,我們還是希望它簡潔一些,不想讓過多的細枝末節掩蓋核心主干。下面是測試代碼——”
public class TestAuthenticator
{ // 為避免額外依賴,沒有采用JUnit等單元測試工具
public static void main(String[] args)
{
test(new SimpleAuthenticator());
test(new Sha1Authenticator());
}
// 測試給定的Authenticator
private static void test(Authenticator authenticator) // 子類型多態
{
test(authenticator, "user", "password");
test(authenticator, "user", "newPassword");
test(authenticator, "admin", "admin");
test(authenticator, "guest", null);
test(authenticator, null, "pass");
authenticator.save("scott", "tiger");
assert(!authenticator.authenticate("scott", "TIGER")); // 大小寫敏感
assert(!authenticator.authenticate("SCOTT", "tiger")); // 大小寫敏感
}
private static void test(Authenticator authenticator, String user, String password)
{
authenticator.save(user, password);
assert(authenticator.authenticate(user, password));
}
}
引號覺得眼熟:“這不是上節課講的模板方法模式嗎?”
“正是此公。”冒號確認,“該模式的核心思想是:固定整體框架和流程以保證可重用性,留出一些子類定制點以保證可擴展性。在測試代碼的兩個test方法中,傳入的參數是Authenticator類,但數據存放和密碼加密的方式是在運行中才確定的,即先后遵照SimpleAuthenticator類和Sha1Authenticator類的實現。這就是我們所說的子類型多態的效果——讓不同的實現代碼應用于相同的場合。假設沒有多態機制,這種效果就只能靠if/else或switch之類的條件語句才能實現,非常地痛苦。”
冒號的眉頭皺成了粗體的“川”字。
“還有更好的方法嗎?”句號察言觀色,斷定老冒還留有后手。
果不其然,冒號的眉毛立刻又舒展開來,中氣充沛地應道:“有!諸位請看——”
// 鍵值對的存取接口
interface KeyValueKeeper
{
public void store(String key, String value);
public String retrieve(String key);
}
// 加密接口
interface Encrypter
{
public String encrypt(String plainText);
}
class Authenticator
{
private KeyValueKeeper keeper;
private Encrypter encrypter;
public Authenticator(KeyValueKeeper keeper, Encrypter encrypter)
{
this.keeper = keeper;
this.encrypter = encrypter;
}
public void save(String user, String password)
{
if (password == null)
password = "";
keeper.store(user, encrypter.encrypt(password));
}
public boolean authenticate(String user, String password)
{
String storedPassword = keeper.retrieve(user);
if (storedPassword == null) return false;
if (password == null)
password = "";
return storedPassword.equals(encrypter.encrypt(password));
}
}
冒號加以引導:“如果仔細比較兩種設計,就會發現它們很相似。后者只不過把前者對子類開放的接口合成為自己的兩個成員。再看接口的實現類——”
class MemoryKeeper implements KeyValueKeeper
{
private Map<String, String> keyValue = new HashMap<String, String>();
@Override public void store(String key, String value)
{
keyValue.put(key, value);
}
@Override public String retrieve(String key)
{
return keyValue.get(key);
}
}
class PlainEncrypter implements Encrypter
{
@Override public String encrypt(String plainText)
{
return plainText;
}
}
class Sha1Encrypter implements Encrypter
{
private static final String ALGORITHM = "SHA-1";
private static final String CHARSET = "UTF-8";
@Override public String encrypt(String plainText)
{
try
{
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(plainText.getBytes(CHARSET));
byte digest[] = md.digest();
return (new sun.misc.BASE64Encoder()).encode(digest);
}
catch (java.security.NoSuchAlgorithmException e)
{
throw new InternalError(e.getMessage());
}
catch (java.io.UnsupportedEncodingException e)
{
throw new InternalError(e.getMessage());
}
}
}
逗號比較后得出結論:“MemoryKeeper與SimpleAuthenticator、Sha1Encrypter與Sha1Authenticator除了超類型和方法訪問修飾符外,其他毫無二致。”
屏幕滾動出另一段代碼——
public class TestAuthenticator
{
public static void main(String[] args)
{
test(new Authenticator(new MemoryKeeper(), new PlainEncrypter()));
test(new Authenticator(new MemoryKeeper(), new Sha1Encrypter()));
}
private static void test(Authenticator authenticator) // 隱含子類型多態
{ /* 同上,略 */}
}
“測試代碼區別也不大,只是Authenticator的多態性更加隱蔽。”冒號如是說。
嘆號挑剔說:“后一種創建實例稍顯麻煩一些。”
“但它是以小弊換大利。”冒號朗聲而道,“首先,后者用的是合成與接口繼承,比前者的實現繼承更值得推薦,理由在上堂課業已闡明。其次,假設共有M種數據存取方式,包括內存、文件、數據庫等等;共有N種加密方式,包括明文、SHA-1、SHA-256、MD5等等。按第一種設計,需要(M×N)個實現類;按第二種設計,只要(M+N)個實現類。這還只是兩種變化因素,假如需要考慮更多的因素,二者差距將更大。比如增加編碼方式:加密后的數據可以選擇費空間省時間的十六進制編碼、費時間省空間的BASE64編碼、省時間省空間卻包含非打印字符的原始形式等;比如增加安全強度:引入salt、nonce或IV等[3];比如增加密碼狀態:已生效密碼、未生效密碼、已過期密碼等等。對比下面的UML類圖,孰優孰劣更加一目了然。”
眾人眼前出現了兩幅圖——
冒號指著屏幕問:“圖二不僅比圖一少了三個實現類,而且可重用性也更高。大家說是為什么?”
引號應答:“圖一中的九個Authenticator的子類只能作為驗證類來重用,而圖二中六個實現類不僅可以合作完成驗證類的功能,還能分別單獨提供鍵值存儲和加密字符串的功能。”
冒號作出肯定:“這就是職責分離的好處。存儲與加密本是兩樣不相干的工作,必要時可以合作,但平時最好分開管理,符合‘低耦合、高內聚’的原則。”
問號注意到圖中的注釋,遂問:“第二種采用的是策略模式?”
冒號頷首:“簡單地說,策略模式(strategy pattern或policy pattern)的基本思想是:把一個模塊所依賴的某類算法委交其他模塊實現。比如Java中的Comparable和Comparator、C#中的IComparer就是比較算法的接口,當一個類的某個方法接收了此種類型的參數,實質上就采用了策略模式。”
逗號不以為奇:“這豈非很平常?”
“你認為設計模式真的高不可攀嗎?”冒號反問道,“包括模板方法模式,你們很可能也在編程實踐中采用過,只不過相交不相識罷了。”
句號看出:“模板方法模式與策略模式非常神似,都是把一個類的可變部分移交給其他類處理。”
“照你這么說,絕大多數設計模式都是神似的,這也是為什么我們不專門談設計模式的緣故。GoF設計模式是OOP大樹上結出的碩果,在你心中培養的OOP成熟之前,匆忙締結的果實多半是青澀弱小的。”冒號忠告,“我們也不會對設計模式避而不談,但凡提及都是水到渠成的產物。再說回這兩種設計模式,雖然有相通的思想,也能解決相同的問題,在穩定性與靈活性之間都取得了某種平衡,但還是各有側重的。模板方法模式突出的是穩定堅固的骨架,策略模式突出的是靈活多變的手腕。不妨拿國家政策作比:一個強調對內要穩,老一輩制訂了大政方針,下一代必須在堅持原則的前提下進行完善;一個強調對外要活,不能或不便自行開發的技術不妨從國外引進。”
嘆號一樂:“哈!設計模式上升到了政策模式。”
冒號抽絲剝繭:“正如模板方法模式可看作控制反轉的特例,策略模式與依賴注射(Dependency Injection)也異曲同工。第二個Authenticator所依賴的兩個功能KeyValueKeeper和Encrypter,就是是通過構造方法‘注射’進來的[4]。當然策略只是一種特殊的依賴,是自內而外的——將算法抽出來外包;依賴注射的機制更復雜、涵蓋面更廣,是自外而內的——從外部嵌入定制功能。后者被廣泛地用于框架應用之中,尤以Spring Framework和Google Guice為代表。”
引號聽得起勁:“這下熱鬧了,設計模式、框架與OOP范式全攪和到一塊了。”
“還有GP范式呢。”冒號順接話題,“讓我們再用C++的模板來實現一下Authenticator類吧。沒有繼續采用Java,是因為它的泛型仍離不開子類型多態。”
說著,他換上了C++代碼——
#include <string>
#include <map>
using namespace std;
template <typename KeyValueKeeper, typename Encrypter>
class Authenticator
{
private:
KeyValueKeeper keeper;
Encrypter encrypter;
public:
void save(const string& user, const string& password)
{
keeper.store(user, encrypter.encrypt(password));
}
bool authenticate(const string& user, const string& password) const
{
string storedPassword;
if (!keeper.retrieve(user, storedPassword)) return false;
return storedPassword == encrypter.encrypt(password);
}
};
class MemoryKeeper
{
private:
map<string, string> keyValue;
public:
void store(const string& key, const string& value)
{
keyValue[key] = value;
}
bool retrieve(const string& key, string& value) const
{
map<string, string>::const_iterator itr = keyValue.find(key);
if (itr == keyValue.end()) return false;
value = itr->second;
return true;
}
};
class PlainEncrypter
{
public:
string encrypt(const string& plainText) const { return plainText; }
};
class Sha1Encrypter
{
public:
string encrypt(const string& plainText) const { /* 省略代碼 */ }
};
namespace
{
template <typename K, typename E>
void test(Authenticator<K, E> authenticator) // 參數多態
{ /* 省略代碼 */ }
}
int main()
{
test(Authenticator<MemoryKeeper, PlainEncrypter>());
test(Authenticator<MemoryKeeper, Sha1Encrypter>());
return 0;
}
“以上代碼與Java版的策略模式代碼很相似,主要的區別是把KeyValueKeeper和Encrypter兩個接口換成了模板參數。由于模板是在編譯期間實例化的,因此沒有動態綁定的運行開銷,但缺點是不能動態改變策略[5]。”冒號分析道,“至此,我們通過一個驗證類的三種解法,分別展示了三種形式的多態:基于類繼承的多態、基于接口繼承的多態和基于模板的多態。它們殊途同歸,都能讓代碼更簡潔、更靈活、可重用性更高、更易維護和擴展。”
問號想到一個問題:“C語言既沒有子類型多態也沒有參數多態,又如何保證高質量的C程序呢?”
冒號眉梢輕挑:“C語言有指針啊,C++、Java和C#的多態在底層就是用指針實現的。C中的函數指針比Java中的接口更加靈活高效,當然對程序員的要求也更高。”
引號驀地記起:“重載不也是一種多態嗎?”
“剛才所說的多態都屬于通用多態(universal polymorphism)。此外,還有一類特別多態(ad-hoc polymorphism),常見有兩種形式。一種是強制多態(coercion polymorphism),即一種類型的變量在作為參數傳遞時隱式轉換成另一種類型,比如一個整型變量可以匹配浮點型變量的函數參數。另一種就是重載多態(overloading polymorphism),它允許不同的函數或方法擁有相同的名字。特別多態淺顯易懂,其重要性與通用多態也不可同日而語,故不在我們關注之列。只是要注意一點,正如子類型應遵守超類型的規范,同名的函數或方法也應遵守相同的規范。如果為貪圖取名方便而濫用重載,早晚因小失大。”冒號告誡道。
逗號突發奇論:“一個多態類型的對象可以在不同的類型之間變來變去,是不是叫‘變態類型’更生動些?”
“我看你就屬于典型的變態類型。”句號乘機拿他開涮。
全班哈哈大笑。