C#語(yǔ)言引入了許多新的語(yǔ)法來(lái)表達(dá)程序設(shè)計(jì)。我們所選擇的技巧,實(shí)際上是向維護(hù)、擴(kuò)展和使用我們軟件的開(kāi)發(fā)人員表達(dá)了我們的設(shè)計(jì)意圖。所有的C#類型都生存于.NET環(huán)境中。.NET環(huán)境對(duì)于所有類型的能力也都有某種假設(shè)。如果我們違反了這些假設(shè),那么類型不能正常工作的可能性就會(huì)大大增加。 本章的條款并不是要對(duì)軟件設(shè)計(jì)技巧進(jìn)行概要介紹——這方面的著作已經(jīng)不少。相反,本章主要探討如何更好地利用不同的C#語(yǔ)言特性,來(lái)表達(dá)我們的軟件設(shè)計(jì)意圖。C#語(yǔ)言的設(shè)計(jì)者們添加了許多語(yǔ)言特性,來(lái)讓我們更清晰地表達(dá)現(xiàn)代軟件設(shè)計(jì)中的各種慣用法(idiom)。某些語(yǔ)言特性之間的差別非常小,我們通常有許多選擇。選擇多剛開(kāi)始看起來(lái)似乎是好事情,但是當(dāng)我們發(fā)現(xiàn)需要擴(kuò)展現(xiàn)有的程序時(shí),區(qū)別就開(kāi)始顯現(xiàn)了。我們首先要確保很好地理解本章中的各個(gè)條款,然后在應(yīng)用它們的時(shí)候,要對(duì)軟件未來(lái)可能的擴(kuò)展有一個(gè)清醒的認(rèn)識(shí)。 某些語(yǔ)法的改變使我們擁有了新的詞匯來(lái)表述日常的慣用法。屬性、索引器、事件和委托都是這樣例子,還有類與接口的區(qū)別:類定義類型,接口聲明行為?;惵暶黝愋?,同時(shí)定義一組相關(guān)類型所共有的行為。其他一些設(shè)計(jì)慣用法也由于垃圾收集器的引入而有所改變。而且,由于絕大多數(shù)變量都是引用類型,因此也會(huì)為我們的設(shè)計(jì)慣用法帶來(lái)一些變化。 本章的推薦條款將幫助大家選擇最自然的構(gòu)造來(lái)表達(dá)自己的軟件設(shè)計(jì),從而使創(chuàng)建的軟件更易于維護(hù)、擴(kuò)展和使用。 條款19:定義并實(shí)現(xiàn)接口優(yōu)于繼承類型 抽象基類為類層次(class hierarchy)提供了一個(gè)共用的祖先類(ancestor)。接口則描述了一組可以由某個(gè)類型實(shí)現(xiàn)的緊湊的功能。每一個(gè)都有自己的用武之地,但用處各不相同。接口是一種按合同設(shè)計(jì)(design by contract)的方式:一個(gè)實(shí)現(xiàn)了某個(gè)接口的類型,必須提供接口中約定的方法實(shí)現(xiàn)。抽象基類則為一組相關(guān)的類型提供了一個(gè)共用的抽象。下面的表述雖然是陳詞濫調(diào),但是很有用:繼承意味著“is a”,接口意味著“behaves like”。這些表述之所以至今仍有生命力,是因?yàn)樗鼈兒芎玫孛枋隽藘煞N構(gòu)造之間的差別:基類描述了對(duì)象是什么;接口描述了對(duì)象的行為方式。 接口描述了一組功能,或者說(shuō)一個(gè)合同。我們可以在接口中為任何構(gòu)造創(chuàng)建占位符(placeholder):方法、屬性、索引器和事件。任何實(shí)現(xiàn)了接口的類型都必須為接口中定義的所有元素提供具體的實(shí)現(xiàn),即必須實(shí)現(xiàn)所有的方法,提供所有的屬性訪問(wèn)器和索引器,并定義接口中定義的所有事件。我們應(yīng)該識(shí)別可重用的行為,并將它們提取出來(lái)定義在接口中。我們可以將接口用做函數(shù)的參數(shù),并返回值。由于不相關(guān)的類型可以共同實(shí)現(xiàn)一個(gè)接口,因此我們將有更多機(jī)會(huì)重用代碼。而且,實(shí)現(xiàn)一個(gè)接口對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),要比繼承一個(gè)我們創(chuàng)建的類型更加容易。 我們不能在接口中提供任何成員的實(shí)現(xiàn)。接口不能包含實(shí)現(xiàn),也不能包含任何具體的數(shù)據(jù)成員。接口是在聲明一種合同:所有實(shí)現(xiàn)了接口的類型都要負(fù)責(zé)履行其中的約定。 除了描述共同的行為外,抽象基類還可以為派生類型提供一些具體的實(shí)現(xiàn)。在抽象類中,我們可以指定數(shù)據(jù)成員、具體的方法、虛方法的實(shí)現(xiàn)、屬性、事件和索引器。基類可以實(shí)現(xiàn)一些具體的方法,因此可以為子類提供一些通用的可重用代碼。任何元素都可以為虛擬成員、抽象成員或者非虛成員。抽象基類可以為任何具體的行為提供一個(gè)實(shí)現(xiàn),而接口則不能。 這種實(shí)現(xiàn)重用還提供了另一種好處:如果向基類中添加一個(gè)方法,所有派生類都將自動(dòng)隱含這個(gè)方法。從這個(gè)角度來(lái)看,基類為我們提供了一種隨時(shí)間推移可以有效擴(kuò)展多個(gè)類型功能的方式。通過(guò)向基類中添加并實(shí)現(xiàn)某種功能,所有的派生類都將立即擁有該功能。而向接口中添加一個(gè)成員,則會(huì)破壞所有實(shí)現(xiàn)了該接口的類。它們不會(huì)包含新的方法,并且不會(huì)再通過(guò)編譯。每一個(gè)具體的類型都必須更新自己,來(lái)實(shí)現(xiàn)新的成員。 在抽象基類和接口之間做選擇,實(shí)際上是一個(gè)如何隨著時(shí)間的推移更好地支持抽象的問(wèn)題。接口的特點(diǎn)是比較穩(wěn)定:我們將一組功能封裝在一個(gè)接口中,作為其他類型的實(shí)現(xiàn)合同?;悇t可以隨著時(shí)間的推移進(jìn)行擴(kuò)展。這些擴(kuò)展將成為每個(gè)派生類的一部分。 上述兩種模型可以混合使用,從而允許類型在支持多個(gè)接口的同時(shí),可以重用實(shí)現(xiàn)代碼。一個(gè)典型的例子是System.Collections.CollectionBase。該類提供了一個(gè)基類,使用它可以避免.NET集合類中缺乏類型安全的問(wèn)題。同時(shí),它也實(shí)現(xiàn)了幾個(gè)我們需要的接口:IList、ICollection和IEnumerable。另外,它還提供了一些受保護(hù)的方法,我們可以重寫(xiě)它們來(lái)定制一些自己需要的行為。IList接口包含的Insert()方法會(huì)將一個(gè)新的對(duì)象添加到集合中。不用提供我們自己的Insert()實(shí)現(xiàn),我們就可以通過(guò)重寫(xiě)CollectionBase類的OnInsert()或者OnInsertCcomplete()虛方法來(lái)處理一些事件。 public class IntList : System.Collections.CollectionBase { protected override void OnInsert( int index, object value ) { try { int newValue = System.Convert.ToInt32( value ); Console.WriteLine( "Inserting {0} at position {1}", index.ToString(), value.ToString()); Console.WriteLine( "List Contains {0} items", this.List.Count.ToString()); } catch( FormatException e ) { throw new ArgumentException( "Argument Type not an integer", "value", e ); } } protected override void OnInsertComplete( int index, object value ) { Console.WriteLine( "Inserted {0} at position {1}", index.ToString( ), value.ToString( )); Console.WriteLine( "List Contains {0} items", this.List.Count.ToString( ) ); } } public class MainProgram { public static void Main() { IntList l = new IntList(); IList il = l as IList; il.Insert( 0,3 ); il.Insert( 0, "This is bad" ); } } 上述代碼創(chuàng)建了一個(gè)整數(shù)數(shù)組鏈表,并使用IList接口指針往集合中添加兩個(gè)不同的值。通過(guò)重寫(xiě)OnInsert()方法,IntList類可以測(cè)試插入值的類型,如果其類型不是整數(shù),它就會(huì)拋出一個(gè)異常?;悶槲覀兲峁┝四J(rèn)的實(shí)現(xiàn),并設(shè)置了一些掛鉤(hook)供我們定制派生類的行為。 CollectionBase基類為我們提供了一個(gè)可用的實(shí)現(xiàn)。我們基本上不需要編寫(xiě)很多代碼,因?yàn)榭梢允褂没愔刑峁┑耐ㄓ脤?shí)現(xiàn)。但是IntList的公有API來(lái)自于CollectionBase實(shí)現(xiàn)的接口:IList、ICollection和IEnumerable。CollectionBase為我們提供了這些接口的通用實(shí)現(xiàn)。 下面談?wù)剬⒔涌谟米鰠?shù)和返回值的情況。一個(gè)接口可以被任意數(shù)量的無(wú)關(guān)類型實(shí)現(xiàn)。針對(duì)接口的編碼方式(coding to interface)為其他開(kāi)發(fā)人員提供了比針對(duì)基類型的編碼方式(coding to base class type)更大的靈活性。這很重要,因?yàn)?NET環(huán)境將類型繼承層次限定為單繼承。 下面兩個(gè)方法執(zhí)行的是同樣的任務(wù): public void PrintCollection( IEnumerable collection ) { foreach( object o in collection ) Console.WriteLine( "Collection contains {0}", o.ToString( ) ); } public void PrintCollection( CollectionBase collection ) { foreach( object o in collection ) Console.WriteLine( "Collection contains {0}", o.ToString( ) ); } 第2個(gè)方法的可重用性比較差,它不能和Arrays、ArrayLists、DataTables、Hashtables、ImageLists或其他很多集合類一起使用。將接口作為方法的參數(shù)類型不僅適應(yīng)面廣,而且易于重用。 使用接口為一個(gè)類定義API還會(huì)為我們提供更大的靈活性。例如,許多應(yīng)用程序都使用DataSet在應(yīng)用程序的組件之間傳遞數(shù)據(jù)。這樣,就很容易將代碼像如下一樣寫(xiě)死: public DataSet TheCollection { get { return _dataSetCollection; } } 這會(huì)使我們很容易在將來(lái)遇到問(wèn)題。比如,在未來(lái)的某個(gè)時(shí)候,我們可能不希望向外界提供DataSet,轉(zhuǎn)而提供DataTable或者DataView,甚至是創(chuàng)建自定義的對(duì)象。所有這些改變都會(huì)破壞現(xiàn)有的代碼。當(dāng)然,我們可以改變參數(shù)類型,但是那會(huì)改變類型的公有接口。改變一個(gè)類的公有接口,會(huì)導(dǎo)致我們對(duì)龐大的系統(tǒng)做很多改變。該公有屬性被訪問(wèn)的所有地方,都需要進(jìn)行改變。 第2個(gè)問(wèn)題更為直接和棘手:DataSet類提供有許多方法可以改變其中包含的數(shù)據(jù)。這樣,類型用戶便可能刪除其中的表,修改其中的列,甚至替換其中的每一個(gè)對(duì)象。那肯定不會(huì)是我們想要的結(jié)果。幸運(yùn)的是,我們可以通過(guò)返回期望給用戶使用的接口(而非返回整個(gè)DataSet對(duì)象引用),來(lái)限制類型用戶的能力。DataSet支持IListSource接口,可作數(shù)據(jù)綁定之用: using System.ComponentModel; public IListSource TheCollection { get { return _dataSetCollection as IListSource; } } IListSource接口允許用戶通過(guò)GetList()方法來(lái)查看其中的數(shù)據(jù)。它還有一個(gè)ContainsListCollection屬性允許用戶判斷集合的整體結(jié)構(gòu)。使用IListSource接口,可以訪問(wèn)DataSet中的單個(gè)條目,但是其整體結(jié)構(gòu)不能被改變。另外,調(diào)用者也不能通過(guò)刪除約束或者添加功能,來(lái)使用DataSet上的方法改變其中數(shù)據(jù)上可用的行為。 當(dāng)使用類將屬性提供給外界時(shí),它實(shí)際上會(huì)把整個(gè)類的接口暴露給外界。通過(guò)使用接口,我們可以選擇只提供那些期望給用戶使用的方法和屬性。用來(lái)實(shí)現(xiàn)接口的類屬于實(shí)現(xiàn)細(xì)節(jié),它會(huì)隨著時(shí)間的推移而改變(參見(jiàn)條款23)。 此外,不相關(guān)的類型可以實(shí)現(xiàn)同樣的接口。假設(shè)我們編寫(xiě)了一個(gè)應(yīng)用程序來(lái)管理員工、客戶和廠商。至少在類層次中,它們之間沒(méi)有關(guān)聯(lián)。但是,它們共享著某種相同的功能。它們都有名稱,我們可能會(huì)在一些Windows控件中顯示這些名稱。 public class Employee { public string Name { get { return string.Format( "{0}, {1}", _last, _first ); } } // 忽略其他細(xì)節(jié)。 } public class Customer { public string Name { get { return _customerName; } } // 忽略其他細(xì)節(jié)。 } public class Vendor { public string Name { get { return _vendorName; } } } Employee、Customer和Vendor三個(gè)類不應(yīng)該共享一個(gè)基類。但是它們共享著一些屬性:名稱(如上面的代碼所展示)、地址和聯(lián)系電話。我們可以將這些屬性放在一個(gè)接口中: public interface IContactInfo { string Name { get; } PhoneNumber PrimaryContact { get; } PhoneNumber Fax { get; } Address PrimaryAddress { get; } } public class Employee : IContactInfo { // 忽略實(shí)現(xiàn)。 } 這個(gè)新的接口可以簡(jiǎn)化我們的編程任務(wù),因?yàn)樗试S我們創(chuàng)建相同的函數(shù)來(lái)操作不相關(guān)的類型: public void PrintMailingLabel( IContactInfo ic ) { // 忽略實(shí)現(xiàn)。 } 上面的函數(shù)可以應(yīng)用于所有實(shí)現(xiàn)了IContactInfo接口的類型。Employee、Customer和Vendor類型都可以作為上述函數(shù)的參數(shù),因?yàn)樗鼈兌紝?shí)現(xiàn)了該接口。 有時(shí)候,使用接口還可以幫助我們避免結(jié)構(gòu)類型的拆箱(unbox)代價(jià)。當(dāng)我們將結(jié)構(gòu)實(shí)例放入一個(gè)裝箱對(duì)象時(shí),該裝箱對(duì)象實(shí)際上支持結(jié)構(gòu)支持的所有接口。當(dāng)通過(guò)接口指針來(lái)訪問(wèn)該結(jié)構(gòu)時(shí),我們不必拆箱即可訪問(wèn)到內(nèi)部的數(shù)據(jù)。下面的例子展示了一個(gè)結(jié)構(gòu),其中定義了一個(gè)鏈接和一個(gè)描述: public struct URLInfo : IComparable { private string URL; private string description; public int CompareTo( object o ) { if (o is URLInfo) { URLInfo other = ( URLInfo ) o; return CompareTo( other ); } else throw new ArgumentException( "Compared object is not URLInfo" ); } public int CompareTo( URLInfo other ) { return URL.CompareTo( other.URL ); } } 由于URLInfo實(shí)現(xiàn)了IComparable接口,因此我們可以創(chuàng)建一個(gè)URLInfo對(duì)象的排序鏈表。將URLInfo結(jié)構(gòu)添加到鏈表中時(shí),它會(huì)被裝箱。但是Sort()方法不需要對(duì)排序過(guò)程中需要比較的兩個(gè)對(duì)象進(jìn)行拆箱,即可調(diào)用CompareTo()方法。當(dāng)然,我們?nèi)匀恍枰獙?duì)其中作為參數(shù)的那個(gè)對(duì)象(other)進(jìn)行拆箱,但是對(duì)于調(diào)用IComparable.CompareTo()方法時(shí)左邊的那個(gè)對(duì)象,則不需要拆箱。 綜上所述,基類描述并實(shí)現(xiàn)了一組相關(guān)類型間共用的行為。接口則描述了一組比較緊湊的功能,供其他不相關(guān)的具體類型來(lái)實(shí)現(xiàn)。二者都有自己的用武之地。類定義了我們要?jiǎng)?chuàng)建的類型。接口以功能分組的形式描述了那些類型的行為。如果理解好二者之間的差別,我們便可以創(chuàng)建更富表現(xiàn)力、更能應(yīng)對(duì)變化的設(shè)計(jì)。應(yīng)該使用類層次來(lái)定義相關(guān)的類型,然后讓它們實(shí)現(xiàn)不同的接口,以便通過(guò)接口向外界提供功能。 |
|
來(lái)自: coding > 《我的圖書(shū)館》