关于在Interface和Abstract Class间选择的一些思考

本文系笔者在学习软件构造课程期间所写,不保证通用性和正确性,仅供参考。
基于课程要求,本文所涉及语言为Java。

目录

  1. 前言
  2. 接口:组件思想
  3. "Composition over Inheritance"
  4. 何时选择继承类
  5. 结语

一、前言与简要介绍

在学习软件构造课程之前,自己写代码遇到需要复用类中功能时,基本上都是优先选择继承类的,而接口对我来说只有“添加某个小功能”的作用,比如在一个“电脑”的类中加入"USB插口"的功能。

然而在课程中,对实现一个有向图这样比较“大”的功能也选择了用接口来作为“基类”,这与我之前的思维有所冲突,于是决定研究一下接口与基类到底有什么差别,什么时候用谁比较好。

那么以下是对二者异同的介绍(因为重点不在这里,所以只做简要介绍,并不全面和严谨,具体请参考官方文档):

共同点:

  • 都不能被直接实例化;
  • 都可以在其中定义抽象方法;
  • 都可以定义有具体实现的方法(JDK1.8以后),不过代码中的体现方式不一样。
  • 都可以定义静态方法与变量。(注意关键字static与default、abstract冲突)
  • 都可以定义变量。

不同点:

  • 在Java中,接口可以多继承,而抽象类只能单继承。
  • 方法上:接口中的方法默认都是抽象方法,默认不能有方法体,如果想要有方法体,则需要在方法前加上default修饰(并且只有接口中才有这种修饰)。接口中的方法只能是public。而抽象类中的方法默认与普通类的方法没什么不同,只有在不需要写实现的时候声明一下abstract。可以看到,某种意义上二者在抽象方法上的思路是正好相反的。
  • 变量上:接口中的变量默认都是public static final,而且只能是public,必须在声明时进行赋值。这里的思想大概是定义一个常量,但是如果声明的是可变类型的话实际上就没什么意义了(所以一般也不会在接口中大量声明变量)。而抽象类中的变量同样与普通类没什么不同。

二、接口:组件思想

说回来,接口的思想当然不只是添加一个小功能,那么它体现的思想是什么呢?我认为主要有以下两点:

  1. 作为一系列类的大规约,相当于声明了API。即一个类只要实现了这个接口定义的这些方法,那么理论上它就可以胜任这个接口所期待可完成的任务。这样的好处一是可以一眼就明白所有继承接口的类都有什么基本功能,二是一个类可以实现多个这样的接口,从而扮演多个角色。
  2. 作为组件存在。这一点就相当于之前提到的“添加小功能”了。有的接口可以不必具有一套完整的功能,而只是相当于更通用的组件,或者说插件一般,插在一个类上,它就可以做这样一些功能。

事实上,接口反映的这种组件思想在代码中的应用相当广泛。举我最近在学习的游戏引擎为例,为游戏物体添加功能也是一种典型的组件思想。

例如,在Unity中,通过添加组件(Component)为游戏物体添加功能:

Unity

如上图中的这个物体,就同时具有Rect Transform、Animator和自己编写的BottomInfo这三个组件所赋予的功能。

在最近比较火的Godot中,更是发挥了组件思想的特点,对组件(脚本)进行了可视化的界面设计,将脚本本身也拆分成了可视化的组件。(我还没用过呢就不多说了免得出错x

图片来自Godot官方文档

从某种意义上来说,函数库的抽象和封装也可以看作组件思想的一种体现,在代码中引入了某个库,就相当于给它装上了这个组件,它就具备了这个组件所提供的功能。

说到这倒不如反过来:接口的组件思想其实是这种更大层面上的“组件思想”在代码层面上的一种体现,只不过接口本身一般情况下并不具备真正的功能,而只是定义了该有的功能而已。

三、"Composition over Inheritance"

于是便不得不谈到一句老话:“Composition over Inheritance.”就是说,总是偏向于优先选择组件而非继承。为什么呢?下面举一个案例来体现继承中可能遇到的问题,以及接口如何巧妙地化解这一问题。

现在,假设你手头有一个GUI库,正在构建一个GUI界面。你想向界面中添加一个按钮,这需要用到库中已经封装好的Button类。不仅如此,该库还提供了丰富的按钮变种,比如MenuButton(菜单按钮)、SilderButton、DropdownButton等。现在甲方希望在菜单中添加一个按钮,它有个独特的需要你自己写的功能,比如按下之后有特效。

如果使用继承的话,直观的思路就是新开一个类继承MenuButton,比如命名为SpecialMenuButton吧。

public class SpecialMenuButton extends MenuButton{
    public void SpecialEffectOnClick(){...}
}

它很好地完成了它的使命,没啥毛病。

现在甲方希望所有的按钮都要有这种特效。那思路似乎也挺清晰的,新开一个类直接继承Button类就好了嘛。

public class SpecialButton extends Button{
    public void SpecialEffectOnClick(){...}
}

可是问题来了,如果之后甲方希望SliderButton、DropdownButton也有这种特效,那就用不了SpecialButton了,因为它并不具备Slider和Dropdown的功能。
如果继承这两个类,因为SpecialButton跟它们又没有什么关系,那又得重新写一遍产生特效的方法,久而久之,重复的代码会越来越多,各种功能排列组合的类也越来越多,整个代码就会变得越来越冗余。

继承的思想这时就不好用了。那么看看接口如何解决:

public interface HaveSpecialEffect{
    // void SpecialEffectOnClick();
    default void SpecialEffectOnClick(){...}
}

之后想要一个按钮有特效只要实现这个接口就行了,甚至如果接口里已经写好了默认方法体,那么就真正成为了一个即插即用的组件了。

其实对于上面这个问题,解决的方法有很多,纯接口也不是最好的解决方法,其他的解决方法有定义一个方法,它传入一个按钮类,并控制按钮产生特效;或者使用如Decorator等的设计模式。总之还是跳出了继承的思想,从而避免了无用的重复。

四、何时选择继承类

既然如此,那么抽象类还有什么存在的意义呢?什么时候才会选择继承一个抽象类?

一是在写具体实现的时候。Java中,现在接口虽然可以定义默认的方法体,但本质思想也只是起一个规约的作用。如果类里有复杂的方法,比如一个public的方法得调用好几个private的方法,那用接口写起来就比较难受了。此外,接口里定义的变量都是static的,也不便于写复杂的实现。
这时,在抽象类里写好实现,选择性地留一点小的抽象方法留给继承类发挥空间,就很方便了。这里的抽象类更多地已经可以成为一个普通的类,只不过不希望它被实例化而已。

二是在框架结构很清晰的时候。上文提到的按钮的例子,未来的要求还有很多不确定性,但是如果已经可以预见未来在框架上不会有大的变动时,一些抽象类可以一定程度上简化代码布局:通用的方法就可以直接在抽象类里写完了。

一句话,接口更偏向抽象,抽象类更偏向实现。

五、结语

总结下来,我的感受是其实接口和抽象类的区别更多是在思想上,而非语言的定义上,比如在C++中类也可以多继承,接口和抽象类的差异就更小了。

除了选择好接口和抽象类之外,它们各自或二者结合还可以衍生出很多设计模式,这些设计模式又可以填补单用接口或单用继承的不足,因此更加体现了这种接口和抽象类间的比较更多是在思想上而非具体实践中。

这篇文章其实早该发了,但是一是懒二是觉得该尽量写的正确性实用性高一点,所以一直拖到课都上完了才发......然而写完并不自觉有多好,相反,感觉局限性挺大,而且思路比较跳跃。作为课堂任务来说算是有自己的思考了;作为一篇正经的博文来说,不敢说有什么用,如果能帮到你那真是再好不过了。

posted on 2024-05-04 11:03  Senolytics  阅读(10)  评论(0编辑  收藏  举报