设计思想:接口与抽象类解决的痛点

1,604 阅读5分钟

在面对对象编程中,抽象类和接口会被经常用到的俩个概念,是编程实现的基础。基于接口,可以实现对象的抽象、对象的多态、以及基于接口而非实现的设计原则等等;基于抽象类,可以实现对象的继承特性、模板设计模式等等。不过,并非所有面对对象语言都支持这俩语法概念,e.g. C++只支持抽象类,不支持接口;Python 不支持抽象类,也不支持接口。

虽然抽象类和接口经常被用到,也被经常提及,但是遇到选型时却不知所措。e.g. 接口和抽象类的区别是什么? 啥时候应该用接口?啥时候应该用抽象类?接口解决了什么编程问题? and so on ...

如果你对上面的问题不知道怎么回答,你可以往下看,否则,你可以直接返回主页看其他大大的文章了。

什么接口?什么是抽象类?俩者区别又是什么呢?

什么是接口呢? 我们先通过一个例子来了解一下接口。

public interface EventListener {
    /**
    * 监听事件
    */
    void onEvent(Event e);
}

public class LoginEventListener implements EventListener {
    @Override
    public void onEvent(Event e) {
        // 只处理登录事件
        if(!(e instanceOf LoginEvent)) {
            return;
        }
        
        // ...
    }
}

public class LogoutEventListener implements EventListener {
    @Override
    public void onEvent(Event e) {
        // 只处理登出事件
        if(!(e instanceOf LogoutEvent)) {
            return;
        }
        
        // ...
    }
}

上面的例子是比较典型的使用场景,定义了事件监听器接口,登录事件监听器、登出事件监听器分别对登录和登出事件分别监听。

从上面的例子,我们可以看到接口有以下特点:

  • 接口不能定义字段;(java 从1.8开始,支持)
  • 接口不能定义方法实现,只能声明方法(java 从1.8开始,支持 default 方法,可定义函数体);
  • 非抽象类实现接口时,必须实现接口声明的所有方法;

接下来,我们再看看抽象类。我们改一下上面的事例代码:

public interface EventListener {
   /**
   * 监听事件
   */
   void onEvent(Event e);
}

public abstract class AbstractConditionalEventListener implements EventListener {
    private Logger logger = LogFactory.getLogger(AbstractConditionalEventListener.class);

   @Override
   public final void onEvent(Event e) {
      logger.debug("....");
      
      if(match(e)) {
         resolveEvent(e);
      }
   }

   /**
   * 处理事件
   */
   protected abstract void resolveEvent(Event e);

   /**
   * 匹配事件是否需要处理
   */
   protected abstract boolean match(Event e);
}

public class LoginEventListener extends AbstractConditionalEventListener {
   @Override
   public void resolveEvent(Event e) {
      // ...
   }

   @Override
   public boolean match(Event e) {
      return e instanceof LoginEvent;
   }
}

public class LogoutEventListener extends AbstractConditionalEventListener {
   @Override
   public void resolveEvent(Event e) {
      // ...
   }

   @Override
   public boolean match(Event e) {
      return e instanceof LogoutEvent;
   }
}

上面的例子,是对前面的例子做更上一层的丰富,AbstractConditionalEventListener 实现了条件化匹配事件的事件监听器,拆分了处理事件的逻辑到方法 resolveEvent,匹配事件到方法 match。LoginEventListener、LogoutEventListener 各自覆写了处理事件方法 resolveEvent 、匹配事件方法 match。

我们可以看到抽象类有以下特点:

  • 抽象类不允许实例化,只能被继承;
  • 抽象类可以定义属性和实现方法体;
  • 抽象类被子类继承,若子类非抽象类,则必须实现所有抽象方法和接口方法;

聊了那么多,那到底接口和抽象类有什么区别呢?

语法上的区别,可以通过上面的特点,XDM都可以看到区别,这里就不说太多了。

除此之外,接口和抽象类还存在语义上的区别。

继承,是一种 is-a 的关系。比方说,猫是哺乳动物,为什么呢?因为猫具备了哺乳动物的特征,因此猫继承了哺乳动物,并在哺乳动物的基础上,衍生了属于猫的特征,比如会爬树。

而接口,是一种 has-a 的关系,意在具有某些能力,也可以认为是一种协议。比方说,电脑主板有很多接口,CPU插座接上CPU后为电脑提供运算能力,存储器插口接上内存条后为电脑提供运行内存能力等等。电脑主板上各式各样的接口,表明它有各式各样的功能,至于能力的强弱,在于怎么实现(如内存使用2G还是32G)。

抽象类解决了什么编程的痛点呢?

继承是面对对象编程的特点之一,解决了 代码复用 的问题,在这里,抽象类也同样是。多个子类可以继承抽象类中定义的属性和方法,避免重复的代码。不过,这里并不要求父类为抽象类,同样可以实现。

除了代码复用外,抽象类与继承协作,还能实现 多态。多态提高了代码的复用性和扩展性,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。关于多态可以看看这篇文章总结

接口解决了什么编程的痛点呢?

抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。

实际上,接口是一个比抽象类应用更加广泛、更加重要的知识点。比如,我们经常提到的“基于接口而非实现编程”,就是一条几乎天天会用到,并且能极大地提高代码的灵活性、扩展性的设计思想。

抽象类和接口,我们应该怎么选择呢?

判断的标准非常简单。如果是 is-a 关系并且解决代码复用问题,应该选择抽象类。如果表示的是 has-a 的关系并且解决抽象问题,我们可以选择接口。

从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。