再探Java抽象类与接口的设计理念差异

Java抽象类与接口都可以实现功能与实现的分离,都对多态提供了很好的支持,那么我们什么时候应该使用抽象类或接口呢?在以前的一篇文章初探Java抽象类与接口中谈到了他们语法的区别,在博客通过模板方法模式深入理解Java抽象类中写到了该如何正确使用抽象类,那么这次我就从更高的层次上——设计思想 上谈谈它们的差异!

1、抽象类与接口的抽象层次是不同的
抽象类是对类抽象,接口是对行为抽象。类包含了属性与行为,所以说接口是更具体的抽象。

2、抽象类与接口的设计层次是不同的
抽象类是一种自下而上的设计,先有子类才能提取公同的属性与行为,抽象出父类;
接口是一种自上而下的设计,先规定行为方法,只要可以实现这些行为,就可以成为接口的实现类。

3、抽象类与其派生类的关系和接口与其实现类的关系本质是不同的
抽象类与其派生类是一种“is-a”关系,即父类和派生子类在概念上的本质是相同的(父子关系,血缘联系,很亲密)。
接口与其实现类是一种“like-a”关系,即接口与实现类的关系只是实现了定义的行为,并无本质上的联系(契约关系,很淡漠的利益关系)。

举个例子:比如说一个动物抽象类,定义了跑的方法、叫的方法,但如果一个汽车类可以实现跑、可以实现叫,它就可以继承动物抽象类吗?!这太不合理了,汽车不是动物呀!而如果通过接口定义跑的方法、叫的方法,汽车类作为实现类实现跑和叫,完全OK很合理,就因为没有继承关系的约束。

为了更好的阐述他们之间的区别,下面将使用一个很棒的例子来说明。该例子引自博文链接。

我们有一个Door的抽象概念,它具备两个行为open()和close(),此时我们可以定义通过抽象类和接口来定义这个抽象概念:

抽象类:

abstract class Door{  
    abstract void open();  
    abstract void close();  
}  

接口:

interface Door{  
    void open();  
    void close();  
}  

至于其他的派生类可以通过:
1、使用extends使用抽象类方式定义Door
2、使用implements接口方式定义Door

这里发现两者并没有什么很大的差异,但是现在如果我们需要门具有报警的功能,那么该如何实现呢?

解决方案一:给Door增加一个报警方法:alarm();

abstract class Door{  
    abstract void open();  
    abstract void close();  
    abstract void alarm();  
}  

或者

interface Door{  
    void open();  
    void close();  
    void alarm();  
}  

这种方法违反了面向对象设计中的一个核心原则 ISP (Interface Segregation Principle)—见批注,在Door的定义中把Door概念本身固有的行为方法和另外一个概念”报警器”的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为”报警器”这个概念的改变而改变,反之依然。

解决方案二
既然open()、close()和alarm()属于两个不同的概念,那么我们依据ISP原则将它们分开定义在两个代表两个不同概念的抽象类里面,定义的方式有三种:
1、两个都使用抽象类来定义。
2、两个都使用接口来定义。
3、一个使用抽象类定义,一个是用接口定义。

由于java不支持多继承所以第一种是不可行的。后面两种都是可行的,但是选择何种就反映了你对问题域本质的理解。

如果选择第二种都是接口来定义,那么就反映了两个问题:
1、我们可能没有理解清楚问题域,AlarmDoor在概念本质上到底是门还报警器。
2、如果我们对问题域的理解没有问题,比如我们在分析时确定了AlarmDoor在本质上概念是一致的,那么我们在设计时就没有正确的反映出我们的设计意图。因为你使用了两个接口来进行定义,他们概念的定义并不能够反映上述含义。

第三种,如果我们对问题域的理解是这样的:AlarmDoor本质上Door,但同时它也拥有报警的行为功能,这个时候我们使用第三种方案恰好可以阐述我们的设计意图。AlarmDoor本质是们,所以对于这个概念我们使用抽象类来定义,同时AlarmDoor具备报警功能,说明它能够完成报警概念中定义的行为功能,所以alarm可以使用接口来进行定义。如下:

abstract class Door{  
    abstract void open();  
    abstract void close();  
}  

interface Alarm{  
    void alarm();  
}  

class AlarmDoor extends Door implements Alarm{  
    void open(){}  
    void close(){}  
    void alarm(){}  
}  

这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实抽象类表示的是”is-a”关系,接口表示的是”like-a”关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。

批注: ISP(Interface Segregation Principle):面向对象的一个核心原则。它表明使用多个专门的接口比使用单一的总接口要好。
一个类对另外一个类的依赖性应当是建立在最小的接口上的。
一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。

你可能感兴趣的:(Java)