前段时间逛到了一个老博客,里面举了一个例子,仔细回味了下,真是通俗易懂,对于个人去理解接口和抽象类的区别有了质的提升。例子大致如下:
开发中涉及到一个关于“门”的抽象概念,并且具有“开门”和“关门”两个动作,可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:
public abstract class Door {
abstract void close();
abstract void open();
}
或者
public interface Door {
void open();
void close();
}
此时对于实际具体的”门“,可以通过继承abstract class Door或者实现interface Door,看起来好像使用abstract class和interface没有大的区别,都可以满足诉求。假设随着功能的迭代,出现了具备报警功能的门,也就是门需要具备报警相关的功能,此时又该怎么去设计:
public abstract class Door {
abstract void close();
abstract void open();
// 报警
abstract void alarm();
}
或者
public interface Door {
void open();
void close();
// 报警
void alarm();
}
如果直接在abstract class Door或者interface Door中新增报警方法,此时具有报警功能的AlarmDoor的定义方式如下:
public class AlarmDoor implements Door {
@Override
public void open() {
}
@Override
public void close() {
}
@Override
public void alarm() {
}
}
或者
public class AlarmDoor extends Door {
@Override
public void close() {
}
@Override
public void open() {
}
@Override
public void alarm() {
}
}
这样设计存在一个问题:并非所有的门都有报警的功能或者都需要去感知报警这个功能,但按照目前的设计方式,似乎把Door概念本身固有的行为方法“开门”和“关门”与另外一个概念"报警"的行为方法混在了一起,对于那些仅仅需要依赖Door概念本身固有的行为方法的模型,就会被迫去感知“报警”这个行为,违反了面向对象设计中的一项重要原则:ISP原则,即接口隔离原则(Interface Segregation Principle)
ISP原则: 避免使用单一的大接口,而是将接口拆分成多个专门的小接口,每个接口只提供特定的一组相关功能。这样做的好处在于可以降低类之间的耦合度,提高类的高内聚性和低耦合性,从而提高软件的可维护性和可扩展性。具体来说,ISP原则的实施包括以下几点:
- 避免使用单一的大接口:应该将一个庞大的接口拆分成多个专门的小接口,每个接口只包含使用方感兴趣的方法,这样可以使接口更加清晰和易于管理
- 最小接口依赖:一个类对另一个类的依赖应该建立在最小的接口上,即不应该强迫使用方依赖于它们不使用的方法。这有助于减少不必要的依赖,提高系统的灵活性。
- 角色分离:一个接口代表一个角色,不应该将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
既然觉得【把Door概念本身固有的行为方法“开门”和“关门”与另外一个概念"报警"的行为方法混在了一起】,那么根据ISP原则,应该把它们分别定义在代表这两个单独概念的抽象类中。定义方式有:
- 这两个概念都使用abstract class方式定义
- 两个概念都使用interface方式定义
- 一个概念使用abstract class方式定义,另一个概念使用interface方式定义
由于Java不支持多重继承,只支持继承一个类,所以两个概念都使用abstract class方式定义是不可行的,后面两种方式都是可行的。对于这两者的选择,其实是看我们怎么去界定”Door和AlarmDoor“的关系,思考这两个问题:
- AlarmDoor本质上是不是Door?
- 报警是否是Door本身所固有的功能?
问题的答案显而易见: AlarmDoor本质上也是Door,且报警并非是Door本身所固有的功能。综合我们可以得到下面的结论:AlarmDoor也是Door,只不过其具有报警功能。
- 【AlarmDoor也是Door】说明两者是“is a“的关系,所以对于Door这个概念,我们应该使用abstarct class方式来定义;
- 【具有报警功能】说明AlarmDoor又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。
结合以上分析,最终实现如下:
- 定义一个抽象类Door,具有开门和关门的行为方法
public abstract class Door {
public abstract void close();
public abstract void open();
}
- 定义一个接口Alarm,具有报警相关的行为方法
public interface Alarm {
void alarm();
}
- 定义一个类AlarmDoor,继承抽象类Door,实现接口Alarm
public class AlarmDoor extends Door implements Alarm {
@Override
public void close() {
}
@Override
public void open() {
}
@Override
public void alarm() {
}
}
当初看到这个例子的时候,确实帮助自己进一步的理解了抽象类和接口的一些本质区别,可见abstract class和interface所反映出的设计理念确有不同,其实这两者不仅在设计理念上有所不同,还有如下的异同点:
- 抽象类和接口都不能够实例化
- 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
- 接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法
- 抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的
- 抽象类中可以定义自己的成员变量,而接口中定义的成员变量实际上都是静态常量
- 有抽象方法的类必须被声明为抽象类,而抽象类中未必要有抽象方法
- 一个类只能继承一个抽象类但是可以实现多个接口
- 接口中的方法只能是public abstract类型的,接口中的成员变量只能是public static fianl类型的