程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則19:選擇定義和實現接口而不是繼承

Effective C#原則19:選擇定義和實現接口而不是繼承

編輯:關於C#

抽象類在類的繼承中提供了一個常規的“祖先”。一個接口描述 了一個可以被其它類型實現的原子級泛型功能。各有千秋,卻也不盡相同。接口 是一種合約式設計:一個類型實現了某個接口的類型,就必須實現某些期望的方 法。抽象類則是為一個相關類的集合提供常規的抽象方法。這些都是老套的東西 了:它是這樣的,繼承就是說它是某物(is a,),而接口就是說它有某個功能 (behaves like.)! 這些陳詞濫調已經說了好久了,因為它們提供了說明,同時 在兩個結構上描述它們的不同:基類是描述對象是什麼,接口描述對象有某種行為。

接口描述了一組功能集合,或者是一個合約。你可以在接口裡創建 任何的占位元素(placeholder,譯注:就是指先定義,後面再實現的一些內容) :方法,屬性,索引器以及事件。任何實現類型這個接口的類型必須為接口裡的 每個元素提供具體的內容。你必須實現所有的方法,提供全部屬性訪問器,索引 器,以及定義接口裡的所有事件。你在接口裡標記並且構造了可重用的行為。你 可以把接口當成參數或者返回值,你也可以有更多的機會重用代碼,因為不同的 類型可以實現相同的接口。更多的是,比起從你創建的基類派生,開發人員可以 更容易的實現接口。(譯注:不見得!)

你不能在接口裡提供任何成員的具 體實現,無論是什麼,接口裡面都不能實現。並且接口也不能包含任何具體的數 據成員。你是在定義一個合約,所有實現接口的類型都應該實現的合約。

抽象的基類可以為派生類提供一些具體的實現,另外也描述了一些公共 的行為。你可以更詳細的說明數據成員,具體方法,實現虛函數,屬性,事件以 及索引器。一個基類可以只提供部份方法的實現,從而只提供一些公共的可重用 的具體實現。抽象類的元素可以是虛的,抽象的,或者是非虛的。一個抽象類可 以為具體的行為提供一個可行的實現,而接口則不行。

重用這些實現還 有另一個好處:如果你在基類中添加一個方法,所有派生類會自動隱式的增加了 這個方法。這就是說,基類提供了一個有效的方法,可以隨時擴展幾個(派生)類 型的行為:就是向基類添加並實現方法,所有派生類會立即具有這些行為。而向 一個接口添加一個方法,所會破壞所有原先實現了這個接口的類。這些類不會包 含新的方法,而且再也通不過編譯。所有的實現者都必須更新,要添加新的方法 。

這兩個模式可以混合並重用一些實現代碼,同時還可以實現多個接口 。System.Collections.CollectionBase就是這樣的一個例子,它個類提供了一 個基類。你可以用這個基類你的客戶提供一些.Net缺少的安全集合。例如,它已 經為你實現了幾個接口:IList, ICollection,和IEnumerable。另外,它提供了 一個受保護的方法,你可以重載它,從而為不同的使用情況提供自己定義的行為 。IList接口包含向集合中添加新對象的Insert()方法。想自己更好的提供一個 Insert方法的實現,你可以通過重載CollectionBase類的OnInsert()或 OnInsertCcomplete()虛方法來處理這些事件:

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" );
 }
}

前面的代碼創建了一個整型的數組鏈表,而且使用 IList接口指針添加了兩個不同的值到集合中。通過重載OnInsert()方法, IntList類在添加類型時會檢測類型,如果不是一個整數時,會拋出一個異常。 基類給你實現了默認的方法,而且給我們提供了機會在我們自己的類中實現詳細 的行為。

CollectionBase這個基類提供的一些實現可以直接在你的類中 使用。你幾乎不用寫太多的代碼,因為你可以利用它提供的公共實現。但 IntList的公共API是通過CollectionBase實現接口而來的: IEnumerable,ICollection和IList接口。CollectionBase實現了你可以直接使用 的接口。

現在我們來討論用接口來做參數和返回值。一個接口可以被任 意多個不相關的類型實現。比起在基類中編碼,實現接口的編碼可以在開發人員 中提供更強的伸縮性。因為.Net環境中強制使用單繼承的,這使得實現接口這一 方法顯得很重要。

下面兩個方法完成了同樣的任務:

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( ) );
}

第二個方法的重用性遠不及第一個。Array,ArrayList, DataTable,HashTable,ImageList或者很多其它的集合類無法使用第二個方法 。讓方法的參數使用接口,可以讓程序具有通用性,而且更容易重用。

用接口為類定義API函數同樣可以取得很好的伸縮性。例如,很多應用程序使用 DataSet與你的應用程序進行數據交換。假設這一交流方法是不變的,那這太容 易實現了:

public DataSet TheCollection
{
 get { return _dataSetCollection; }
}

然而這讓你在將來很 容易遇到問題。某些情況下,你可能從使用DataSet改為暴露一個DataTable,或 者是使用DataView,甚至是使用你自己定義的對象。任何的改變都會破壞這些代 碼。當然,你可能會改變參數類型,但這會改變你的類的公共接口。修改一個類 的公共接口意味著你要在一個大的系統中修改更多的內容;你須要修改所有訪問 這個公共接口的地方。

緊接著的第二個問題麻煩問題就是:DataSet類提 供了許多方法來修改它所包含的數據。類的用戶可能會刪除表,修改列,甚至是 取代DataSet中的所有對象。幾乎可以肯定這不是你想要的。幸運的是,你可以 對用戶限制類的使用功能。不返回一個DataSet的引用,你就須要返回一個期望 用戶使用的接口。DataSet支持IListSource接口,它用於數據綁定:

using System.ComponentModel;
public IListSource TheCollection
{
 get { return _dataSetCollection as IListSource; }
}

IListSource讓用戶通過GetList()方法 來訪問內容,它同時還有ContainsListCollection屬性,因此用戶可以修改全部 的集合結構。使用IListSource接口,在DataSet裡的個別對象可以被訪問,但 DataSet的所有結構不能被修改。同樣,調用者不能使用DataSet的方法來修改可 用的行為,從而在數據上移動約束或者添加功能。

當你的類型以類的方 式暴露一些屬性時,它就暴露了這個類的全部接口。使用接口,你可以選擇只暴 露一部分你想提供給用戶使用的方法和屬性。以前在類上實現接口的詳細內容, 在後來是可以修改的(參見原則23)。

另外,不相關的類型可以實現同樣 的接口。假設你在創建一個應用程序。用於管理雇員,客戶和賣主。他們都不相 關,至少不存在繼承關系。但他們卻共享著某些功能。他們都有名字,而且很有 可能要在一些Windows控件中顯示他們的名字:

public class Employee
{
 public string Name
 {
  get
  {
   return string.Format( "{0}, {1}", _last, _first );
  }
 }
 // other details elided.
}
public class Customer
{
 public string Name
  {
  get
  {
   return _customerName;
  }
 }
 // other details elided
}
public class Vendor
{
 public string Name
 {
  get
   {
   return _vendorName;
  }
 }
}

Eyployee,Customer和Vendor類不應該共享一個基類。但它們共 享一些屬性:姓名(正如前面顯示的那樣),地址,以及聯系電話。你應該在一個 接口中創建這些屬性:

public interface IContactInfo
{
 string Name { get; }
 PhoneNumber PrimaryContact { get; }
 PhoneNumber Fax { get; }
 Address PrimaryAddress { get; }
}
public class Employee : IContactInfo
{
 // implementation deleted.
}

對於不的類型使用一些通用的 功能,接口可以簡化你的編程任務。Customer, Employee, 和Vendor使用一些相 同的功能,但這只是因為你把它們放在了接口上。

使用接口同樣意味著 在一些意外情況下,你可以減少結構類型拆箱的損失。當你把一個結構放到一個 箱中時,這個箱可以實現結構上的所有接口。當你用接口指針來訪問這個結構時 ,你不用結構進行拆箱就可以直接訪問它。這有一個例子,假設這個結構定義了 一個鏈接和一些說明:

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的對象創建一個有序表, 因為URLInfo實現了IComparable接口。URLInfo結構會在添加到鏈表中時被裝箱 ,但Sort()方法不須要拆箱就可以調用對象的CompareTo()方法。你還須要對參 數(other)進行拆箱,但你在調用IComparable.CompareTo()方法時不必對左邊的 對象進行拆箱。

基類可以用來描述和實現一些具體的相關類型的行為。 接口則是描述一些原子級別的功能塊,不相關的具體類型都可以實現它。接口以 功能塊的方法來描述這些對象的行為。如果你明白它們的不同之處,你就可以創 建出表達力更強的設計,並且它們面對修改是有很加強的伸縮性的。類的繼承可 以用來定義一些相關類型。通過實現一些接口來暴露部份功能來訪問這些類型。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved