接口和抽象类
接口是一组操作的集合,通常接口中定义的操作是密切相关的,也是职责单一的,这是接口设计的规范要求。所以在一个系统中可能抽象出许多接口,这些接口彼此之间几乎没有交集(各个接口职责单一)。
但是对于一个系统而言,往往是由许多组件(接口的具体实现类)构成的,如果构成系统的组件没有交集,则这个系统的功能也就非常单一了。
所以对于接口的具体实现类而言,它们之间会存在协作关系,例如:组合关系。这时候就可以使用抽象类,来表达这种关系。例如:(参考:集合类的设计)
- ICount ,计数器
1 | public interface ICount { |
- ILock, 用来确保线程安全的锁
1 | public interface ILock { |
- 线程安全的ICount
1 | public abstract class AbstractCount implements ICount { |
如果一个系统中需要,基于不同实现的ILock类的ICount实例,做上面的一层抽象,非常有必要。这个抽象类的构造函数:
1 | public AbstractCount(ILock lock) { |
将 ILock 接口和 ICount 接口 连接 了起来。
同时,还可以提供一个默认实现:
1 | public AbstractCount() { |
这样方便了,子类来实现ICount 接口。
抽象完成接口与接口之间关系的耦合,具体的实现通常继承抽象类,提供具体的实现。
集合类中的继承关系:
使用接口的使用
在一个功能模块的实现过程中,总会有一些作为完成核心功能的抽象,
1 | +---------------+ |
如上图所示,在一个最终完成功能的可以就是 client 代码通过调用抽象的 core1 和 core2 模块代码来实现功能。如果在 core1 ,core2 在完成功能的变动风险很小或者说它们其实就是不变的,则使用上面的设计完全没有问题,但是如果 core1 存在变动的风险,则 core1 提供的功能,可以发生变化,导致 client 代码必须重新修改,编译才可以被使用,这说明 client 和 core1 类发生的紧耦合,这种耦合关系使得其中的任何一个模块,出现牵一发而动全身的状况,所以在这种情况下,就需要对 core1 类所能够提供的功能进行抽象从中提取出一个接口,然后 client 可以通过 这个接口来访问 core1 而不是直接,访问,core1 了。此时系统就变成了:
1 | +---------------+ |
这个时候,client 和 core1 类都依赖于抽象的接口 Icore1,而不同两个实体类之间依赖。所以只要接口(Icore1)不变,当 core1 发生变化的时候,并不会影响到 client 的代码实现。
从而,达到了 client 和 core1 的 解耦。
其实,这里还是有问题,此时 client 和 core1 类都依赖于抽象的接口 Icore1,那么 Icore1 这个接口的设计,就显得非常重要了,因为如果这个接口设计的不好,没有能够完全表达 core1 的功能,则可以 Icore1 接口会发生变化,此时 client 和 core1 就必须都得跟着变。
所以,对一个功能模块抽象接口出来,是一件很重要的事。接口要能够充分表达功能,并且足够的稳定。
抽象过程
抽象过程是功能细分的过程。接口或者抽象类,应该尽可能的功能单一,便于复用和修改。
功能的提取划分。从业务代码中抽取和业务无关,或者其它业务可以复用的,代码。作为一个新功能模块。