Effective C# 原则19:选择定义和实现接口,而不是继承

抽象类在类的继承中提供了一个常规的“祖先”。一个接口描述了一个可以被其它类型实现的原子级泛型功能。各有千秋,却也不尽相同。接口是一种合约式设计:一个类型实现了某个接口的类型,就必须实现某些期望的方法。抽象类则是为一个相关类的集合提供常规的抽象方法。这些都是老套的东西了:它是这样的,继承就是说它是某物(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()方法时不必对左边的对象进行拆箱。

基类可以用来描述和实现一些具体的相关类型的行为。接口则是描述一些原子级别的功能块,不相关的具体类型都可以实现它。接口以功能块的方法来描述这些对象的行为。如果你明白它们的不同之处,你就可以创建出表达力更强的设计,并且它们面对修改是有很加强的伸缩性的。类的继承可以用来定义一些相关类型。通过实现一些接口来暴露部份功能来访问这些类型。

========================================================    

Item 19: Prefer Defining and Implementing Interfaces to Inheritance
Abstract base classes provide a common ancestor for a class hierarchy. An interface describes one atomic piece of functionality that can be implemented by a type. Each has its place, but it is a different place. Interfaces are a way to design by contract: A type that implements an interface must supply an implementation for expected methods. Abstract base classes provide a common abstraction for a set of related types. It's a cliché, but it's one that works: Inheritance means "is a," and interfaces means "behaves like." These clichés have lived so long because they provide a means to describe the differences in both constructs: Base classes describe what an object is; interfaces describe one way in which it behaves.

Interfaces describe a set of functionality, or a contract. You can create placeholders for anything in an interface: methods, properties,indexers, and events. Any type that implements the interface must supply concrete implementations of all elements defined in the interface. You must implement all methods, supply any and all property accessors and indexers, and define all events defined in the interface. You identify and factor reusable behavior into interfaces. You use interfaces as parameters and return values. You also have more chances to reuse code because unrelated types can implement interfaces. What's more, it's easier for other developers to implement an interface than it is to derive from a base class you've created.

What you can't do in an interface is provide implementation for any of these members. Interfaces contain no implementation whatsoever,and they cannot contain any concrete data members. You are declaring the contract that must be supported by all types that implement an interface.

Abstract base classes can supply some implementation for derived types, in addition to describing the common behavior. You can specify data members, concrete methods, implementation for virtual methods, properties, events, and indexers. A base class can provide implementation for some of the methods, thereby providing common implementation reuse. Any of the elements can be virtual, abstract, or nonvirtual. An abstract base class can provide an implementation for any concrete behavior; interfaces cannot.

This implementation reuse provides another benefit: If you add a method to the base class, all derived classes are automatically and implicitly enhanced. In that sense, base classes provide a way to extend the behavior of several types efficiently over time: By adding and implementing functionality in the base class, all derived classes immediately incorporate that behavior. Adding a member to an interface breaks all the classes that implement that interface. They will not contain the new method and will no longer compile. Each implementer must update that type to include the new member.

Choosing between an abstract base class and an interface is a question of how best to support your abstractions over time. Interfaces are fixed: You release an interface as a contract for a set of functionality that any type can implement. Base classes can be extended over time. Those extensions become part of every derived class.

The two models can be mixed to reuse implementation code while supporting multiple interfaces. One such example is System.Collections.CollectionBase.. This class provides a base class that you can use to shield clients from the lack of type safety in .NET collections. As such, it implements several interfaces on your behalf: IList, ICollection, and IEnumerable. In addition, it provides protected methods that you can override to customize the behavior for different uses. The IList interface contains the Insert() method to add a new object to a collection. Rather than provide your own implementation of Insert, you process those events by overriding the OnInsert() or OnInsertCcomplete() virtual methods of the CollectionBase class.

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

The previous code creates an integer array list and uses the IList interface pointer to add two different values to the collection. By overriding the OnInsert() method, the IntList class tests the type of the inserted value and throws an exception when the type is not an integer. The base class provides the default implementation and gives you hooks to specialize the behavior in your derived classes.

CollectionBase, the base class, gives you an implementation that you can use for your own classes. You need not write nearly as much code because you can make use of the common implementation provided. But the public API for IntList comes from the interfaces implemented by CollectionBase: the IEnumerable, ICollection, and IList interfaces. CollectionBase provides a common implementation for the interfaces that you can reuse.

That brings me to the topic of using interfaces as parameters and return values. An interface can be implemented by any number of unrelated types. Coding to interfaces provides greater flexibility to other developers than coding to base class types. That's important because of the single inheritance hierarchy that the .NET environment enforces.

These two methods perform the same task:

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

The second method is far less reusable. It cannot be used with Arrays, ArrayLists, DataTables, Hashtables, ImageLists, or many other collection classes. Coding the method using interfaces as its parameter types is far more generic and far easier to reuse.

Using interfaces to define the APIs for a class also provides greater flexibility. For example, many applications use a DataSet to transfer data between the components of your application. It's too easy to code that assumption into place permanently:

[code]public DataSet TheCollection
{
  get { return _dataSetCollection; }
}
That leaves you vulnerable to future problems. At some point, you might change from using a DataSet to exposing one DataTable, using a DataView, or even creating your own custom object. Any of those changes will break the code. Sure, you can change the parameter type, but that's changing the public interface to your class. Changing the public interface to a class causes you to make many more changes to a large system; you would need to change all the locations where the public property was accessed.

The second problem is more immediate and more troubling: The DataSet class provides numerous methods to change the data it contains. Users of your class could delete tables, modify columns, or even replace every object in the DataSet. That's almost certainly not your intent. Luckily, you can limit the capabilities of the users of your class. Instead of returning a reference to the DataSet type, you should return the interface you intend clients to use. The DataSet supports the IListSource interface, which it uses for data binding:

using System.ComponentModel;

public IListSource TheCollection
{
  get { return _dataSetCollection as IListSource; }
}

IListSource lets clients view items through the GetList() method. It also has a ContainsListCollection property so that users can modify the overall structure of the collection. Using the IListSource interface, the individual items in the DataSet can be accessed, but the overall structure of the DataSet cannot be modified. Also, the caller cannot use the DataSet's methods to change the available actions on the data by removing constraints or adding capabilities.

When your type exposes properties as class types, it exposes the entire interface to that class. Using interfaces, you can choose toexpose only the methods and properties you want clients to use. The class used to implement the interface is an implementation detail that can change over time (see Item 23).

Furthermore, unrelated types can implement the same interface. Suppose you're building an application that manages employees, customers, and vendors. Those are unrelated, at least in terms of the class hierarchy. But they share some common functionality. They all have names, and you will likely display those names in Windows controls in your applications.

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

The Employee, Customer, and Vendor classes should not share a common base class. But they do share some properties: names (as shown earlier), addresses, and contact phone numbers. You could factor out those properties into an interface:

public interface IContactInfo
{
  string Name { get; }
  PhoneNumber PrimaryContact { get; }
  PhoneNumber Fax { get; }
  Address PrimaryAddress { get; }
}

public class Employee : IContactInfo
{
  // implementation deleted.
}
This new interface can simplify your programming tasks by letting you build common routines for unrelated types:

public void PrintMailingLabel( IContactInfo ic )
{
  // implementation deleted.
}

This one routine works for all entities that implement the IContactInfo interface. Customer, Employee, and Vendor all use the same routinebut only because you factored them into interfaces.

Using interfaces also means that you can occasionally save an unboxing penalty for structs. When you place a struct in a box, the box supports all interfaces that the struct supports. When you access the struct through the interface pointer, you don't have to unbox the struct to access that object. To illustrate, imagine this struct that defines a link and a description:

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

You can create a sorted list of URLInfo objects because URLInfo implements IComparable. The URLInfo structs get boxed when added to the list. But the Sort() method does not need to unbox both objects to call CompareTo(). You still need to unbox the argument (other), but you don't need to unbox the left side of the compare to call the IComparable.CompareTo() method.

Base classes describe and implement common behaviors across related concrete types. Interfaces describe atomic pieces of functionality that unrelated concrete types can implement. Both have their place. Classes define the types you create. Interfaces describe the behavior of those types as pieces of functionality. If you understand the differences, you will create more expressive designs that are more resilient in the face of change. Use class hierarchies to define related types. Expose functionality using interfaces implemented across those types.

评论: 0 | 引用: 0 | 查看次数: 4823
发表评论
登录后再发表评论!