0%

抽象类与接口

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 final String name;
private final boolean enabled;
private Level minPermittedLevel;

public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLeve = 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 {
public Writer fileWriter;

public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filepath) throws IOException {
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filepath);
}

@Override
protected void doLog(Level level, String message) {
// 格式化level和message,输出到日志文件
// fileWriter.write(...);
}
}

public class MessageQueueLogger extends Logger {
private final MessageQueueClient msgQueueClient;

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

@Override
protected void doLog(Level level, String message) {
// 格式化level和message,输出到消息中间件
// msgQueueClient.send(...)

}
}
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
// 接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}

// 接口实现类:鉴权过滤器
public class AuthenticationFilter 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 AuthenticationFilter());
// filters.add(new RateLimitFilter());
private final List<Filter> filters = new ArrayList<>();

public void handleRpcRequest(RpcRequest req) {
try {
for (Filter filter : filters) {
filter.doFilter(req);
}
} catch (RpcException e) {
// ...处理过滤结果...
}
// ...省略其他处理逻辑...
}
}

抽象类实际上就是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承。我们知道,继承关系是一种is-a的关系,那抽象类既然属于类是,也表示一种is-a的关系。相对于抽象类的is-a关系来说,接口表示一种has-a关系,表示具有某些工呢过。对于接口,有一个更加形象的叫法,那就是协议(contract)。

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
public class Logger {
private final String name;
private final boolean enabled;
private final Level minPermittedLevel;

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

public boolean isLoggable(Level level) {
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 message) {
if (!isLoggable(level)) return;
// 格式化level和message,输出到日志文件
fileWriter.write(...)
}
}

// subclass:output message to message middleware
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;

public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient msgQueueClient) {
// 构造函数不变
}

public void log(Level level, String message) {
if (!isLoggable()) return;
// formatting level and message, output to message middleware
msgQueueClient.send(...)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Logger {
// ...省略部分代码...
public void log(Level level, String mesage) { // do nothing... }
}
public class FileLogger extends Logger {
// ...省略部分代码...
@Override
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,输出到日志文件
fileWriter.write(...);
}
}
public class MessageQueueLogger extends Logger {
// ...省略部分代码...
@Override
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,输出到消息中间件
msgQueueClient.send(...);
}
}

Logger中并没有定义log()方法。可能会想,在Logger父类中,定义一个空的log()方法,让子类重写父类的log()方法,实现自己的记录日志的逻辑,不就可以了吗?这个设计思路能用,但是,它显然没有之前通过抽象类的实现思路优雅。

抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下API接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。

实际上,借口是一个比抽象类应用更加广泛、更加重要的知识点。比如,我们经常提到的“基于接口而非实现编程”,就是一条几乎天天会用到,并且能极大地提高代码的灵活性、扩展性的设计思想。

如何模拟抽象类和接口两个语法概念?

在前面举的例子中,我们使用Java的接口语法实现了一个Filter过滤器。不过,如果你熟悉的是C++这种编程语言,你可能会说,C++只有抽象类,并没有接口,那从代码实现的角度来说,是不是就无法实现Filter的设计思路了呢?

实际上,我们可以通过抽象类来模拟接口。怎么来模拟呢?

我们先来回忆一下接口的定义:接口中没有成员变量,只有方法声明,没有方法实现,实现接口的类必须实现接口中的所有方法。只要满足这样几点,从设计的角度上来说,我们就可以把它叫做接口。实际上,要满足接口的这些语法特性并不难。在下面这段C++代码中,我们就用抽象类模拟了一个接口(下面这段代码实际上是策略模式中的一段代码)。

1
2
3
4
5
6
7
8
9
class Strategy { // 用抽象类模拟接口
public:
~Strategy();

virtual void algorithm() = 0;

protected:
Strategy();
};

抽象类Strategy没有定义任何属性,并且所有的方法都声明为virtual类型(等同于Java中的abstract关键字),这样,所有的方法都不能有代码实现,并且所有集成这个抽象类的子类,都要实现这些方法。从语法特性上来看,这个抽象类就相当于一个接口。

不过,如果你熟悉的既不是Java,也不是C++,而是现在比较流行的动态编程语言,比如Python、Ruby等,你可能还会有疑问:在这些动态语言中,不仅没有接口的概念,也没有类似abstract,virtual这样的关键字来定义抽象类,那该如何实现上面讲到的Filter、Logger的设计思路呢?实际上,除了用抽象类来模拟接口之外,我们还可以用普通类来模拟接口。具体的Java代码实现如下所示。

1
2
3
4
5
6
7
8
public class MockInterface {
protected MockInterface() {
}

public void funcA() {
throw new MethodUnSuportedException();
}
}

我们知道类中的方法必须包含实现,这个不符合接口的定义。但是,我们可以让类中的方法报出MethodUnSupportedException异常,来模拟不包含实现的接口,并且能强迫子类在继承父类的时候,都去主动实现父类的方法,否则就会在运行时抛出异常。那又如何避免这个类被实例化呢?实际上很简单,我们只需要将这个类的构造函数声明为protected访问权限就可以了。

对动态编程语言来说,还有一种对接口支持的策略就是duck-typing,Go也是这样支持接口的。

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

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

小结

  1. 抽象类和接口的语法特性

    抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫做抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。

  2. 抽象类和接口存在的意义

    抽象类是对成员变量和方法的抽象,是一种is-a关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种has-a关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

  3. 抽象类和接口的应用场景区别

    什么时候该用抽象类?什么时候该用接口?实际上,判断的标准很简单。如果要表示一种is-a的关系,并且是为了解决代码服用问题,我们就用抽象类;如果要表示一种has-a关系,并且是为了解决抽象而代码复用问题,那我们就用接口。