10.接口而非实现编程

简介: 本文详细介绍了“基于接口而非实现编程”的原则及其应用。首先阐述了接口编程的核心思想,即依赖抽象而非具体实现,以提高代码的灵活性和可维护性。接着通过图片存储案例分析了业务拓展时可能遇到的问题,并展示了如何通过定义抽象接口解决这些问题。同时探讨了定义接口的具体场景,如模块间通信、多态性、框架开发等,并强调了接口设计时需注意的事项,包括避免暴露实现细节和确保接口通用性。最后总结指出,接口原则旨在降低耦合性、提升扩展性,但并非所有场景都需要定义接口,应根据实际需求权衡使用。

10.接口而非实现编程

目录介绍

  • 01.接口编程原则
    • 1.1 接口指导思想
  • 02.如何理解接口
    • 2.1 重点搞清楚接口
    • 2.2 抽象的思想
  • 03.来看一个案例
    • 3.1 图片存储的案例
    • 3.2 业务拓展问题
    • 3.3 代码演变设计思想
    • 3.4 重构后的代码
  • 04.定义接口的场景
    • 4.1 要有接口意识
    • 4.2 接口具体的场景
    • 4.3 定义接口掌握度
  • 05.定义接口原则
    • 5.1 接口定义原则
    • 5.2 设计接口案例
    • 5.3 不涉及接口案例
  • 06.总结和重点回顾

01.接口编程原则

1.1 接口指导思想

基于接口而非实现编程。这个原则非常重要,是一种非常有效的提高代码质量的手段,在平时的开发中特别经常被用到。

“基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”。

理解这条原则的时候,千万不要一开始就与具体的编程语言挂钩,局限在编程语言的“接口”语法中(比如 Java 中的 interface 接口语法)。

这条原则最早出现于 1994 年 GoF 的《设计模式》这本书,它先于很多编程语言而诞生(比如 Java 语言),是一条比较抽象、泛化的设计思想。

基于接口而非实现编程的主要思想是,代码应该依赖于抽象的概念和契约,而不是具体的实现细节。通过定义接口或抽象类,将具体的实现细节隐藏起来,使得代码更加灵活、可扩展和可维护。

02.如何理解接口

2.1 重点搞清楚接口

实际上,理解这条原则的关键,就是理解其中的“接口”两个字。从本质上来看,“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。

如果落实到具体的编码,“基于接口而非实现编程”这条原则中的“接口”,可以理解为编程语言中的接口或者抽象类。

这条原则能非常有效地提高代码质量,之所以这么说,那是因为,应用这条原则,可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。

“基于接口而非实现编程”这条原则的另一个表述方式,是“基于抽象而非实现编程”。后者的表述方式其实更能体现这条原则的设计初衷。

2.2 抽象的思想

在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。

而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

03.来看一个案例

3.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 中。

代码实现非常简单,类中的几个方法定义得都很干净,用起来也很清晰,乍看起来没有太大问题,完全能满足我们将图片存储在阿里云的业务需求。

3.2 业务拓展问题

过了一段时间后,我们自建了私有云,不再将图片存储到阿里云了,而是将图片存储到自建私有云上。

为了满足这样一个需求的变化,我们该如何修改代码呢?我们需要重新设计实现一个存储图片到私有云的 PrivateImageStore 类,并用它替换掉项目中所有的 AliyunImageStore 类对象。这样的修改听起来并不复杂,只是简单替换而已,对整个代码的改动并不大。

实际上,刚刚的设计实现方式,就隐藏了很多容易出问题的“魔鬼细节”,一块来看看都有哪些。

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

  1. 首先,AliyunImageStore 类中有些函数命名暴露了实现细节,比如,uploadToAliyun() 和 downloadFromAliyun()。如果开发这个功能的同事没有接口意识、抽象思维,那这种暴露实现细节的命名方式就不足为奇了,毕竟最初我们只考虑将图片存储在阿里云上。而我们把这种包含“aliyun”字眼的方法,照抄到 PrivateImageStore 类中,显然是不合适的。如果我们在新类中重新命名 uploadToAliyun()、downloadFromAliyun() 这些方法,那就意味着,我们要修改项目中所有使用到这两个方法的代码,代码修改量可能就会很大。
  2. 其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产 access token,而私有云不需要 access token。一方面,AliyunImageStore 中定义的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另一方面,我们在使用 AliyunImageStore 上传、下载图片的时候,代码中用到了 generateAccessToken() 方法,如果要改为私有云的上传下载流程,这些代码都需要做调整。

3.3 代码演变设计思想

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

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

3.4 重构后的代码

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

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

04.定义接口的场景

4.1 要有接口意识

除此之外,很多人在定义接口的时候,希望通过实现类来反推接口的定义。

先把实现类写好,然后看实现类中有哪些方法,照抄到接口定义中。如果按照这种思考方式,就有可能导致接口定义不够抽象,依赖具体的实现。这样的接口设计就没有意义了。

不过,如果你觉得这种思考方式更加顺畅,那也没问题,只是将实现类的方法搬移到接口定义中的时候,要有选择性的搬移,不要将跟具体实现相关的方法搬移到接口中,比如 AliyunImageStore 中的 generateAccessToken() 方法。

在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。

在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。

4.2 接口具体的场景

  1. 模块间通信:接口可以用于定义模块之间的通信协议。通过定义接口,不同的模块可以按照接口规范进行交互,实现模块之间的解耦合。
  2. 多态性:接口在实现多态性方面起到关键作用。通过定义接口,可以实现不同类的对象对同一个接口的实现,从而实现多态性。
  3. 框架开发:在开发框架或库时,接口是非常重要的。通过定义接口,可以为框架提供一组公共的规范和契约,供开发者使用和扩展。
  4. 面向对象编程:在面向对象编程中,接口是一种重要的概念。通过定义接口,可以定义一组方法和属性,以规范类的行为和功能。
  5. 依赖注入(DI):接口的定义在依赖注入中扮演着重要的角色。通过定义接口,可以将依赖关系从具体的实现中解耦出来。
  6. API设计:在设计应用程序编程接口(API)时,接口的定义是关键。通过定义接口,可以明确API的功能、参数和返回值,以提供给其他开发者使用。
  7. 数据访问层:在应用程序中,接口的定义可以用于抽象数据访问层。通过定义接口,可以定义一组数据访问操作,供不同的数据存储实现类进行实现。

4.3 定义接口掌握度

为了满足这条原则,我是不是需要给每个实现类都定义对应的接口呢?在开发的时候,是不是任何代码都要只依赖接口,完全不依赖实现编程呢?

做任何事情都要讲求一个“度”,过度使用这条原则,非得给每个类都定义接口,接口满天飞,也会导致不必要的开发负担。

至于什么时候,该为某个类定义接口,实现基于接口的编程,什么时候不需要定义接口,直接使用实现类编程,我们做权衡的根本依据,还是要回归到设计原则诞生的初衷上来。

只要搞清楚了这条原则是为了解决什么样的问题而产生的,你就会发现,很多之前模棱两可的问题,都会变得豁然开朗。

05.定义接口原则

5.1 接口定义原则

定义接口这条原则的设计初衷是,将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。

上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。

5.2 案例分析对比

假设我们正在开发一个电子商务应用程序,其中有多个支付提供商(例如支付宝、微信支付、信用卡支付等)。希望能够轻松地切换和添加新的支付提供商,而不需要修改大量的代码。

如果没有定义接口,我们可能会在代码中直接使用特定支付提供商的实现类,这将导致以下问题:

  1. 高耦合性:代码中直接依赖于特定支付提供商的实现类,使得代码与该实现类紧密耦合。如果要更换支付提供商,需要修改大量的代码。
  2. 可维护性差:由于代码与特定实现类紧密耦合,修改一个支付提供商的实现可能会对整个代码库产生连锁反应。这增加了维护的复杂性。

那么按照今天学习的内容,通过定义接口,我们可以获得以下优势:

  1. 低耦合性:通过定义一个支付接口,代码只依赖于该接口,而不依赖于具体的支付提供商。使得代码更加灵活和可扩展。
  2. 可替换性:由于代码只依赖于支付接口,我们可以轻松地切换不同的支付提供商,只需提供符合接口定义的新实现即可,而不需要修改大量的代码。
  3. 可扩展性:通过接口定义,我们可以轻松地添加新的支付提供商,只需实现接口并提供相应的功能即可,而不需要修改现有的代码。
  4. 易于测试:通过接口,我们可以轻松地创建模拟实现来进行单元测试,而不需要依赖于真实的支付提供商。

从这个设计初衷上来看,如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。

除此之外,越是不稳定的系统,越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。

5.3 不涉及接口案例

确实,在某些情况下,不定义接口也是可以的。以下是一个例子来说明不定义接口的情况:

假设我们正在开发一个简单的脚本,用于读取一个文本文件并对其进行处理。在这种情况下,如果我们只需要一个简单的功能,不需要考虑扩展性或可替换性,那么定义接口可能是不必要的。

class TextFileProcessor {
   
    func processFile(at path: String) {
   
        // 读取文件逻辑和处理逻辑
        // ...
    }
}

// 使用示例
let processor = TextFileProcessor()
processor.processFile(at: "/path/to/file.txt")

在这个例子中,我们直接定义了一个具体的类 TextFileProcessor,它负责读取文件并进行处理。由于这个脚本只是一个简单的功能,不需要与其他组件进行交互或扩展,因此不定义接口也没有明显的劣势。

当涉及到更复杂的系统、模块之间的交互、可扩展性和可替换性时,定义接口通常是更好的选择。接口的使用可以提供更高的灵活性、可维护性和可测试性,使系统更易于扩展和修改。因此,在具体情况下,是否定义接口取决于需求和设计目标。

06.总结和重点回顾

  • 什么是基于接口而非实现编程:代码应该依赖于抽象的概念和契约,而不是具体的实现细节。通过定义接口或抽象类,将具体的实现细节隐藏起来,可扩展和可维护。
  • 定义接口主要是解决什么问题:通过定义接口或抽象类,将具体的实现细节隐藏起来,使得代码更加灵活、可扩展和可维护。
  • 如何如何理解接口:可以理解为编程语言中的接口或者抽象类。可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
  • 设计接口的时候要注意什么:接口的定义只表明做什么,而不是怎么做。要多思考一下,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。
  • 定义接口的场景有哪些:比如数据访问层定义抽象接口可以方便不同数据存储方式;比如依赖注入通过定义接口,可以将依赖关系从具体的实现中解耦出来。
  • 定义接口原则是什么:设计初衷是将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
  • 举一例子知道接口原则重要性:开发一个支付程序,多个支付提供商,例如支付宝、微信支付等,希望能够轻松地切换和添加新的支付提供商,需要用接口编程思想!

07.更多内容推荐

模块 描述 备注
GitHub 多个YC系列开源项目,包含Android组件库,以及多个案例 GitHub
博客汇总 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 YCBlogs
设计模式 六大设计原则,23种设计模式,设计模式案例,面向对象思想 设计模式
Java进阶 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM Java高级
网络协议 网络实际案例,网络原理和分层,Https,网络请求,故障排查 网络协议
计算机原理 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 计算机基础
学习C编程 C语言入门级别系统全面的学习教程,学习三到四个综合案例 C编程
C++编程 C++语言入门级别系统全面的教学教程,并发编程,核心原理 C++编程
算法实践 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 Leetcode
Android 基础入门,开源库解读,性能优化,Framework,方案设计 Android
目录
相关文章
|
机器学习/深度学习 自然语言处理 算法
文本分析-使用jieba库进行中文分词和去除停用词(附案例实战)
文本分析-使用jieba库进行中文分词和去除停用词(附案例实战)
11660 145
|
数据采集 存储 算法
终于有人把数据挖掘讲明白了
在大数据时代,许多企业面临一个难题:数据存储量庞大,却难以从中挖掘真正价值。本文深入探讨了数据挖掘的核心概念与实践方法,解析了其与普通数据分析的区别,并通过真实案例展示了如何通过数据挖掘发现隐藏的业务规律。文章还详细介绍了数据挖掘的六个步骤及三大关键点,强调了业务理解与数据质量的重要性,帮助企业在实际应用中少走弯路,真正实现数据驱动决策。
终于有人把数据挖掘讲明白了
|
Java 关系型数据库 MySQL
阿里巴巴Java开发手册简介(终极版、华山版、泰山版)(附下载地址)
阿里巴巴Java开发手册简介(终极版、华山版、泰山版)(附下载地址)
10164 0
|
2月前
|
数据采集 人工智能 运维
AI 搜索引擎优化和传统 SEO 的本质区别是什么?
本文深度剖析AI搜索优化(GEO)与传统SEO的本质差异,从流量目标、评判体系、用户链路、资产属性、实践逻辑五大维度揭示二者分野:前者重AI答案引用与语义可信度,后者重关键词排名与点击流量。强调双轨并行才是理性策略。(239字)
142 0
|
SQL 存储 分布式计算
【万字长文,建议收藏】《高性能ODPS SQL章法》——用古人智慧驾驭大数据战场
本文旨在帮助非专业数据研发但是有高频ODPS使用需求的同学们(如数分、算法、产品等)能够快速上手ODPS查询优化,实现高性能查数看数,避免日常工作中因SQL任务卡壳、失败等情况造成的工作产出delay甚至集群资源稳定性问题。
1938 36
【万字长文,建议收藏】《高性能ODPS SQL章法》——用古人智慧驾驭大数据战场
|
供应链 Serverless BI
基于阿里云区块链服务(BaaS)的供应链金融系统开发与部署
随着区块链技术的快速发展,其在供应链金融领域的应用成为热点。阿里云区块链服务(BaaS)提供安全、高效、易用的平台,支持Hyperledger Fabric和蚂蚁区块链,帮助企业快速构建供应链金融系统。本文通过实战案例展示基于阿里云BaaS开发供应链金融系统的全流程,涵盖企业认证、应收账款融资、交易记录及数据分析等功能,显著提升透明度和可信度,降低融资成本。
|
消息中间件 存储 负载均衡
两个实验让我彻底弄懂了「订阅关系一致」
这篇文章,笔者想聊聊 RocketMQ 最佳实践之一:**保证订阅关系一致**。 订阅关系一致指的是同一个消费者 Group ID 下所有 Consumer 实例所订阅的 Topic 、Tag 必须完全一致。 如果订阅关系不一致,消息消费的逻辑就会混乱,甚至导致消息丢失。
两个实验让我彻底弄懂了「订阅关系一致」
|
存储 SQL 关系型数据库
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
MySQL如何进行分库分表、数据迁移?从相关概念、使用场景、拆分方式、分表字段选择、数据一致性校验等角度阐述MySQL数据库的分库分表方案。
2607 15
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
|
Linux Windows
Centos7配置DHCP(简单模式)
Centos7配置DHCP(简单模式)
1376 0
Centos7配置DHCP(简单模式)
|
消息中间件 Java 关系型数据库
【二十】springboot整合ElasticSearch实战(万字篇)
【二十】springboot整合ElasticSearch实战(万字篇)
4137 47