有關(guān)virtual,override與擴(kuò)展點的討論
Virtual即虛擬。在C#中,Virtual指的是虛方法或虛函數(shù)。有關(guān)這個Virtual有一個經(jīng)常被討論的問題:所有的成員都應(yīng)該是virtual的嗎?
這是一個由來已久的討論,由于Java默認(rèn)所有的方法都是可以被override的(除非手動寫成final),因此從C#語言設(shè)計起初就有此番爭論,甚至讓Anders都出來解釋了一下。最近又有人在討論這方面話題了,雖然我的看法并沒有超出這些人所涉及的范疇,但是我還是打算談一下我的理解。退幾步說,就當(dāng)補(bǔ)充一些“實例”吧。
Virtual,Override與擴(kuò)展點的關(guān)系簡述
此次的話題是由Ward Bell引起的,他在review了Roy Osherove的新書《The Art of Unit Testing》之后認(rèn)為,他不同意Roy給出的建議“將所有的成員默認(rèn)為virtual”,為此他還獨立開篇解釋了他的觀點。這篇文章引起的討論較為熱烈,我也打算在詳細(xì)總結(jié)一番。與Ward觀點對應(yīng)的是,著名的Jeremy Miller希望.NET中所有的成員默認(rèn)就是virtual的,而“月寫博客80篇”的Oren Eini甚至認(rèn)為所有的成員都應(yīng)該標(biāo)為virtual。
繼承一個類并override掉其中的成員,是面向?qū)ο缶幊讨凶畛S玫姆绞街?。這是一種擴(kuò)展方式,而能夠被override的方法便是“擴(kuò)展點”。所以我認(rèn)為,是否把成員標(biāo)記為virtual,其實涉及到的概念便是“是否把它開放為一個擴(kuò)展點”。Oren認(rèn)為“所有成員都應(yīng)該virtual”則意味著“任何成員都是可擴(kuò)展點”,而對于“默認(rèn)為virtual”的觀點來說,則意味著“傾向于打開更多的擴(kuò)展點”——其實除了Oren有些極端外,“傾向性”代表的更多是一種“口味”,因為無論是Java還是.NET,都可以標(biāo)記一個成員能否被override。
從Virtual,Override與擴(kuò)展點延伸談去
我不想討論“口味”問題,不過我的觀點與Ward類似,即使在C#出現(xiàn)之前,我也一直不太喜歡Java的這個特性(不過當(dāng)時相關(guān)體會比較少,所以感覺并不強(qiáng)烈)。Oren認(rèn)為打開更多擴(kuò)展點,有助于從各方面進(jìn)行擴(kuò)展,他說他的這個做法也過于也得到了較多的“實惠”。不過我認(rèn)為,這是由于Oren的能力過于厲害,并且知道該做什么不該做什么,并且可以對自己作的事情所負(fù)責(zé)決定的。
對于一個可“全面擴(kuò)展”的類型來說,意味著開發(fā)人員有更多的自由,進(jìn)而意味著選擇(即使是做同一件事情)。但是選擇多,則同樣意味著我們需要了解的多,一個不慎可能就會發(fā)現(xiàn)沒有得到預(yù)期的效果。例如,在繼承了ASP.NET的Control類之后,您要改變它輸出的內(nèi)容,您會選擇覆蓋哪一個方法?
- protected internal virtual void Render(HtmlTextWriter writer)
- {
- this.RenderChildren(writer);
- }
- protected internal virtual void RenderChildren(HtmlTextWriter writer)
- {
- ...
- }
您可能會說,覆蓋哪個都可以。但是,它們其實都有不同的語義,只是因為在Control基類中Render自身就是Render所有的子控件。但是到了子類中,Render自身可能就會涉及到邊框等其他內(nèi)容了。如果您隨便選一個,您的類型看上去沒有問題,但是如果別人希望繼承你寫的類,補(bǔ)充一些實現(xiàn),那么你的“選擇”就會影響到他的結(jié)果了。當(dāng)然我并不是說Control類設(shè)計的不對,它的設(shè)計我覺得是正確的,我只是想說明,如果每個成員都可擴(kuò)展,那么用戶在真正需要擴(kuò)展的時候就會比較麻煩了。
架構(gòu)是一種擴(kuò)展,也是一種約束,限制了別人可以怎么做,也必須怎么做。我們雖然無法避免別人的惡意行為,但是良好的擴(kuò)展點也可以給別人更好的指導(dǎo)。
再舉一個例子。在.NET中,最容易擴(kuò)展的抽象元素是什么呢?應(yīng)該是“接口”。接口中的所有成員都是由實現(xiàn)方提供的,除了成員的簽名之外,接口并沒有作任何限制。正如我在之前寫過的一篇文章里提到,別人完全可以實現(xiàn)出外強(qiáng)中干的對象:
- public interface IList<T>
- {
- void Add(T item);
- int Count { get; }
- ...
- }
根據(jù)接口中隱含的協(xié)議,Add方法調(diào)用之后,Count必須加一。但是這個協(xié)議并無法加諸于實現(xiàn)之上。如果要提供這方面的約束,我們只能公開一部分的擴(kuò)展點,而不是把所有的職責(zé)交給實現(xiàn)方:
- public abstract class ListBase<T>
- {
- public int Count { get; private set; }
- public void Add(T item)
- {
- this.Count++;
- this.AddCore(item);
- }
- protected abstract void AddCore(T item);
- ...
- }
Ward也舉了一個“電梯”的例子。電梯有一個Up方法,調(diào)用它則意味著電梯上升。但是如果把Up這個關(guān)鍵行為擴(kuò)展出去,那么別人在“修改電路”(即override這個Up方法)的時候,可能就會把電梯搞亂套了,例如原本應(yīng)該先關(guān)門再啟動,現(xiàn)在可能先啟動再關(guān)門,甚至一旦Up電梯就下降了。Oren認(rèn)為擴(kuò)展方應(yīng)該為自己的擴(kuò)展負(fù)責(zé),但是我還是認(rèn)為,擴(kuò)展點應(yīng)該和成員訪問級別等東西一樣,只給出必要的,控制住關(guān)鍵的。
最后的例子也是常見的:
- public class SomeClass
- {
- public void SomeMethed()
- {
- this.SomeMethed(String.Empty);
- }
- public void SomeMethed(string s)
- {
- this.SomeMethod(s, 0);
- }
- public virtual void SomeMethod(string s, int i)
- {
- // ...
- }
- }
為了方便起見,我們常常會對類型中的方法給出重載,其中大部分的重載最終都委托給一個唯一的核心方法。當(dāng)用戶繼承SomeClass類之后,他便擁有了一個唯一的擴(kuò)展點,這樣便可以確保這個類的行為按照一定的“準(zhǔn)則”在正常開展。否則的話,用戶就需要在三個方法中進(jìn)行選擇性的override,并且要平衡三者的行為。因為在擴(kuò)展SomeClass的時候,并不知道SomeClass的使用者會調(diào)用SomeMethod的哪個重載。
這對于單元測試一樣。如果三個方法都可以Mock,那么在測試時我們可能就會去“猜測”用戶究竟調(diào)用了哪個SomeMethod重載,而這是不確定的,也是容易變化的。如果我們只有一個重載可以Mock,那么則意味著“別挑了,就是這個”。所以,我有時候也不太喜歡Type Mock如此強(qiáng)大有力的Mock框架,因為它可能會破壞了被測試方的設(shè)計,把一切都變成了擴(kuò)展點——雖然這對于測試來說的確很方便,幾乎不輸給動態(tài)語言了。
“可測試性”也是設(shè)計出來,不是語言或平臺自動賦予的。這就是“design for testability”的體現(xiàn)之一吧。
以上就是有關(guān)virtual,override與擴(kuò)展點的一些討論。本文來自老趙點滴:《所有的成員都應(yīng)該是virtual的嗎?》
【編輯推薦】