设计模式学习笔记 - 面向对象 - 6.为什么要基于接口而非实现编程?有必要为每个类都定义接口吗?

前言

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


如何解读原则中的“接口”二字

要理解“基于接口而非实现编程”的关键就是要理解其中的“接口”二字,我们可以理解为编程语言中的接口或抽象类。

前面我们提到,这条原则非常有效地提高代码质量,因为这条原则可以将接口与实现分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现,这样当实现放生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合,提高扩展性。

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

如何将这条原则应用到实战中

假设,系统中有很多设计图片处理和存储的业务逻辑。图片经过处理之后被上传到阿里云上。为了代码复用,我们封装了图片存储相关的代码逻辑,提供了一个统一的 AliyunImageStore 类。

public class AliyunImageStore {// 省略构造属性、构造函数...public void createBucketIfNotExists(String bucketName) {// 创建bucket代码逻辑...// 失败或抛出异常}public String generateAccessKey() {// 根据accessKey/secretKey等生成 access Token...}public String uploadToAliyun(Image image, String bucketName, String accessToken) {// 上传图片到阿里云...// 返回图片存储在阿里云上的地址(url)...}public String downloadFromAliyun(String url, String accessToken) {// 从阿里云下载图片...}
}// AliyunImageStore类的使用举例
public class ImageProcessJob {private static final String BUCKET_NAME = "ai_image_bucket";// 省略其他无关代码...public void process() {Image image = ...; //处理图片,并封装为Image对象AliyunImageStore imageStore = new AliyunImageStore(/*省略参数*/);imageStore.createBucketIfNotExists(BUCKET_NAME);String accessToken = imageStore.generateAccessKey();imageStore.uploadToAliyun(image, BUCKET_NAME, accessToken);}
}

整个上传流程分为三步:创建 bucket(简单理解为目录)、生成 accessToken、携带 accessToken 上传图片到指定的 bucket 中。咋看起来,这段代码没有太大问题,完全能满足我们将图片存储在阿里云的需求。

软件开发经常会发生需求变化。假设我们自建了私有云,不在将图片存储到阿里云了,而是将图片存储到自建云上。此时,该如何修改代码呢?

需要重新设计一个 PrivateImageStore 类,并用它来替掉项目中所有的 AliyunImageStore 类。这样的修改看起来并不复杂,知识简单替换而已,对整个代码的改动不大。不过,我们常说“细节是魔鬼”。实际上,刚刚的设计实现方式,就因此了很多容易出问题的“魔鬼细节”。

新的 PrivateImageStore 类需要设计哪些方式,才能在尽量最小化代码修改的情况下,替换掉 AliyunImageStore 呢?这就要求我们必须将 AliyunImageStore 类中定义的所有 public 方法都注意定义并实现以便。但是这样做忽悠一些问题:

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

如何解决上述两个问题呢?那就是要遵从“基于接口而非实现编程”的原则,具体来讲,需要做到 3 点:

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

按照这个思路,重构以下上面的代码。

public interface ImageStore {String upload(Image image, String bucketName);Image download(String bucketName);
}public class AliyunImageStore implements ImageStore {// 省略属性、构造函数等...@Overridepublic String upload(Image image, String bucketName) {createBucketIfNotExists(bucketName);String accessToken = generateAccessKey();// 上传图片到阿里云...// 返回图片存储在阿里云上的地址(url)...}@Overridepublic Image download(String bucketName) {String accessToken = generateAccessKey();// 从阿里云下载图片...}public void createBucketIfNotExists(String bucketName) {// 创建bucket代码逻辑...// 失败或抛出异常}public String generateAccessKey() {// 根据accessKey/secretKey等生成 access Token...}
}// 上传流程改变:私有云不需要accessToken
public class PrivateImageStore implements ImageStore {// 省略属性、构造函数等...@Overridepublic String upload(Image image, String bucketName) {createBucketIfNotExists(bucketName);// 上传图片到私有云...// 返回图片存储在私有云上的地址(url)...}@Overridepublic Image download(String bucketName) {// 从私有云下载图片...}public void createBucketIfNotExists(String bucketName) {// 创建bucket...// 失败或抛出异常}
}// ImageStore类的使用举例
public class ImageProcessJob {private static final String BUCKET_NAME = "ai_image_bucket";// 省略其他无关代码...public void process() {Image image = ...; //处理图片,并封装为Image对象ImageStore imageStore = new PrivateImageStore(/*省略参数*/);imageStore.upload(image, BUCKET_NAME);}
}

有很多人在定义接口的时候,希望通过实现类来反推接口的定义。如果按照这种的方式,可能导致接口定义不够抽象,依赖具体的实现。这样的接口设计就没有意义了。

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

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

是否需要为每个类定义接口?

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

至于什么时候,该为某个类设计接口,实现基于接口的编程,什么时候不需要定义接口,直接使用实现类编程,我们要做权衡的根本依据,还是要回归到设计原则诞生的初衷上来。只要搞清楚了这条规则,很多之前模棱两可的问题,都会变得豁然开朗。

前面已经讲过,这条原则的设计初衷是,将接口和实现分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实际发生变化的时候,上游系统的代码基本上不需要做改动,依次来降低代码间的耦合性,提高代码的扩展性。

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

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

总结

  1. 基于接口而非实现编程,这条原则的另一个表达方式是“基于抽象而非实现编程”,我们在开发的时候,一定要有抽象意识、封装意识、接口意识。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性和可维护性。
  2. 在定义接口的时候,一方面,命名要足够通用,不能包含跟具体实现相关的字眼;另一方面,与特定实现有关的方法,不要定义在接口中。
  3. “基于接口而非实现编程”这条原则,不仅仅可以指导非常细节的编程开发,还能指导更加上层的架构设计、系统设计等。比如,客户端与服务器端之间的“接口设计”、类库的“接口”设计。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/698805.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学习数据节构和算法的第14天

题目讲解 链表的移除 #include <stdio.h> #include <stdlib.h> // 定义链表节点结构体 typedef struct Node {int data; // 节点数据struct Node* next; // 指向下一个节点的指针 } Node; // 初始化链表节点 Node* initNode(int data) {Node* n…

mfc 疑难杂症之一

病情&#xff1a; 1.xxxx处的第一机会异常: 0xC0000005: 读取位置 0x00000004 时发生访问冲突。 2.不定时程序闪退 访问违例 程序定位到 main处 0x76DCB5B2 处(位于 Tetris.exe 中)引发的异常: Microsoft C 异常: CResourceException&#xff0c;位于内存位置 0x008FF0D4 处…

vue计算属性和监听器详解

1.watch 和 computed 的作用和区别 watch&#xff08;侦听器&#xff09; 作用&#xff1a; 监听器允许开发者自定义一个函数来观察 Vue 实例上的特定数据属性的变化&#xff0c;当这些属性发生变化时&#xff0c;会触发相应的回调函数。 特点&#xff1a; 非缓存&#xff1a…

用支持向量机进行光学符号识别

&#x1f349;CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一&#xff5c;统计学&#xff5c;干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项&#xff0c;参与研究经费10w、40w级横向 文…

企业级大数据安全架构(十一)Kerberos接入dophinscheduler

作者&#xff1a;楼高 建议将dophinscheduler集成到Ambari安装部署&#xff0c;在Ambari上面开启kerberos 1.安装准备 编译 从GitHub获取dolphinscheduler-1.3.9源码 git clone https://github.com/apache/dolphinscheduler.git -b 1.3.9-releasehttps://github.com/apache/…

多输入回归预测|GWO-CNN-LSTM|灰狼算法优化的卷积-长短期神经网络回归预测(Matlab)

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、算法介绍&#xff1a; 灰狼优化算法&#xff1a; 卷积神经网络-长短期记忆网络&#xff1a; 四、完整程序下载&#xff1a; 一、程序及算法内容…

java日志框架总结(七、使用过滤器自动打印接口入参、出参)

使用过滤器自动打印接口入参、出参首先要了解一个过滤器OncePerRequestFilter&#xff0c;一般使用这个过滤器进行日志打印。 一、OncePerRequestFilter 1)、什么是OncePerRequestFilter 回顾一下 Filter 的工作原理。Filter 可以在 Servlet 执行之前或之后调用。当请求被调度…

ChatGPT/GPT4科研应用与AI绘图及论文写作

2023年随着OpenAI开发者大会的召开&#xff0c;最重磅更新当属GPTs&#xff0c;多模态API&#xff0c;未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚于互联网和个人电脑的问世。360创始人周鸿祎认为未来各行各业如果不能搭上这班车…

重看LeakCanary

LeakCanary是我很久之前看的东西了&#xff0c;我当时侯对它的印象就是它可以用来检测内存泄漏&#xff0c;具体原理就是将弱引用对象延迟个5s然后看是否被回收,如果没有被回收,那么就说明发生了内存泄漏,其他的也没有仔细地看 现在就详细地梳理一遍这个流程&#xff1a; 1.L…

微服务篇之分布式事务

一、Seata架构 Seata事务管理中有三个重要的角色&#xff1a; TC (Transaction Coordinator) - 事务协调者&#xff1a;维护全局和分支事务的状态&#xff0c;协调全局事务提交或回滚。 TM (Transaction Manager) - 事务管理器&#xff1a;定义全局事务的范围、开始全局事务、…

docker学习总结

docker 1.初识Docker1.1.什么是Docker1.1.1.应用部署的环境问题1.1.2.Docker解决依赖兼容问题1.1.3.Docker解决操作系统环境差异1.1.4.小结 1.2.Docker和虚拟机的区别1.3.Docker架构1.3.1.镜像和容器1.3.2.DockerHub1.3.3.Docker架构1.3.4.小结 1.4.安装Docker 2.Docker的基本操…

Kubernetes Prometheus 系列|Prometheus介绍和使用|Prometheus+Grafana集成

目录 第1章Prometheus 入门1.1 Prometheus 的特点1.1.1 易于管理1.1.2 监控服务的内部运行状态1.1.3 强大的数据模型1.1.4 强大的查询语言 PromQL1.1.5 高效1.1.6 可扩展1.1.7 易于集成1.1.8 可视化1.1.9 开放性 1.2 Prometheus 的架构1.2.1 Prometheus 生态圈组件1.2.2 架构理…

Go 数据库编程精粹:database/sql 实用技巧解析

Go 数据库编程精粹&#xff1a;database/sql 实用技巧解析 简介database/sql 库的基础知识核心概念连接池驱动事务 环境配置 建立数据库连接连接到数据库示例&#xff1a;连接 MySQL 数据库连接池管理 执行查询和处理结果基本查询执行多行查询执行单行查询 结果处理处理多行结果…

基于Java SSM框架实现问卷调查系统项目【项目源码】

基于java的SSM框架实现问卷调查系统演示 B/S结构 BROWSER/SERVER程序架构方式是使用电脑中安装的各种浏览器来进行访问和使用的&#xff0c;相比C/S的程序结构不需要进行程序的安装就可以直接使用。BROWSER/SERVER架构的运行方式是在远程的服务器上进行安装一个&#xff0c;然…

普中51单片机学习(DS1302)

DS1302时钟 DS1302实时时钟具有能计算2100年之前的秒、分、时、日、日期、星期、月、年的能力&#xff0c;还有闰年调整的能力。内部含有31个字节静态RAM&#xff0c;可提供用户访问。采用串行数据传送方式&#xff0c;使得管脚数量最少&#xff0c;简单SPI 3线接口。工作电压…

4.8 Verilog过程连续赋值

关键词&#xff1a;解除分配&#xff0c;强制&#xff0c;释放 过程连续赋值是过程赋值的一种。赋值语句能够替换其他所有wire 或 reg 的赋值&#xff0c;改写wire 或 reg 类型变量的当前值。 与过程赋值不同的是&#xff0c;过程连续赋值表达式能被连续的驱动到wire 或 reg …

C++——基础语法(2):函数重载

4. 函数重载 函数重载就是同一个函数名可以重复被定义&#xff0c;即允许定义相同函数名的函数。但是相同名字的函数怎么在使用的时候进行区分呢&#xff1f;所以同一个函数名的函数之间肯定是要存在不同点的&#xff0c;除了函数名外&#xff0c;还有返回类型和参数两部分可以…

Composition API 和 Options API

为什么Composition API 比 Options API 更好 Composition API是Vue.js 3.x版本引入的一种新的组织代码的方式。它相对于Options API有一些明显的优势&#xff0c;使得它在某些场景下更加灵活和易于使用。 更好的逻辑组织&#xff1a;Composition API允许将相关代码逻辑打包在一…

如何进行数据库分区和分片操作?

什么是数据库分区和分片&#xff1f; 数据库分区和分片都是数据库物理设计中的技术&#xff0c;旨在提高数据库的性能和管理大规模数据。 数据库分区是一种物理数据库的设计技术&#xff0c;其主要目的是在特定的SQL操作中减少数据读写的总量以缩减响应时间。分区并不是生成新…

mysql 输出所在月份的最后一天

内置函数 LAST_DAY(date) 参数&#xff1a; date &#xff1a;一个日期或日期时间类型的值&#xff0c;表示要获取所在月份最后一天的日期。 返回值&#xff1a; 返回一个日期值&#xff0c;表示输入日期所在月份的最后一天。 栗子 月总刷题数和日均刷题数_牛客题霸_牛客…