面向对象编程02:接口与抽象类

一、接口和抽象类

众所周知,我们可以使用接口来实现面向对象的抽象特性多态特性基于接口而非实现的设计原则,使用抽象类来实现面向对象的继承特性和模板设计模式等等。

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
50
51
52
// 抽象类
public abstract class Logger {
  private String name;
  private boolean enabled;
  private Level minPermittedLevel;

  public Logger(String name, boolean enabled, Level minPermittedLevel) {
    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(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {
  private Writer fileWriter;

  public FileLogger(String name, boolean enabled,
    Level minPermittedLevel, String filepath) {
    super(name, enabled, minPermittedLevel);
    this.fileWriter = new FileWriter(filepath); 
  }
  
  @Override
  public void doLog(Level level, String mesage) {
    // 格式化level和message,输出到日志文件
    fileWriter.write(...);
  }
}
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
  private MessageQueueClient msgQueueClient;
  
  public MessageQueueLogger(String name, boolean enabled,
    Level minPermittedLevel, MessageQueueClient msgQueueClient) {
    super(name, enabled, minPermittedLevel);
    this.msgQueueClient = msgQueueClient;
  }
  
  @Override
  protected void doLog(Level level, String mesage) {
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

在上面这段代码中,Logger是一个记录日志的抽象类,而FileLogger和MessageQueueLogger分别是两个子类,用于记录日志到文件和记录日志到消息队列。这两个子类复用了父类name、enabled、minPermittedLevel属性和log方法,但是因为这两个子类写日志的方式不同,它们又各自重写了父类中的doLog()方法。

我们可以看到,抽象类有以下几个特性:

  • 抽象类不允许被实例化,只能被继承;
  • 抽象类可以包含属性和方法。其中方法可以包含代码实现,也可以不包含,不包含代码实现的叫抽象方法;
  • 子类继承抽象类,必须重写抽象类中的所有抽象方法。

接口

 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
// 接口
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 {
  // filters.add(new AuthencationFilter());
  // filters.add(new RateLimitFilter());
  private List<Filter> filters = new ArrayList<>();
  
  public void handleRpcRequest(RpcRequest req) {
    try {
      for (Filter filter : filters) {
        filter.doFilter(req);
      }
    } catch(RpcException e) {
      // ...处理过滤结果...
    }
    // ...省略其他处理逻辑...
  }
}

以上是一个典型的Filter接口,AuthencationFilter 和 RateLimitFilter 是接口的两个实现类,分别实现了对 RPC 请求鉴权和限流的过滤功能。

接口具有以下特性:

  • 接口不能包含属性(也就是成员变量)
  • 接口只能声明方法,方法不能包含代码实现
  • 类实现接口的时候,必须实现接口中声明的所有方法

接口和抽象类之间的区别

抽象类实际上就是一种特殊的类,这种类不能被实例化,只能被子类继承。如果说抽象类是一种is-a的关系,那么接口就是has-a的关系,表示具有某种功能。

2. 抽象类和接口类能解决什么编程问题?

既然继承本身能实现代码复用,抽象类有什么其他存在的意义吗?

我们改造一下一开始的Logger类,具体代码如下:

 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

// 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable().
public class Logger {
  private String name;
  private boolean enabled;
  private Level minPermittedLevel;

  public Logger(String name, boolean enabled, Level minPermittedLevel) {
    //...构造函数不变,代码省略...
  }

  protected boolean isLoggable() {
    boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
    return loggable;
  }
}
// 子类:输出日志到文件
public class FileLogger extends Logger {
  private Writer fileWriter;

  public FileLogger(String name, boolean enabled,
    Level minPermittedLevel, String filepath) {
    //...构造函数不变,代码省略...
  }
  
  public void log(Level level, String mesage) {
    if (!isLoggable()) return;
    // 格式化level和message,输出到日志文件
    fileWriter.write(...);
  }
}
// 子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
  private MessageQueueClient msgQueueClient;
  
  public MessageQueueLogger(String name, boolean enabled,
    Level minPermittedLevel, MessageQueueClient msgQueueClient) {
    //...构造函数不变,代码省略...
  }
  
  public void log(Level level, String mesage) {
    if (!isLoggable()) return;
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

可以看到,这个代码虽然达到了代码复用的目的,但是以及无法使用多态特性了。所以我们为了能够实现多态,要在Logger类中定义一个log方法,然后子类重写log方法以达到多态的目的,但是这样实现太不“优雅”了,主要原因有以下几点:

  • 影响代码可读性,如果不了解Logger的设计思想,那么很难理解这里为什么会定义一个空的log方法
  • 抽象类会强制子类重写log方法,防止遗漏
  • Logger类如果不是抽象类,是可以被实例化的

下面说说为什么需要接口,接口能够解决什么编程问题?

抽象类更多的是为了代码复用,而接口更侧重于解耦。接口是对行为的一种抽象,调用者只需要关注抽象的接口,而不需要了解具体的实现,实现了约定和实现相分离,降低了代码间的耦合性。

如何决定该用抽象类还是接口?

如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类;如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。

从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。

二、基于接口而非实现编程

在面向对象编程中,有一条非常经典的设计原则,那就是“基于接口而非实现编程”,主要有以下几点:

  • 函数的命名不能暴露任何实现细节
  • 封装具体的实现细节
  • 为实现类定义抽象的接口,具体的实现类都依赖统一的接口定义,使用者依赖接口而非具体的实现类编程
updatedupdated2023-06-032023-06-03
加载评论