Java -- 抽象类与接口的思考

最近在极客时间学习设计模式时,回顾了下抽象类与接口的设计意义,以及如何决定什么时候使用抽象类或者接口,写了两三年代码,也算有点想法,本文分享下自己的观点。

接口与抽象类的意义

首先两者本身就很类似,尤其是JDK8之后,接口增加了default方法,允许接口中有部分实现逻辑,这个阶段虽然大多数接口与抽象类都可以互相替换,但他们的侧重点不同。

  • 抽象类
    • 描述的是is - a关系
    • 侧重点在于代码复用,比如大多数Abstract/Base开头的类
    • 抽象类更多的表示某一类事物,比如人,分为教师,工人等等,描述面向对象编程中对象关系,底层仍要关心这个是哪一类对象
  • 接口
    • 描述的是has - a关系或者说 behavior like更形象
    • 侧重点在于解耦,解耦的方式则是利用接口定义规范制约实现
    • 接口更多的标识某一行为,比如排序接口,底层可以不关心实现

什么时候用抽象类,什么时候用接口

了解上述区分点后,对于该问题,应该要从实现考虑,当你描述的关系为is - a时,那么抽象类更加合适,如果has - a时,那么接口合适。往往大多数时候两者都具备,所以经常见到三层继承结构,最上层是接口,中间抽象类,叶子节点为实现类,比如Mybatis中Executor实现,最上层接口Executor标识has -a 关系,表示Executor是一个执行器,有着这些操作方法。中间BaseExecutor侧重于代码复用,描述的是is - a关系,表示这一类实现都是Executor,方便子类实现。

如何理解面向接口编程

这种问题要从接口的意义来讨论,接口的意义在于解耦,解耦的是什么呢?自然是规范与实现,接口本身可以认为是稳定的规范,实现逻辑则认为是不稳定的实现,面向接口编程本质目地是依赖稳定,屏蔽不稳定。

面向接口编程,对于开发人员有一定要求,接口作为稳定的存在,则要求开发人员定义接口时,要充分屏蔽相关实现细节,比如要定义一个上传文件接口,那么upload(target),比uploadAliyun()更加合适,因为其没有包含细节,开发人员需要具有接口意识。

如何理解多用组合少用继承

常见的编程原则中有组合优于继承这一说法,在《Effective Java》中也有类似说法,作者举出了一个继承HashMap的例子,因为实现类不了解HashMap中addAll方法调用了add方法,导致复写时出现重复计算,来表明继承的缺陷,近而引出了组合设计思路。在复杂多层次的继承关系描述上,组合能够让结构更加简单,没有了复杂的对象关系存在,开发人员更加容易理解业务,这是组合优于继承的一点。
但是继承真的该被抛弃吗?当然不是,我们使用的都是面向对象语言,写的都是面向对象代码,继承能很好的描述对象之间的关系,组合却很难做到这一点,这是继承的优势,那问题到底出在哪里?问题的本质原因是继承的滥用,比如存在复杂的继承关系树,甚至关系网,继承本意是让你可以更快的了解对象关系,但是这种关系网的存在反而带来的理解障碍,那么此时继承就是不合理的实现方式。再比如类似HashMap这种不是为继承设计的类,但有业务去继承,一不小心就会因为对父类中实现不了解而踩坑。因此对于继承,一般策略是要么专门为继承设计,要么不使用继承,如果使用继承,那么层次不要超过3层

这一点很好理解,在现在框架中有很多类似的做法,比如Abstract/Base开头的类在Spring中经常出现,如下图所示,AbstractThemeResolver就是专门为继承设计的类,这种的好处很明显,结构简单,叶子节点都是实现类,非叶子节点都是抽象类或者接口,关系也简单明了。因此我们在设计时,应尽量遵循树根为接口,中间非叶子节点为抽象类,叶子节点永远是具体实现类方式来设计,让自己的代码变得更加清晰。

编程没有统一的标准,如果有什么想法欢迎评论区讨论

实践 -- 项目日志配置经验分享
设计模式 -- 享元模式的思考