面向接口编程原理与实践

面向接口编程原理

“基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”。咱们理解这条原则的时候,千万不要一开始就与具体的编程语言挂钩,局限在编程语言的“接口”语法中(好比 Java 中的 interface 接口语法)。这条原则最先出现于 1994 年 GoF 的《设计模式》这本书,它先于不少编程语言而诞生(好比 Java 语言),是一条比较抽象、泛化的设计思想。java

这条原则能很是有效地提升代码质量,之因此这么说,那是由于,应用这条原则,能够::编程

  • 将接口和实现相分离
  • 封装不稳定的实现
  • 暴露稳定的接口

上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不须要作改动,以此来下降耦合性,提升扩展性设计模式

实际上,“基于接口而非实现编程”这条原则的另外一个表述方式,是“基于抽象而非实现编程”。后者的表述方式其实更能体现这条原则的设计初衷。在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提升代码的灵活性,越能应对将来的需求变化。好的代码设计,不只能应对当下的需求,并且在未来需求发生变化的时候,仍然可以在不破坏原有代码设计的状况下灵活应对。而抽象就是提升代码扩展性、灵活性、可维护性最有效的手段之一架构

面向接口编程实践

假设咱们的系统中有不少涉及图片处理和存储的业务逻辑。图片通过处理以后被上传到阿里云上。为了代码复用,咱们封装了图片存储相关的代码逻辑,提供了一个统一的 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 中。代码实现很是简单,类中的几个方法定义得都很干净,用起来也很清晰,乍看起来没有太大问题,彻底能知足咱们将图片存储在阿里云的业务需求。函数

不过,软件开发中惟一不变的就是变化。过了一段时间后,咱们自建了私有云,再也不将图片存储到阿里云了,而是将图片存储到自建私有云上。为了知足这样一个需求的变化,咱们该如何修改代码呢?阿里云

咱们须要从新设计实现一个存储图片到私有云的 PrivateImageStore 类,并用它替换掉项目中全部的 AliyunImageStore 类对象。这样的修改听起来并不复杂,只是简单替换而已,对整个代码的改动并不大。不过,咱们常常说,“细节是魔鬼”。这句话在软件开发中特别适用。实际上,刚刚的设计实现方式,就隐藏了不少容易出问题的“魔鬼细节”,咱们一块来看看都有哪些。url

新的 PrivateImageStore 类须要设计实现哪些方法,才能在尽可能最小化代码修改的状况下,替换掉 AliyunImageStore 类呢?这就要求咱们必须将 AliyunImageStore 类中所定义的全部 public 方法,在 PrivateImageStore 类中都逐必定义并从新实现一遍。而这样作就会存在一些问题,我总结了下面两点。架构设计

首先,AliyunImageStore 类中有些函数命名暴露了实现细节,好比,uploadToAliyun() 和 downloadFromAliyun()。若是开发这个功能的同事没有接口意识、抽象思惟,那这种暴露实现细节的命名方式就不足为奇了,毕竟最初咱们只考虑将图片存储在阿里云上。而咱们把这种包含“aliyun”字眼的方法,照抄到 PrivateImageStore 类中,显然是不合适的。若是咱们在新类中从新命名 uploadToAliyun()、downloadFromAliyun() 这些方法,那就意味着,咱们要修改项目中全部使用到这两个方法的代码,代码修改量可能就会很大。设计

其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并非彻底一致的。好比,阿里云的图片上传和下载的过程当中,须要生产 access token,而私有云不须要 access token。一方面,AliyunImageStore 中定义的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另外一方面,咱们在使用 AliyunImageStore 上传、下载图片的时候,代码中用到了 generateAccessToken() 方法,若是要改成私有云的上传下载流程,这些代码都须要作调整。

那这两个问题该如何解决呢?解决这个问题的根本方法就是,在编写代码的时候,要听从“基于接口而非实现编程”的原则,具体来说,咱们须要作到下面这 3 点。

  1. 函数的命名不能暴露任何实现细节。好比,前面提到的 uploadToAliyun() 就不符合要求,应该改成去掉 aliyun 这样的字眼,改成更加抽象的命名方式,好比:upload()。
  2. 封装具体的实现细节。好比,跟阿里云相关的特殊上传(或下载)流程不该该暴露给调用者。咱们对上传(或下载)流程进行封装,对外提供一个包裹全部上传(或下载)细节的方法,给调用者使用。
  3. 为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,听从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。

咱们按照这个思路,把代码重构一下。重构后的代码以下所示:

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);
  }
}

除此以外,不少人在定义接口的时候,但愿经过实现类来反推接口的定义。先把实现类写好,而后看实现类中有哪些方法,照抄到接口定义中。若是按照这种思考方式,就有可能致使接口定义不够抽象,依赖具体的实现。这样的接口设计就没有意义了。不过,若是你以为这种思考方式更加顺畅,那也没问题,只是将实现类的方法搬移到接口定义中的时候,要有选择性的搬移,不要将跟具体实现相关的方法搬移到接口中,好比 AliyunImageStore 中的 generateAccessToken() 方法。

总结一下,咱们在作软件开发的时候,必定要有抽象意识、封装意识、接口意识。在定义接口的时候,不要暴露任何实现细节。接口的定义只代表作什么,而不是怎么作。并且,在设计接口的时候,咱们要多思考一下,这样的接口设计是否足够通用,是否可以作到在替换具体的接口实现的时候,不须要任何接口定义的改动。

面向接口编程总结

  1. “基于接口而非实现编程”,这条原则的另外一个表述方式,是“基于抽象而非实现编程”。后者的表述方式其实更能体现这条原则的设计初衷。咱们在作软件开发的时候,必定要有抽象意识、封装意识、接口意识。越抽象、越顶层、越脱离具体某一实现的设计,越能提升代码的灵活性、扩展性、可维护性。

  2. 咱们在定义接口的时候,一方面,命名要足够通用,不能包含跟具体实现相关的字眼;另外一方面,与特定实现有关的方法不要定义在接口中。

  3. “基于接口而非实现编程”这条原则,不只仅能够指导很是细节的编程开发,还能指导更加上层的架构设计、系统设计等。好比,服务端与客户端之间的“接口”设计、类库的“接口”设计。

相关文章
相关标签/搜索