你必须知道的.NET(第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.5 玩转接口

本节将介绍以下内容:

·什么是接口

·接口映射本质

·面向接口编程

·典型的.NET接口

1.5.1 引言

接口,是面向对象设计中的重要元素,也是打开设计模式精要之门的钥匙。玩转接口,就意味着紧握这把钥匙,打开面向对象的抽象之门,成全设计原则、成就设计模式,实现集优雅和灵活于一身的代码艺术。

本节,从接口由来讲起,通过概念阐述、面向接口编程的分析以及.NET框架中的典型接口实例,勾画一个理解接口的框架蓝图,通过这一蓝图将会了解玩转接口的学习曲线。

1.5.2 什么是接口

所谓接口,就是契约,用于规定一种规则由大家遵守。所以,.NET中很多的接口都以able为命名后缀,例如INullable、ICloneable、IEnumerable、IComparable等,意指能够为空、能够克隆、能够枚举、能够对比,其实正是对契约的一种遵守寓意,只有实现了ICloneable接口的类型,才允许其实例对象被拷贝。以社会契约而言,只有司机,才能够驾驶,人们必须遵守这种约定,无照驾驶将被视为犯罪而不被允许,这是社会契约的表现。由此来理解接口,才是对面向接口编程及其精髓的把握,例如:

interface IDriveable
{
   void Drive();
}

面向接口编程就意味着,在自定义类中想要有驾驶这种特性,就必须遵守这种契约,因此必须让自定义类实现IDriveable接口,从而才使其具有了“合法”的驾驶能力。例如:

public class BusDriver : IDriveable
{
   public void Drive()
   {
      Console.WriteLine("有经验的司机可以驾驶公共汽车。");
   }
}

没有实现IDriveable接口的类型,则不被允许具有Drive这一行为特性,所以接口是一组行为规范。例如要使用foreach语句迭代,其前提是操作类型必须实现IEnumerable接口,这也是一种契约。

实现接口还意味着,同样的方法对不同的对象表现为不同的行为。如果使司机具有驾驶拖拉机的能力,也必须实现IDriveable接口,并提供不同的行为方式,例如:

public class TractorDriver: IDriveable
{
   public void Drive()
   {
      Console.WriteLine("拖拉机司机驾驶拖拉机。");
   }
}

在面向对象世界里,接口是实现抽象机制的重要手段,通过接口实现可以部分的弥补继承和多态在纵向关系上的不足,具体的讨论可以参见1.4节“多态的艺术”和8.4节“面向抽象编程:接口和抽象类”。接口在抽象机制上,表现为基于接口的多态性,例如:

public static void Main()
{
   IList<IDriveable> drivers = new List<IDriveable>();
   drivers.Add(new BusDriver());
   drivers.Add(new CarDriver());
   drivers.Add(new TractorDriver());
   foreach (IDriveable driver in drivers)
   {
      driver.Drive();
   }
}

通过接口实现,同一个对象可以有不同的身份,这种设计的思想与实现,广泛存在于.NET框架类库中,正是这种基于接口的设计成就了面向对象思想中很多了不起的设计模式。

1.5.3 .NET中的接口

1.接口多继承

在.NET中,CLR支持单实现继承和多接口继承。这意味着同一个对象可以代表多个不同的身份,以DateTime为例,其定义为:

public struct DateTime : IComparable, IFormattable, IConvertible, ISerializable,IComparable<DateTime>, IEquatable<DateTime>

因此,可以通过DateTime实例代表多个身份,不同的身份具有不同的行为,例如:

public static void Main()
{
   DateTime dt = DateTime.Today;
   int result = ((IComparable)dt).CompareTo(DateTime.MaxValue);
   DateTime dt2 = ((IConvertible)dt).ToDateTime(new
                                    System.Globalization.DateTimeFormatInfo());
}

2.接口的本质

从概念上理解了接口,还应进一步从本质上揭示其映射机制,在.NET中基于接口的多态究竟是如何被实现的呢?这是值得思考的话题,根据下面的示例,及其IL分析,我们对此进行一定的探讨:

interface IMyInterface
{
   void MyMethod();
}

该定义在Reflector中的IL为:

.class private interface abstract auto ansi IMyInterface
{
   .method public hidebysig newslot abstract virtual instance void MyMethod() cil managed
   {
   }
}

根据IL分析可知,IMyInterface接口本质上仍然被标记为.class,同时提供了abstract virtual方法MyMethod,因此接口其实本质上可以看做是一个定义了抽象方法的类,该类仅提供了方法的定义,而没有方法的实现,其功能由接口的实现类来完成,例如:

class MyClass : IMyInterface
{
   void IMyInterface.MyMethod()
   {
   }
}

其对应的IL代码为:

.class private auto ansi beforefieldinit MyClass
   extends [mscorlib]System.Object
   implements InsideDotNet.OOThink.Interface.IMyInterface
{
   .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
   {
   }
   .method private hidebysig newslot virtual final instance void InsideDotNet.OOThink. Interface.IMyInterface.MyMethod() cil managed
   {
       .override InsideDotNet.OOThink.Interface.IMyInterface::MyMethod
   }
}

由此可见,实现了接口的类方法在IL标记为override,表示覆写了接口方法实现,因此接口的抽象机制仍然是多态来完成的。接口在本质上,仍旧是一个不能实例化的类,但是又区别于一般意义上的类,例如不能实例化、允许多继承、可以作用于值类型等。

那么在CLR内部,接口的方法分派是如何被完成的呢?在托管堆中CLR维护着一个接口虚表来完成方法分派,该表基于方法表内的接口图信息创建,主要保存了接口实现的索引记录。以IMyInterface为例,在MyClass第一次加载时,CLR检查到MyClass实现了IMyInterface的MyMethod方法,则会在接口虚表中创建一条记录信息,用于保存MyClass方法表中实现了MyMethod方法的引用地址,其他实现了IMyInterface的类型都会在接口虚表中创建相应的记录。因此,接口的方法调用是基于接口虚表进行的。

3.由string所想到的:框架类库的典型接口

在.NET框架类库中,存在大量的接口,以典型的System.String类型为例,就可知接口在FCL设计中的重要性:

public sealed class String : IComparable, ICloneable, IConvertible, Icomparable <string>, IEnumerable<char>, IEnumerable, IEquatable<string>

其中IComparable<string>、IEnumerable<char>和IEquatable<string>为泛型接口,具体的讨论可以参见11.3节“深入泛型”。

表1.2对几个典型的接口进行简要的分析,以便在FCL的探索中不会感觉陌生,同时也有助于熟悉框架类库。

表1-2 FCL的典型接口

关于框架类库的接口讨论,在本书的各个部分均有所涉及,例如关于集合的若干接口IList、ICollection、IDictionary等在8.9节“集合通论”中有详细的讨论,在本书的学习过程中将会逐渐有所收获,在此仅做简要介绍。

1.5.4 面向接口的编程

设计模式的师祖GoF,有句名言:Program to an interface, not an implementation,表示对接口编程而不要对实现编程,更通俗的说法是对抽象编程而不要对具体编程。关于面向对象和设计原则,将始终强调对抽象编程的重要性,这源于抽象代表了系统中相对稳定并又能够通过多态特性对其扩展,这很好地符合了高内聚、低耦合的设计思想。

下面,就以著名的Petshop 4.0中一个简单的面向对象设计片段为例,来诠释面向接口编程的奥秘。

在Petshop 4.0的数据访问层设计上,微软设计师将较为基础的增删改查操作封装为接口,由具体的实体操作类来实现。抽象出的单独接口模块,使得对于数据的操作和业务逻辑对象相分离。借鉴这种设计思路实现一个简单的用户操作数据访问层,其设计如图1-14所示。

从上述设计可见,通过接口将增删改查封装起来,再由具体的MySQLUser、AccessUser和XMLUser来实现,Helper类则提供了操作数据的通用方法。基于接口的数据访问层和具体的数据操作实现彻底隔离,对数据的操作规则的变更不会影响实体类对象的行为,体现了职责分离的设计原则,而这种机制是通过接口来完成的。

图1-14 基于Petshop的数据访问层设计

同时,能够以IUser接口来统一处理用户操作,例如在具体的实例创建时,可以借助反射机制,通过依赖注入来设计实现:

public sealed class DataAccessFactory
{
   private static readonly string assemblyPath = ConfigurationManager.AppSettings["AssemblyPath"];
   private static readonly string accessPath = ConfigurationManager.AppSettings["AccessPath"];
   public static IUser CreateUser()
   {
      string className = accessPath + ".User";
      return (IUser)Assembly.Load(assemblyPath).CreateInstance(className);
   }
}

你看,通过抽象可以将未知的对象表现出来,通过读取配置文件的相关信息可以很容易创建具体的对象,当有新的类型增加时不需要对原来的系统做任何修改只要在配置文件中增加相应的类型全路径即可。这种方式体现了面向接口编程的另一个好处:对修改封闭而对扩展开放。

正是基于这种设计才形成了数据访问层、业务逻辑层和表现层三层架构的良好设计。而数据访问层是实现这一架构的基础,在业务逻辑层,将只有实体对象的相互操作,而不必关心具体的数据库操作实现,甚至看不到任何SQL语句执行的痕迹,例如:

public class BLL
{
   private static readonly IUser user = DataAccessFactory.CreateUser();
   private static User userInfo = new User();
   public static void HandleUserInfo(string ID)
   {
      userInfo = user.GetUser(ID);
      //对userInfo实体对象进行操作
   }
}

另外,按照接口隔离原则,接口应该被实现为具有单一功能的多个小接口,而不是具有多个功能的大接口。通过多个接口的不同组合,客户端按需实现不同的接口,从而避免出现接口污染的问题。

1.5.5 接口之规则

关于接口的规则,可以有以下的归纳:

·接口隔离原则强调接口应该被实现为具有单一功能的小接口,而不要实现为具有多个功能的胖接口,类对于类的依赖应建立在最小的接口之上。

·接口支持多继承,既可以作用于值类型,也可以作用于引用类型。

·禁止为已经发布的接口,添加新的成员,这意味着你必须重新修改所有实现了该接口的类型,在实际的应用中,这往往是不可能完成的事情。

·接口不能被实例化,没有构造函数,接口成员被隐式声明为public。

1.5.6 结论

通常而言,良好的设计必然是面向抽象的,接口是实现这一思想的完美手段之一。通过面向接口编程,保证了系统的职责清晰分离,实体与实体之间保持相对合适的耦合度,尤其是高层模块不再依赖于底层模块,而依赖于比较稳定的抽象,使得底层的更改不会波及高层,实现了良好的设计架构。

透彻地了解接口,认识对接口编程,体会面向对象的设计原则,是培养一个良好设计习惯的开端。关于接口,是否玩得过瘾,就看如何体会本节强调的在概念上的契约,在设计上的抽象。