原文地址:http://www.codeproject.com/Articles/814367/Interfaces-and-Abstract-Classes
【譯者注】
? ??網(wǎng)上有很多關(guān)于這兩弟兄掐架比高低的文章坪仇,但個人認(rèn)為這篇文章解析非常具體昏苏,而且提到了很多人沒有分析到的點(diǎn)吱抚,特別是從接口的“使用者”堕虹,“開發(fā)者”不同的角度來分析接口與抽象類的差異固耘,實(shí)在精彩筹麸!原文很長耸携,就只能節(jié)選一部分了纵诞。
前提介紹
? ??我經(jīng)趁Ω桑看見有人問關(guān)于接口與抽象類的區(qū)別器予,而且大部分的回答都只是在關(guān)注他們各自的外在特點(diǎn),并沒有對如何使用進(jìn)行進(jìn)一步解釋捐迫。
? ??比如乾翔,大部分的解釋像這樣:
1. 抽象類有具體實(shí)現(xiàn),而接口卻沒有施戴;
2. 在.NET中我們沒辦法進(jìn)行多繼承反浓,但可以同時實(shí)現(xiàn)多個接口;
3. 接口就是一個協(xié)議赞哗,而抽象類的內(nèi)容則遠(yuǎn)遠(yuǎn)不止于此(這句話說對我來說簡直毫無意義雷则,但是卻是非常常見的解釋)
? ??除了這些外,其實(shí)還有很多答案都和這些大同小異肪笋。換句話說月劈,不管這些答案各自到底正確與否,總之是沒有完全答到點(diǎn)上(當(dāng)然有時候問題也問得不夠明確)涂乌。
? ? ? ?那到底我們該什么時候用接口艺栈,什么時候用抽象類?
只在乎傳參調(diào)用 -- 接口是個好東西
? ??假設(shè)你正在開發(fā)一個系統(tǒng)湾盒,而且這個系統(tǒng)目前需要有日志記錄功能湿右。至于這個日志記錄是否與整體業(yè)務(wù)邏輯有關(guān),這個我不會去在意罚勾,我準(zhǔn)備和大家探討的毅人,是這個日志的不同記錄方式:有的寫成文件吭狡,有的寫到數(shù)據(jù)庫里,有的則有其他更多的記錄方式等等丈莺。
? ??這樣一來划煮,你可以很容易想到它大概應(yīng)該是這個樣子:
logger.Log("An error happened in module X.");
logger.ConcatLog("An error happened in module ", moduleName, ".");
logger.FormatLog("An error happened in module {0}.", moduleName);
? ? ? ?如果各位覺得還不夠清楚,我進(jìn)一步說明一下:第一個Log語句只接受了一個String參數(shù)缔俄,第二個Log接收的是一個Object數(shù)組弛秋,第三個Log接受一個String類型的分隔符(即{0})以及一個帶有分隔符的Object數(shù)組。
? ? ? ?那么要寫出這些方法對應(yīng)的接口就容易了:
interface ILogger
{
void Log(string message);
void ConcatLog(params object[] messageParts);
void FormatLog(string format, params object[] parameters);
}
? ??請注意俐载,這個時候你不用在意他們到底是怎么實(shí)現(xiàn)的蟹略,你需要的只是一個能夠這樣完成方法調(diào)用的接口而已。如果你覺得還需要其他的Log執(zhí)行方法遏佣,很簡單挖炬,加到這個接口上就行了。這就是完全體現(xiàn)了“我只管調(diào)用方法状婶,我無需在乎具體實(shí)現(xiàn)”意敛。
抽象類--我能給你提供一個基礎(chǔ)的實(shí)現(xiàn)方案
? ? 在我看來,抽象類是在如下情形下才有價值:當(dāng)你發(fā)現(xiàn)一個接口中帶有一個“幾乎不變”而且會被之后的實(shí)現(xiàn)類“反復(fù)使用”的方法的時候膛虫,你便有必要把這個方法給具體實(shí)現(xiàn)出來草姻,而這樣的一個接口,也就進(jìn)而轉(zhuǎn)變成了一個抽象類走敌。
? ??繼續(xù)以前面的例子為例碴倾,如果將Log日志記錄到數(shù)據(jù)庫逗噩,或是文本或者甚至通過TCP/IP發(fā)送一封信件掉丽,那么我們可以假設(shè)ConcatLog()與FormatLog()方法會實(shí)現(xiàn)成這樣:
public void ConcatLog(params object[] messageParts)
{
string message = string.Concat(messageParts);
Log(message);
}
public void FormatLog(string format, params object[] parameters)
{
string message = string.Format(format, parameters);
Log(message);
}
? ??所以,可以建立一個抽象類來實(shí)現(xiàn)上面兩個方法异雁,但繼續(xù)保持Log()方法為抽象方法捶障。這樣一來,這個抽象類已經(jīng)實(shí)現(xiàn)了3個方法中的2個纲刀,當(dāng)進(jìn)一步開發(fā)FileLogger项炼,DatabaseLogger 和 TcpIpLogger 的時候,開發(fā)人員可以繼承這個抽象類進(jìn)行開發(fā)示绊,避免了重復(fù)的代碼锭部。
干嘛不一開始就使用抽象類?
? ??好了面褐,看到我的這個例子拌禾,我想很多人會問:“為什么不一開始就使用抽象類,免得還采用這么一個無用的接口展哭?”
? ??少年請淡定湃窍,誰敢保證說這個接口是“無用的”闻蛀?
? ??再打個比方,假定我之前的那些方法實(shí)現(xiàn)并不完全正確——它并沒有校驗(yàn)傳入?yún)?shù)的合法性您市,一旦ConcatLog()方法調(diào)用的參數(shù)是NULL觉痛,將會出現(xiàn)一個ArgumentNullException,同樣的問題也會出現(xiàn)在theFormatLog()方法上茵休。如果我們是這個代碼的原開發(fā)者薪棒,我們有權(quán)利來修正代碼,那自然好辦榕莺;但如果這個存在問題的抽象類是來自于一個已經(jīng)編譯好的庫盗尸,而你自己只是一個可憐巴巴的使用者,這時候你怎么辦帽撑?泼各?(全文最警醒之言 ——譯者注)
? ??如果一個類庫的開發(fā)者更多的使用接口,將抽象類分別單獨(dú)存在亏拉,這樣我們便可以方便的重新實(shí)現(xiàn)我們需要的方法扣蜻,比如可以加入我們想要的錯誤返回碼等等。
? ??另外一個例子——NullLogger會如何呢及塘?NullLogger莽使,其實(shí)就是一個不會做任何事情的Log工具。
? ??你可以通過抽象類來進(jìn)一步實(shí)現(xiàn)這個NullLogger笙僚,但是三個方法中有兩個方法是在完全浪費(fèi)時間:調(diào)用方法會免不了執(zhí)行格式化芳肌、拼接參數(shù)等操作,但是這些內(nèi)容是完全不會有任何顯示的——因?yàn)樗荖ullLogger肋层,本來就是空數(shù)據(jù)亿笤。所以NullLogger即使實(shí)現(xiàn)了所有的方法,其實(shí)都是毫無作為栋猖,浪費(fèi)時間净薛。像這樣一種“無為”的做法非常普遍,主要用來避免對NULL的檢查蒲拉,甚至目前都已經(jīng)產(chǎn)生了一種設(shè)計模式:Null Object Pattern (空對象模式)肃拜。
? ??最后,依然假定我們是這個代碼的開發(fā)者雌团,我們提供給其他程序員使用燃领,那么我們還很難知道其他程序員對于這些方法的統(tǒng)計需求到底是怎樣的。假如有人覺得這些Logger不能僅僅用來輸出日志锦援,還要同時統(tǒng)計每個log方法的調(diào)用次數(shù)猛蔽。如果我們一開始使用的是抽象類的設(shè)計方案,那么可以看到具體的實(shí)現(xiàn)中總是導(dǎo)向Log()方法雨涛,我們在進(jìn)一步的擴(kuò)展里枢舶,只能去寫代碼統(tǒng)計Log()方法的調(diào)用次數(shù)懦胞,而其實(shí)很顯然其他方法也肯定有被調(diào)用,但沒法統(tǒng)計了凉泄。如果換做是完全重新實(shí)現(xiàn)所有接口躏尉,我們就可以避免這種問題,因?yàn)槊總€方法的實(shí)現(xiàn)后众,此刻由我們自己做主胀糜。
? ??所以,當(dāng)我們自己作為一個組件/類庫的開發(fā)提供者的時候蒂誉,我們要盡可能的讓組件內(nèi)容完全正確教藻,并且要允許用戶能夠重新重寫任何他們需要的內(nèi)容——有的時候你也免不了犯錯,抑或他們確實(shí)有什么特殊的需求需要特殊處理右锨。