OOP设计思考——何时使用接口?
一、“接口”初步:
从OOP设计角度来分析,“接口”是一个很特殊的“类”,特殊之处在于:
1)不能为接口中定义的方法添加关键字(只允许是public)。
2)可以定义事件/方法/属性,但都无法实现(只能让实现类去实现)。
以上这些限制了接口的应用范围——意味着接口只是作为一个对类的契约而存在,谈不上“代码复用”(因为接口自身不能定义方法,自然实现类无法调用它们,也就不构成重载/重写的概念了),也根本不存在“继承”的概念。因此“接口”的关键字是Implement而不是Inherit(在C#中则都是英文状态的冒号)。
由接口自身的定义,我们联想到最简单的接口应用就是强制某个类必须具备某种方法,譬如说我要设计一个围棋程序,其中的“棋子”如果是一个类,那么星罗密布的围棋子其实都是自身的拷贝和翻版,因此我可以实现NET自带的IClonable类;又如NET自带的数据库类中由于需要消耗大量内存和磁盘开销,因此及时释放内存很有必要;此时我们可以实现IDispose接口来强制让这个类带有Dispose方法,用于销毁内部消耗大量资源的托管和非托管组件和实例等东西。
其次,实现了一个特定接口的类可以通过接口来调用。这就意味着对于客户端而言,无需知道接口内具体方法是如何实现的,只要知道能够实现这个效果是哪个类,直接用接口引用这个类的实体就可以了。这就是我们所谓的“面向接口编程”。NET一些特定场景下大量使用到接口,譬如最典型的泛型List中Sort方法就是传入一个实现了IComparer<T>的接口——只要你把实现了这个接口的实例当成参数传入到此方法中,则Sort内部自动调用你的方法排序,相对于NET类库而言方法是黑箱的:它只需要结果,根本不关心过程。这种“屏蔽实现端,让客户只知道该知道的东西的”的设计理念还广泛地应用于各类OOP设计模式中——比如工厂模式,适配器模式等。
让子类去实现具体方法并非“接口”之专利,“抽象类”的abstract方法也是延时实现。那么它们有何区别呢?
二、“接口”VS“抽象类”:
这个题目简直每次面试都被问及,但要真正说清楚它们的区别还真的非常不容易!前面已经说过——抽象类是类的抽象表述(不可实例化,一般具备一个或多个abstract方法让子类去实现),因此抽象类主要的作用在于尽可能提高“代码复用”(包括方法继承以及方法重载/重写),而且一个抽象类往往衍生出多个具备相似功能的子类,因而抽象类是衍生其它一系列类的通用模板。
相比较而言接口非常单纯,“城府”远没有“抽象类”那么“深”:简而言之,接口作用只是让某个类拥有某个方法,这个方法是对外公开暴露的,也就是可以为其它拥有该接口的类随意调用,可以使得两个毫不相干的类在不引用的情况下发生特殊的关系——最经典的例子莫过于“设计模式”中的“观察者模式”,微软由此基础上发展出“事件”来取代这种模式,但是它的的确确是“事件”的开山鼻祖。
同时,抽象类衍生出的子类必然具备抽象类允许继承的全部方法(即便是重写!),但是有时并不都是需要这些方法时(比如通过“鸭子工厂”介绍策略模式就是一个典型),接口往往可以把变化的方法和不变的隔离,达到动态的效果。
谈到策略模式,其实“策略模式”的本质是通过一个基本的策略工具衍生出若干个相关的策略算法,然后在其它类中引用“策略工具”,由不同实现该工具的“策略算法”决定如何进行策略的一种设计模式。“策略工具”如果说只是一个单纯的“算法”,那么完全可以使用接口;如果是一个复杂的,乃至这个“策略工具”可以派生出大量类似的“策略算法”的,优先考虑的是抽象类而不是接口。