《Effective C#》读书笔记——条目22:通过定义并实现接口替代继承<使用C#表达设计>
接口是一种按照契约设计的方式,一个类型必须实行接口中定义的方法。抽象基类则为一组相关的类型提供了一个共有的抽象。要注意二者的使用场景和区别:基类描述了对象是什么;接口描述了对象将如何表现行为。
1.关于接口
接口描述了一组功能,是一个契约,任何实现接口的类型必须为接口中定义的所有所有元素提供具体的实现。我们应该将可重用的行为提取出来,定义在接口中;由于不同相关的类型均可以实现一个接口,所有这会增加代码的重用率。对于开发者本身来说,实现接口要比继承自定义的基类更容易。
2.关于抽象基类
抽象基类除了描述共同行为,抽象基类还可以为派生类提供一些具体的实现(为子类通过通用、可重用的代码)。抽象基类可以为任何具体行为提供一个实现,而接口则不能。这种实现重用的方式为我们提供了另一种好处:在扩展系统功能时,通过向基类中添加并实现某种功能,所有的派生类立即拥有该功能,而向接口中添加一个成员,则会破坏所有实现该接口的类。
3.使用接口替代继承
使用抽象基类还是接口,代表了对日后可能发生的变化两种不同的态度:将一组功能封装在一个接口中,作为其他类型的实现契约。而基类则可以再日后进行扩展,这些扩展也会自动的成为子类的一部分。
3.1通过扩展方法来模拟继承
其实,这两种方式可以混合使用,也就是说:既可以让类型支持多个接口,也让其可以重用。我们知道,接口不能包含实现,也不能包含任何具体的数据成员。但是,扩展方法却是可以应用在接口上。例如:System.Linq.Enumerable类中就包含了30多个声明于IEnumerable<T>接口之上的扩展方法,所有实现了该接口的类型都会自动获得这些扩展方法自身的实现(注意:接口本身并不能包含任何实现,只是通过扩展方法的形式模拟地提供一些实现)。例如,下面的这个针对IEnumerable接口扩展方法:
1 public static class Extensions 2 { 3 /// <summary> 4 /// 为IEnumerable<T>类型添加扩展方法 5 /// </summary> 6 /// <typeparam name="T"></typeparam> 7 /// <param name="sequence"></param> 8 /// <param name="action"></param> 9 public static void ForAll<T>(this IEnumerable<T> sequence, Action<T> action) 10 { 11 foreach (T item in sequence) 12 { 13 action(item); 14 } 15 } 16 }
所有实现了IEnumerable接口的类型,会自动获取这个方法的实现;同样的,如果一个类实现了IEnumerable接口,那么它也会获得该接口的所有扩展方法的实现。
3.2 接口编程的灵活性
在.NET 环境下的继承是单根继承,根据接口编程要比根据基类编程拥有更大的灵活性,因为一个类型可以实现多个接口。同样的不相关的类型也可以实现同一个接口,这对在为不相关类编写公有逻辑时,使用接口可以简化你的工作。看下面的示例:假设某个程序需要管理员工、客户、第三方员工,这些类型并不相关(从继承体系来看),但是他们有一些公有属性,例如:名称、地址、电话等等:
1 public class Employee 2 { 3 public string FirstName{get;set;} 4 public string LastName{get;set;} 5 6 public string LastName 7 { 8 get 9 { 10 return string.Format("{0},{1}",LastName,FirstName); 11 } 12 } 13 //略... 14 } 15 16 public class Customer 17 { 18 public string LastName 19 { 20 get 21 { 22 return CustomerName; 23 } 24 } 25 //略... 26 private string CustomerName; 27 } 28 29 public class Vendor 30 { 31 public string Name 32 { 33 get 34 { 35 return vendorName; 36 } 37 } 38 //... 39 private string vendorName; 40 }
上面显示姓名属性,其他属性略过。现在我们可以把公共属性抽象成一个接口:
public interface IContactInfo { //姓名 string Name{get;} //联系电话 PhoneNumber primaryContact{get;} //传真 PhoneNumber Fax{get;} //住址 Address primaryAddress{get;} }
我们将上面所以类都实现IContactInfo接口:
1 //..其他类 略.. 2 public class Employee:IContactInfo 3 { 4 //...略 5 }
现在我们需要编写一个为这些类型打印自身信息的新方法:
1 public void PrintMailingLabel(IContactInfo ic) 2 { 3 //...略 4 }
我们可以看到所有的,只要是实现了该接口的类型都可以使用该方法,这得益于将我们公有逻辑放在了接口中。
同时,使用接口定义类的API可会提供更好的灵活性,当类型的属性以类的形式暴露时,也就暴露了该类的所有接口。而若是以暴露某个接口,那么就可以选择仅为使用者提供那些必要的方法和属性。而实现该接口的类属性实现细节,可能会随着时间改变。
3.3 在结构中使用接口避免拆箱
接口和抽象基类还有一个不同之处在于,抽象基类仅限于引用类型,而接口则没有这个限制。当我们将struct装箱时,该装箱对象实际上支持struct支持的所有接口。当通过接口指针来访问该struct时,我们不必拆箱即可访问到内部数据。如下面的例子:
1 public struct URLInfo : IComparable<URLInfo>, IComparable 2 { 3 private string URL; 4 private string description; 5 6 public int CompareTo(URLInfo other) 7 { 8 return URL.CompareTo(other.URL); 9 } 10 11 #region IComparable 成员 12 13 int IComparable.CompareTo(object obj) 14 { 15 if (obj is URLInfo) 16 { 17 URLInfo other = (URLInfo)obj; 18 return CompareTo(other); 19 } 20 else 21 { 22 throw new ArgumentException("比较的对象不是URLInfo类型"); 23 } 24 } 25 26 #endregion 27 }
由于URLInfo实现了IComparable<T>和IComparable接口,所有我们可以轻松的创建一个保护URLInfo对象的排序链表。即使在那些依赖老版本的IComparable的代码也会减少装箱和拆箱的次数,因为客户代码可以再不拆箱的情况下直接调用IComparable.CompareTo()。
小节
接口是一种按契约设计的方式:一个实现了某个接口的类型,必须提供接口中约定的所有方法实现。抽象基类则为一组项目类型提供了一个共用的共同抽象。仔细理解二者之间的差别,使用我们能够创建更富表现力和提高应对变化的设计。使用类层次来定义相关的类型,用接口暴露功能,并可以让不同类型实现这些接口。