接口是一组操作的集合,通常接口中定义的操作是密切相关的,也是职责单一的,这是接口设计的规范要求。所以在一个系统中可能抽象出许多接口,这些接口彼此之间几乎没有交集(各个接口职责单一)。

但是对于一个系统而言,往往是由许多组件(接口的具体实现类)构成的,如果构成系统的组件没有交集,则这个系统的功能也就非常单一了。

所以对于接口的具体实现类而言,它们之间会存在协作关系,例如:组合关系。这时候就可以使用抽象类,来表达这种关系。例如:(参考:集合类的设计)

  • ICount ,计数器
1
2
3
4
5
6
7
8
public interface ICount {

void incCount();

void decCount();

int getCount();
}
  • ILock, 用来确保线程安全的锁
1
2
3
4
5
6
public interface ILock {

void lock();

void unlock();
}
  • 线程安全的ICount
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
public abstract class AbstractCount implements ICount {

private int messageCount;
private ILock lock;

public AbstractCount() {
this.lock = new SpinLock();
}

public AbstractCount(ILock lock) {
this.lock = lock;
}

@Override
public abstract void incCount();

@Override
public abstract void decCount();

@Override
public int getCount() {
return this.messageCount;
}

}

如果一个系统中需要,基于不同实现的ILock类的ICount实例,做上面的一层抽象,非常有必要。这个抽象类的构造函数:

1
2
3
public AbstractCount(ILock lock) {
this.lock = lock;
}

将 ILock 接口和 ICount 接口 连接 了起来。

同时,还可以提供一个默认实现:

1
2
3
public AbstractCount() {
this.lock = new SpinLock();
}

这样方便了,子类来实现ICount 接口。

抽象完成接口与接口之间关系的耦合,具体的实现通常继承抽象类,提供具体的实现。

集合类中的继承关系:

使用接口的使用

在一个功能模块的实现过程中,总会有一些作为完成核心功能的抽象,

1
2
3
4
5
6
+---------------+
| client |
+---------------+
| | |
| core1 | core2 |
+---------------+

如上图所示,在一个最终完成功能的可以就是 client 代码通过调用抽象的 core1 和 core2 模块代码来实现功能。如果在 core1 ,core2 在完成功能的变动风险很小或者说它们其实就是不变的,则使用上面的设计完全没有问题,但是如果 core1 存在变动的风险,则 core1 提供的功能,可以发生变化,导致 client 代码必须重新修改,编译才可以被使用,这说明 client 和 core1 类发生的紧耦合,这种耦合关系使得其中的任何一个模块,出现牵一发而动全身的状况,所以在这种情况下,就需要对 core1 类所能够提供的功能进行抽象从中提取出一个接口,然后 client 可以通过 这个接口来访问 core1 而不是直接,访问,core1 了。此时系统就变成了:

1
2
3
4
5
6
7
+---------------+
| client |
+---------------+
|Icore1 | |
+------ + |
| core1 | core2 |
+---------------+

这个时候,client 和 core1 类都依赖于抽象的接口 Icore1,而不同两个实体类之间依赖。所以只要接口(Icore1)不变,当 core1 发生变化的时候,并不会影响到 client 的代码实现。

从而,达到了 client 和 core1 的 解耦。

其实,这里还是有问题,此时 client 和 core1 类都依赖于抽象的接口 Icore1,那么 Icore1 这个接口的设计,就显得非常重要了,因为如果这个接口设计的不好,没有能够完全表达 core1 的功能,则可以 Icore1 接口会发生变化,此时 client 和 core1 就必须都得跟着变。

所以,对一个功能模块抽象接口出来,是一件很重要的事。接口要能够充分表达功能,并且足够的稳定。

抽象过程

抽象过程是功能细分的过程。接口或者抽象类,应该尽可能的功能单一,便于复用和修改。

功能的提取划分。从业务代码中抽取和业务无关,或者其它业务可以复用的,代码。作为一个新功能模块。