为什么要使用接口?而不直接使用类呢?
目录
2 为什么要使用接口?而不是直接实现呢?
2.1 面向接口编程
2.2 软件设计中有关接口的原则
2.3 我所遇到的使用接口的场景
3 接口和抽象类有什么不同?
3.1 相同点
3.2 区别
4 使用接口还是抽象类?
4.1 IS A VS CAN-DO 关系
4.2 易于使用
4.3 版本控制
5 C#中的接口
5.1 接口隐式方法实现
5.2 接口的实现加virtual的情况
5.3 接口的显示方法实现
5.4 泛型接口
5.5 泛型和接口约束
6 参考资料
1 什么是接口
- 接口是一种用来定义程序的协议,它描述可属于任何类或结构的一组相关行为。
接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界“如果你是……则必须能……”的理念。
接口是在一定粒度视图上同类事物的抽象表示。因为“同类事物”这个概念是相对的,它因为粒度视图不同而不同。
2 为什么要使用接口?而不是直接实现呢?
接口的使用并非总是从设计的角度来考虑。接口和C#其他语法现象一样,共同构成了C#整个语言体系。
接口的意义在于 抽象、不拘细节,从而使同类事物在同一高度具有通用及可替代性。
关于解耦,并不是接口能解耦,而是抽象能解耦 接口只是手段,如果两个事物有必然联系,那么就不会出现完全解耦,只能耦合转移。
—— from http://bbs.csdn.net/topics/380040137
在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。
- 系统灵活性增强
当下层需要改变时,只要接口及接口功能不变,则上层不用做任何修改。甚至可以在不改动上层代码时将下层整个替换掉,就像我们将一个WD的60G硬盘换成一个希捷的160G的硬盘,计算机其他地方不用做任何改动,而是把原硬盘拔下来、新硬盘插上就行了,因为计算机其他部分不依赖具体硬盘,而只依赖一个IDE接口,只要硬盘实现了这个接口,就可以替换上去。
- 不同部件或层次的开发人员可以并行开工
就像造硬盘的不用等造CPU的,也不用等造显示器的,只要接口一致,设计合理,完全可以并行进行开发,从而提高效率。
那么具体什么时候用,什么时候不用呢?在常见的三层架构中,有以下几个层次,分别进行说明:
- 界面层
也就是展示层,直接呈现给用户的,可能不同的软件有不同的呈现方式,比如Web,WinForm,甚至移动APP,在这个层次,我认为是没有必要写太多的接口。
- 业务逻辑层
这个层次,业务逻辑,可以根据需要使用接口。如果是直接读写数据库什么的,就直接用调用数据库访问层的接口。如果是与多个第三方接口进行交互,那么就需要接口,不同的渠道各自实现。
- 数据访问层
数据访问层,最好使用接口,比如数据库访问,这种可以根据不同的数据库实现相应的接口向业务逻辑层提供服务。
可能在开发的时候,一开始我们并没有想到要使用接口。可能简单就用一个类实现了。到后面新的需求过来的时候,发现代码需要重构,要用接口和抽象类等等。这个也需要看个人编码的习惯。有的人就长篇大论一个类完成所有的逻辑。这样的开发人员,应该是很少见过好的代码,如果见过的话,后面肯定会精简做到更好。而另外一些人可能一开始就能嗅出来哪些地方需要使用接口,哪些地方使用抽象类,这也是一种思维方式。前面一种只管开发当前的功能。而后面一种则会考虑到以后的扩展。总而言之,需要根据不同的情况进行考虑。
2.1 面向接口编程
面向接口编程:面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一.
2.2 软件设计中有关接口的原则
我一直认为这个问题,应该从设计的角度来讲。在软件设计的六大设计原则中,与接口直接相关的就有以下两个:
- 依赖倒置原则
高层模块不应该依赖底层模块,二则都应该依赖其抽象,抽象不应该依赖细节;细节应该依赖抽象。
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:面向接口编程,将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
- 接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案:在设计接口的时候要精简单一,将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
- 总结
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。接口在设计模式中,有很多的灵活应用。
2.3 我所遇到的使用接口的场景
- WCF服务的契约就是接口
- 数据库访问层
定义数据库访问层的接口,然后不同的数据库类型(MySQL/SQL Server)实现不同的接口,向业务层提供服务。这样如果说从SQL Server数据库迁移到MySQL数据库,业务层几乎不需要怎么改动,直接用MySQL的进行访问就可以了。 在数据访问接口层的参数通常都是IDbConnection这样的接口,而不是具体的类。体现了,依赖倒置原则。
3 接口和抽象类有什么不同?
这个主要是以C#语言为基础来讲的。
3.1 相同点
- 都可以被继承
- 都不能被实例化
- 都可以包含方法声明
- 派生类必须实现未实现的方法
3.2 区别
- 语法上
接口 | 抽象类 |
---|---|
接口只能定义属性、索引器、事件、和方法声明,没有普通成员变量 | 抽象类没有此限制 |
接口不能有构造方法(这简直是废话) | 抽象类可以有构造方法 |
接口中的所有方法必须都是抽象的 | 抽象类中可以包含非抽象的普通方法 |
接口中的方法只能是public类型的(默认) | 抽象类中的抽象方法的访问类型可以是public,protected |
接口可以用于支持回调 | 而继承并不具备这个特点 |
实现接口的类中的接口方法却默认为非虚的,(实现类的派生类,不可以再重写实现类接口方法,但派生类可以再显示实现接口的方法) 实现接口的类中的接口方法可以声明为virtual(这样实现类的派生类还可以重写该方法). | 抽象类实现的具体方法默认为虚的 |
- 其他方面
接口 | 抽象类 |
---|---|
接口是一个行为规范 | 抽象类是一个不完整的类,需要进一步细化 |
接口可以被多重实现 | 抽象类只能被单一继承 |
接口大多数是关系疏松但都实现某一功能的类中 | 抽象类更多的是定义在一系列紧密相关的类间 |
接口是为了满足外部调用而定义的一个功能约定, 因此反映的是事物的外部特性 | 抽象类是从一系列相关对象中抽象出来的概念, 因此反映的是事物的内部共性 |
4 使用接口还是抽象类?
4.1 IS A VS CAN-DO 关系
- IS A 关系用抽象类
一般到具体的这样一个关系,就用抽象。
逻辑相关,并且有相同的功能的可以使用抽象类,不用每个接口都去写。
- CAN DO 关系用接口,值类型用接口
接口是对同类事物的横切面的一个抽象。体现能的逻辑关系。在设计接口的时候,依据接口隔离原则,接口的方法都是必须的,最少的。实现类如果要使用一个接口,那么它必须实现接口的所有方法。即全都能。
-
- 接口和抽象类可以同时使用。
两件事情实际上可以同时做:定义一个接口,同时提供一个实现了这个接口的基类。
4.2 易于使用
- 定义一个从基类派生的新类型通常比实现一个接口的所有方法容易得多。基类型可以提供大量功能,所以派生类可能只需要针对其行为稍作改动。
- 使用接口,则新类型必须实现所有的成员。
4.3 版本控制
- 向基类添加一个方法,派生类型将继承新方法。一开始使用的就是一个能正常工作的类型。用户的源代码甚至不需要重新编译。
- 向接口添加一个方法,会强迫接口的继承者更改其源代码并重新编译
5 C#中的接口
下面看几个C#接口的例子及实现。其实单纯的为了应用接口而应用接口是没什么意思的,我的理解,接口一定是跟软件设计相关,它是比面向对象更高一个层次,下面这些例子,第一个例子很常见,我们平常就是这么用的。第二个例子不是特别常见,因为接口的实现类,一般不会允许再可以有派生类,一般来讲,都会是直接实现。第三个例子,只是看看我们接口复杂的用法,相信工作中很少会遇到这样写得。但如果这样写一定有这样写得道理。
5.1 接口隐式方法实现
代码如下:
/// <summary> /// IMessage /// </summary> interface IMessage { void ShowMessage(); }
/// <summary> /// ConsoleMessage /// </summary> class ConsoleMessage : IMessage { #region IMessage 成员 /// <summary> /// ShowMessage /// </summary> public void ShowMessage() { Console.WriteLine("ConsoleMessage.ShowMessage()"); } #endregion }
测试代码如下:
/// <summary> /// Main /// </summary> /// <param name="args">args</param> static void Main(string[] args) { // 1、不加virtual实现接口 ConsoleMessage consoleMsg = new ConsoleMessage(); // 结果:ConsoleMessage.ShowMessage() ((IMessage)consoleMsg).ShowMessage(); // 结果:ConsoleMessage.ShowMessage() consoleMsg.ShowMessage(); }
这是最常用的情况。一个接口,一个实现类。可以通过接口调用方法, 也可以通过实现类调用方法。
5.2 接口的实现加virtual的情况
代码如下:
/// <summary> /// IMessage /// </summary> interface IMessage { void ShowMessage(); } /// <summary> /// VirtualMessage /// </summary> class VirtualMessage : IMessage { /// <summary> /// ShowMessage /// </summary> public virtual void ShowMessage() { Console.WriteLine("VirtualMessage.ShowMessage()"); } } /// <summary> /// ExtendVirtualMessage /// </summary> class ExtendVirtualMessage : VirtualMessage { /// <summary> /// ShowMessage /// </summary> public override void ShowMessage() { Console.WriteLine("ExtendVirtualMessage.ShowMessage()"); } }
测试代码:
/// <summary> /// Main /// </summary> /// <param name="args">args</param> static void Main(string[] args) { ///3、重写VirtualMessage的ShowMessage接口 ExtendVirtualMessage extendVirtualMsg = new ExtendVirtualMessage(); // ExtendVirtualMessage.ShowMessage() ((IMessage)extendVirtualMsg).ShowMessage(); // ExtendVirtualMessage.ShowMessage() ((VirtualMessage)extendVirtualMsg).ShowMessage(); // ExtendVirtualMessage.ShowMessage() extendVirtualMsg.ShowMessage(); }
实现类的派生类重写接口方法,可用接口,实现类,实现类的派生类去调用方法,但结果都是一致的。
5.3 接口的显示方法实现
显示接口方法实现的定义:将定义方法的那个接口的名称作为方法名前缀(例如IDisposable.Dispose),就会创建显式接口方法实现。注意C#不允许在显式接口方法指定可访问性(比如public或者private)。但是编译器生成方法的元数据时,可访问性会自动设为private
代码如下:
/// <summary> /// IMessage /// </summary> interface IMessage { void ShowMessage(); } /// <summary> /// ConsoleMessage /// </summary> class ConsoleMessage : IMessage { #region IMessage 成员 /// <summary> /// ShowMessage /// </summary> public void ShowMessage() { Console.WriteLine("ConsoleMessage.ShowMessage()"); } #endregion } /// <summary> /// EIMIMessage /// </summary> class EIMIMessage : ConsoleMessage, IMessage { /// <summary> /// ShowMessage /// </summary> public new void ShowMessage() { Console.WriteLine("EIMIMessage.new ShowMessage()"); } #region IMessage 成员 /// <summary> /// ShowMessage /// </summary> void IMessage.ShowMessage() { Console.WriteLine("EIMIMessage.IMessage.ShowMessage()"); } #endregion }
测试结果如下:
/// <summary> /// Main /// </summary> /// <param name="args">args</param> static void Main(string[] args) { // 4、显示实现接口等综合类 EIMIMessage eimiMsg = new EIMIMessage(); // EIMIMessage.IMessage.ShowMessage() ((IMessage)eimiMsg).ShowMessage(); // ConsoleMessage.ShowMessage() ((ConsoleMessage)eimiMsg).ShowMessage(); // EIMIMessage.new ShowMessage() eimiMsg.ShowMessage(); Console.Read(); }
1、EIMIMessage 实现IMessage接口,显示实现接口方法ShowMessage,显示实现的接口方法,只能通过接口去调用。所以结果是:EIMIMessage.IMessage.ShowMessage()
2、EIMIMessage 的基类ConsoleMessage,如果转换成:ConsoleMessage,则调用的就是ConsoleMessage类实现的方法。
3、EIMIMessage 类继承ConsoleMessage,是不能够继承ConsoleMessage.ShowMessage的方法,只能通过new 关键字重新写一个方法。因此用EIMIMessage 的对象去调用,则是重写的这个类。
谨慎使用显示接口方法实现
- 没有文档解释类型具体是如何实现一个EIMI方法,也没有Microsoft Visual Studio“智能感知”支持
- 值类型的实例在转换成接口时装箱
- EIMI不能由派生类型调用
5.4 泛型接口
泛型接口的优点:
- 泛型接口提供了出色的编译时类型安全性
有的接口比如(非泛型Icomparable接口)定义的方法使用了Object参数或Object返回类型。在代码中调用这些接口方法时,可以传递对任何类型的实例的引用。但这通常不是我们期望的
- 处理值类型的时候,装箱次数会减少
static void TestInterface() { Int32 x = 1; Int32 y = 2; // x转换为接口类型本身是要装箱的 IComparable<Int32> c = x; // CompareTo方法本来就接受int类型,所以 y不需要装箱 c.CompareTo(y); // 类型安全,编译不通过 c.CompareTo("2"); }
- 类可以实现一个接口若干次
如同时实现Int32的IComparable和string的IComparable
5.5 泛型和接口约束
- 可以将泛型约束为多个接口
public static class SomeType { private static void Test() { Int32 x = 5; Guid g = new Guid(); // 对M调用能通过编译,因为Int32实现了IComparable和IConvertible M(x); // 编译时错误,因为Guid只实现了IComparable,没有实现IConvertible M(g); } private static Int32 M<T>(T t) where T : IComparable, IConvertible { Console.Write(t); return 0; } }
- 传递值类型减少装箱次数
C#编译器为接口约束生成特殊IL指令,导致直接在值类型上调用接口的方法而不装箱。不用接口约束便其他方法让C#编译器生成这些IL指令。 如果值类型实现了一个接口方法,在值类型的实例上调用这个方法不会造成值类型的实例装箱。
6 参考资料
- 抽象类和接口的区别,使用场景
- 为什么要用接口(从编程角度)
- 《CLR via C#》