首页 接口和抽象类
文章
取消

接口和抽象类

1. 接口和抽象类的特性

抽象类

  1. 抽象类不允许被实例化,只能被继承
  2. 抽象类必须有抽象方法
  3. 子类继承抽象类必须实现抽象类中的所有抽象方法

接口

  1. 接口只能定义常量不能定义属性
  2. 接口只能声明方法,不能实现方法
  3. 实现接口的类必须实现接口中声明的所有方法

接口和抽象类的不同点

  • 语法特性:相比抽象类,接口中不能定义属性,不能实现方法。
  • 设计角度:抽象类和子类表示的是 is-a 关系,接口和实现类表示的是 has-a 关系,即具有某些功能。接口的更形象的称呼:协议(contract)

接口和抽象类的意义

  1. 相比普通类,抽象类除了可以实现代码复用,亦可以实现多态,其中模板设计模式就是以抽象类为基础,从设计上而言很有美感。如下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//抽象类
public abstract class Logger {
    private String name;
    private boolean enabled;
    private Level minPermittedLevel;

    public Logger(Stringname,booleanenabled,LevelminPermittedLevel){
        this.name=name;
        this.enabled=enabled;
        this.minPermittedLevel=minPermittedLevel;
    }

    public void log(Level level, String message){
        boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
        if(!loggable) return;
        doLog(level, message);
    }

    protected abstract void doLog(Levellevel,Stringmessage);
}
//抽象类的子类:输出日志到文件
public class FileLogger extends Logger{
    private Writer fileWriter;
    public FileLogger(String name,boolean enabled,Level minPermittedLevel,String filepath){
        super(name, enabled, minPermittedLevel);
        this.fileWriter = newFileWriter(filepath);
    }

    @Override
    public void doLog(Level level, String mesage){
        //格式化level和message,输出到日志文件
        fileWriter.write(...);
    }
}
//抽象类的子类:输出日志到消息中间件(比如kafka)
public class Message QueueLogger extends Logger{
    private MessageQueueClient msgQueueClient;

    publicMessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient msgQueueClient){
        super(name, enabled, minPermittedLevel);
        this.msgQueueClient = msgQueueClient;
    }

    @Override
    protectedvoiddoLog(Level level, String mesage){
        //格式化level和message,输出到消息中间件
        msgQueueClient.send(...);
    }
}
  1. 相比抽象类,接口更多用于实现多态特性,用于解耦,提高程序的可扩展性。如下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//接口
public interface Filter{
    void doFilter(RpcRequest req) throws RpcException;
}

//  接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {
  @Override
  public void doFilter(RpcRequest  req)  throws  RpcException  {/**...鉴权逻辑..**/} 
}
//  接口实现类:限流过滤器
public  class  RateLimitFilter  implements  Filter  {
    @Override
    public  void  doFilter(RpcRequest  req)  throws  RpcException  {/**...限流逻辑...**/}
}
//  过滤器使用Demo
public  class  Application  {
    private  List<Filter>  filters  =  new  ArrayList<>();
    filters.add(new  AuthencationFilter());
    filters.add(new  RateLimitFilter());
    public  void  handleRpcRequest(RpcRequest  req)  {
        try  {
            for  (Filter  filter  :  filters)  {
                filter.doFilter(req);
            }
        }  catch(RpcException  e)  {/**...处理过滤结果...**/}
        //  ...省略其他处理逻辑...
    }
}

2. 什么时候用接口?什么时候用抽象类?

  1. 实际上,判断的标准很简单。如果我们要表示一种is-a的关系,并且是为了解决代码复用的问题,我们就用抽象类;如果我们要表示 一种has-a关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。
  2. 从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。

3. 设计原则:基于接口而非实现编程

首先这条原则是先于 Java 诞生的,因而此 ‘接口’ 非彼‘接口’,实际上,该原则的另一个表述方式是“基于抽象而非实现编程”,这样更能体现该原则的设计初衷。

理解接口的定义

“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。

设计初衷

将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。

如何应用这条原则

  1. 函数的命名不能暴露任何实现细节。
  2. 封装具体的实现细节
  3. 为实现类定义抽象的接口。

4. 多用组合少用继承

什么是组合

参考监听者模式,被监听者类里组合监听者作为它的一个属性,这就是组合

为什么多用组合少用继承

继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性,如下面这张继承图:

该设计旨在抽象现实中的鸟类,而现实往往千变万化,比如除会飞的鸟、不会飞的鸟,还有下蛋的不下蛋的,食肉的和不食肉的等等,这样就会导致类继承关系很复杂,类的数量也会急剧增加

使用方法

实际上,我们可以利用组合(composition)、接口、委托(delegation)三个技术手段一块儿来解决刚刚继承存在的问题。像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Flyable {
  void fly()
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { /**...**/ }
}
public class Ostrich implements Flyable {//鸵鸟
  private FlyAbility flyAbility = new FlyAbility(); //组合
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委托
  }
}

替换组合

继承主要的三个作用都可用其他手段替换:

  1. 表示is-a关系,可通过组合和接口的has-a关系来替代
  2. 支持多态特性,可以利用接口来实现
  3. 代码复用,可以通过组合和委托来实现

少用继承不是完全不用继承?

继承改写成组合意味着要做更细粒度的类的拆分,即要定义更多的类和接口。类和接口的增多必然增加代码的复杂程度和维护成本。要根据具体的情况,来选择继承还是组合。如:

  1. 类继承关系最多2层,就大胆用继承
  2. 类与类之间没有继承关系,即为了实现代码复用没必要抽象一个父类,这时候就大胆用组合吧(搞一个工具类啥的)
  3. 函数参数强制为类而不是接口,这个时候为了实现多态就只能使用继承重写对应的方法
本文由作者按照 CC BY 4.0 进行授权

面向对象编程与面向过程编程

基于贫血模型的MVC三层架构和基于充血模型的DDD分层架构