目录
基于接口而非实现编程是一种非常有效地提高代码质量的手段,在平时的开发中会经常被用到。这里面的接口可以理解为编程语言中的接口或者抽象类。应用这条原则可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
1.实战应用
假设我们的系统中有很多涉及图片处理和存储的业务逻辑,图片经过处理之后被上传到阿里云上。为此我们统一提供了AliyunImageStore类,供整个系统使用:
public class AliyunImageStore {
//...省略属性、构造函数等...
public void createBucketIfNotExisting(String bucketName) {
// ...创建bucket代码逻辑...
// ...失败会抛出异常..
}
public String generateAccessToken() {
// ...根据accesskey/secrectkey等生成access token
}
public String uploadToAliyun(Image image, String bucketName, String accessToken) {
//...上传图片到阿里云...
//...返回图片存储在阿里云上的地址(url)...
}
public Image downloadFromAliyun(String url, String accessToken) {
//...从阿里云下载图片...
}
}
// AliyunImageStore类的使用举例
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
//...省略其他无关代码...
public void process() {
Image image = ...; //处理图片,并封装为Image对象
AliyunImageStore imageStore = new AliyunImageStore(/*省略参数*/);
imageStore.createBucketIfNotExisting(BUCKET_NAME);
String accessToken = imageStore.generateAccessToken();
imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);
}
}
整个上传流程包含三个步骤:创建bucket、生成access token访问凭证、携带access token上传图片到指定的bucket中。代码实现非常简单,类中的几个方法定义都很干净,用起来也很清晰,完全可以满足我们存储图片到阿里云的需求。不过,软件开发中唯一不变的就是变化。过了一段时间,如果我们自建了私有云,不再将图片存储到阿里云,而是存储到自建私有云上。为了满足这个变化,我们该如何修改代码?
首先,AliyunImageStore类中有些函数命名暴露了实现细节,比如uploadToAliyun()和downloadFromAliyun()。此时,我们需要把这种包含“aliyun”字眼的类和方法全部重新命名,代码的改动量可能会很大。其次,将图片存储到阿里云的流程跟存储到私有云的流程,可能并不完全一致的。比如,私有云可能并不需要access token。那这两个问题应该如何解决呢?解决这个问题的根本方法就是,在编写代码的时候,要遵从“基于接口而非实现编程”的原则,具体来讲,需要做到以下三点:
- 方法的命名不能暴露任何实现细节,比如uploadToAliyun()就不符合要求,应该去掉aliyun这样的字眼,改为upload()。
- 封装具体的实现细节。比如,跟阿里云相关的特殊上传或下载流程不应该暴露给调用者,应该对其进行封装,对外提供一个包括所有上传或下载细节的方法,供调用者使用。
- 为实现类定义抽象的接口。具体的实现类都依赖于统一的接口定义,遵从一致的上传功能协议,使用者依赖接口,而不是具体的实现类来编程。
按照这个思路,把代码重构一下:
public interface ImageStore {
String upload(Image image, String bucketName);
Image download(String url);
}
public class AliyunImageStore implements ImageStore {
//...省略属性、构造函数等...
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
String accessToken = generateAccessToken();
//...上传图片到阿里云...
//...返回图片在阿里云上的地址(url)...
}
public Image download(String url) {
String accessToken = generateAccessToken();
//...从阿里云下载图片...
}
private void createBucketIfNotExisting(String bucketName) {
// ...创建bucket...
// ...失败会抛出异常..
}
private String generateAccessToken() {
// ...根据accesskey/secrectkey等生成access token
}
}
// 上传下载流程改变:私有云不需要支持access token
public class PrivateImageStore implements ImageStore {
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
//...上传图片到私有云...
//...返回图片的url...
}
public Image download(String url) {
//...从私有云下载图片...
}
private void createBucketIfNotExisting(String bucketName) {
// ...创建bucket...
// ...失败会抛出异常..
}
}
// ImageStore的使用举例
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
//...省略其他无关代码...
public void process() {
Image image = ...;//处理图片,并封装为Image对象
ImageStore imageStore = new PrivateImageStore(...);
imagestore.upload(image, BUCKET_NAME);
}
}
总结一下,我们在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。
2.是否需要为每个类都定义接口?
为了满足这条原则,我们是不是需要给每个实现类都定义对应的接口呢?其实,做任何事情都要讲究一个度,过度使用这条原则,会导致每个类都定义了接口,接口满天飞,也会导致不必要的开发负担。
从该原则的设计初衷上来看,如果我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没必要基于接口编程,直接使用实现类就可以了。除此之外,越是不稳定的系统,我们越要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那么我们就没有必要为其扩展性,投入不必要的开发时间。