【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 中。这样的设计没有问题,但是一段时间后我们自建了私有云,不再将图片存储到阿里云了,而是将图片存储到自建私有云上。为了满足这样一个需求的变化,我们该如何修改代码呢?

  • 首先,AliyunImageStore 类中有些函数命名暴露了实现细节,比如,uploadToAliyun() 和 downloadFromAliyun(),那就意味着,我们要修改项目中所有使用到这两个方法的代码,代码修改量可能就会很大
  • 其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产 access token,而私有云不需要 access token。一方面,AliyunImageStore 中定义的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另一方面,我们在使用 AliyunImageStore 上传、下载图片的时候,代码中用到了 generateAccessToken() 方法,如果要改为私有云的上传下载流程,这些代码都需要做调整。

解决这个问题的根本方法就是,在编写代码的时候,要遵从基于接口而非实现编程的原则

  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
  }
}
// 私有云的实现
public class PrivateImageStore implements ImageStore  {
  //上传下载流程改变:私有云不需要支持access token
  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() 方法,一直觉得没问题也是因为一般一个接口对应一个实现,很少写多扩展的接口。

为每个实现类都定义接口

这条原则的设计初衷是,将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。从这个设计初衷上来看,如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。除此之外,越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间

总结一下

基于接口而非实现编程,实际上是基于抽象而非实现编程,具体到Java里就是抽象特性的体现,抽象的两个实现方式也就是上篇Blog提到的抽象类和接口类。原则的设计初衷是将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。但这个原则使用时也需要注意,设计接口和实现时思维顺序要摆正,不要通过实现去反推接口定义;写代码时候也不要惯性的为每个实现类都定义接口,使用原则时要理解其初衷,如果某个功能不需要扩展那只有实现就行了,具体到我们的实现里就是facade接口是不就大可不必?MVC里的所有service接口都有必要?这些问题可能得动脑思考而非无脑堆砌。

相关文章
|
1天前
|
Java 调度 开发者
Java 并发编程的探索与实践
【5月更文挑战第3天】在当今多核处理器普及的时代,并发编程已经成为提高程序性能的重要手段。本文将深入探讨 Java 并发编程的基本概念、原理及其在实际项目中的应用,帮助读者更好地理解和掌握 Java 并发编程技巧。
|
2天前
|
XML 监控 安全
18:面向切面编程-Java Spring
18:面向切面编程-Java Spring
19 5
|
2天前
|
设计模式 前端开发 Java
19:Web开发模式与MVC设计模式-Java Web
19:Web开发模式与MVC设计模式-Java Web
11 4
|
2天前
|
设计模式 存储 前端开发
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
18 4
|
2天前
|
设计模式 缓存 监控
JAVA设计模式之结构型模式
结构模型:适配器模型、桥接模型、过滤器模型、组合模型、装饰器模型、外观模型、享受元模型和代理模型。
15 3
|
2天前
|
并行计算 安全 Java
Java 并发编程的探索之旅
【5月更文挑战第2天】 在多线程的世界里,程序的行为变得错综复杂。本文将带您走进 Java 并发编程的核心概念,通过深入分析并发工具的使用和原理,帮助您构建高效、安全且响应迅速的应用程序。我们将探讨线程的基本知识,同步机制,以及高级并发工具如 Executors、Futures 和 Streams。通过理论与实践相结合的方式,为开发者提供一份清晰、实用的并发编程指南。
9 2
|
2天前
|
存储 缓存 前端开发
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
13 3
|
2天前
|
Java
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识。入坑JAVA因它的面向对象特性、平台无关性、强大的标准库和活跃的社区支持。
13 2
|
2天前
|
Java 调度 开发者
Java中的多线程编程:基础与实践
【5月更文挑战第2天】本文将深入探讨Java中的多线程编程,从基础概念到实际应用,为读者提供全面的理解和实践指导。我们将首先介绍线程的基本概念和重要性,然后详细解析Java中实现多线程的两种主要方式:继承Thread类和实现Runnable接口。接着,我们将探讨线程同步的问题,包括synchronized关键字和Lock接口的使用。最后,我们将通过一个实际的生产者-消费者模型来演示多线程编程的实践应用。
|
2天前
|
安全 Java 程序员
Java中的多线程编程:从理论到实践
【5月更文挑战第2天】 在计算机科学中,多线程编程是一项重要的技术,它允许多个任务在同一时间段内并发执行。在Java中,多线程编程是通过创建并管理线程来实现的。本文将深入探讨Java中的多线程编程,包括线程的概念、如何创建和管理线程、以及多线程编程的一些常见问题和解决方案。
12 1