再談抽象類和接口
2009-08-19 12:15 Jeffrey Zhao 閱讀(9556) 評論(69) 收藏 舉報昨天我質(zhì)疑了為什么定義RouteBase抽象類,而不是IRoute接口,我談到對于一個“沒有任何實(shí)現(xiàn)”的抽象類來說,開發(fā)人員應(yīng)該使用接口。不過在后面的評論中,有朋友給了我啟發(fā),讓我忽然想到更多的事情。晚上又再次翻了翻《Framework Design Guidelines》之后,打算再談一些東西,把這個問題討論地更加清楚一些。
上次談了接口的好處,那么這次就主要談一下接口的缺點(diǎn)。
API是會“升級”的。在一個類庫的新版本中,往往會對舊有的API進(jìn)行修改。作為一個公開的的API,應(yīng)該做到“無痛升級”,也就是說API的演變不應(yīng)該破壞現(xiàn)有的應(yīng)用。而類(class)相對于接口(interface)的優(yōu)勢之一,便是在于類適合進(jìn)行API的演變。
試想,在類庫的1.0版本中定義了一個公開的接口,就比如IRoute吧,它其中有兩個成員。于是乎開發(fā)人員在使用1.0版本的時候,就會實(shí)現(xiàn)這個IRoute接口:
public class MyRoute : IRoute { public RouteData GetRouteData(HttpContextBase httpContext) { ... } public VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values) { ... } }
于是到了2.0版本中,開發(fā)人員發(fā)現(xiàn),IRoute接口不夠用,需要加入新的成員——但是不行,因?yàn)镮Route在1.0中已經(jīng)公開了,這意味著如果向IRoute中添加新的成員,就可能會破壞外部已經(jīng)使用IRoute接口的實(shí)現(xiàn)(如上面的MyRoute)。因此,一旦公開接口發(fā)布之后,它就不能被修改了。進(jìn)而,這一點(diǎn)又可以引出了接口的一個設(shè)計準(zhǔn)則:接口的職責(zé)應(yīng)該尤其單一。例如.NET框架中的IComparable,IEnumerable等接口。因?yàn)槿绻O(shè)計了一個接口,其中包含了許多成員,那么在新版本中這個接口則更有可能需要補(bǔ)充新的功能……但是接口又不可以修改,這該怎么辦呢?
的確是進(jìn)退兩難的情況。在《Framework Design Guildlines》里提出了一些沒辦法時的辦法,但書中也不得坦誠道,這些做法都不是好辦法。事實(shí)上真沒有什么好辦法。
如果是“類”的話,問題就相對好辦多了,因?yàn)槲覀兛梢韵蝾愔刑砑有碌某蓡T——只要這個新成員不是abstract的,就不會破壞外部已經(jīng)出現(xiàn)的依賴。不過加上之后,API設(shè)計是否合理,語義是否清晰,就是另一回事情了。API設(shè)計不僅僅是技術(shù)活,每個舉動都是需要推敲的。因?yàn)橐坏┌l(fā)布,就沒法刪除了。
因此在這里再次感謝那位匿名朋友在評論中的提醒,他指出RouteBase可能是為了“預(yù)留”而實(shí)現(xiàn)成抽象類的。
說起語義和協(xié)議,可能就會涉及到接口的另一個特點(diǎn),或者說也是個“缺陷”,那就是協(xié)議并不明確。接口定義了成員,也就是限制了實(shí)現(xiàn)這個接口的“外觀”,但是對于“內(nèi)在”是做不了任何限制的。例如,我們可以讓一個方法拋出NotImplementedException,這便是“外強(qiáng)中干”的典型。還有一種情況,就比如IList<T>接口:
public interface IList<T> { void Add(T item); int Count { get; } ... }
IList<T>接口只是限制了接口的外部表現(xiàn):一個接受T類型的Add方法,還有表示元素數(shù)量的Count。但是在協(xié)議層面上,我們是無法限制這兩個成員的關(guān)系的。例如,Add方法調(diào)用過后,Count肯定會增加1嗎?此外還有,Add方法是不是線程安全的?僅通過接口都是不得而知的。
如果是抽象類,我們可以實(shí)現(xiàn)的東西就多了。例如,我們可以實(shí)現(xiàn)這樣的一個抽象類:
public abstract class ThreadSafeListBase<T> { private ReaderWriterLockSlim m_rwLock = new ReaderWriterLockSlim(); public int Count { get; private set; } public void Add(T item) { this.m_rwLock.EnterWriteLock(); try { this.Count++; this.AddCore(item); } finally { this.m_rwLock.ExitWriteLock(); } } protected abstract void AddCore(T item); ... }
這么做,就在一定程度上對實(shí)現(xiàn)內(nèi)容進(jìn)行了約束。但是很明顯約束是不可能徹底的(如AddCore也可能拋出NotImplementedException),最徹底的約束便是一個sealed class——但這個很明顯,就已經(jīng)不是抽象了。
接口的確有優(yōu)勢(可以讓類來實(shí)現(xiàn)多個接口,且struct實(shí)現(xiàn)接口不能繼承一個類),但是在這篇文章中我們也看到接口也是有一定缺陷的。設(shè)計需要平衡,沒有什么東西是最好的,也沒有什么東西總是最合適的。
不過比較奇怪的是,有(不止一個)朋友回復(fù)說,使用抽象類是為了使用擴(kuò)展方法。我從來沒有看到過某個資料說接口和擴(kuò)展方法有任何沖突,事實(shí)上我們也可以為接口定義擴(kuò)展方法。由于一個類可以實(shí)現(xiàn)多個接口,而接口又可以實(shí)現(xiàn)擴(kuò)展方法,這似乎也又有了“多繼承”的意味。
浙公網(wǎng)安備 33010602011771號