为什么基于接口而非实现编程?
如何解读原则中的“接口”二字?
“基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”。我们理解这条原则的时候,千万不要一开始就与具体的编程语言挂钩,局限在编程语言的“接口”语法中(比如 Java 中的 interface 接口语法)。这条原则最早出现于 1994 年 GoF 的《设计模式》这本书,它先于很多编程语言而诞生(比如 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 类对象。这样的修改听起来并不复杂,只是简单替换而已,对整个代码的改动并不大。不过,我们经常说,“细节是魔鬼”。这句话在软件开发中特别适用。实际上,刚刚的设计实现方式,就隐藏了很多容易出问题的“魔鬼细节”,我们一块来看看都有哪些。
新的 PrivateImageStore
类需要设计实现哪些方法,才能在尽量最小化代码修改的情况下,替换掉 AliyunImageStore
类呢?这就要求我们必须将 AliyunImageStore
类中所定义的所有 public 方法,在 PrivateImageStore
类中都逐一定义并重新实现一遍。而这样做就会存在一些问题,我总结了下面两点。
首先,AliyunImageStore
类中有些函数命名暴露了实现细节,比如,uploadToAliyun()
和 downloadFromAliyun()
。如果开发这个功能的同事没有接口意识、抽象思维,那这种暴露实现细节的命名方式就不足为奇了,毕竟最初我们只考虑将图片存储在阿里云上。而我们把这种包含“aliyun”字眼的方法,照抄到 PrivateImageStore
类中,显然是不合适的。如果我们在新类中重新命名 uploadToAliyun()
、downloadFromAliyun()
这些方法,那就意味着,我们要修改项目中所有使用到这两个方法的代码,代码修改量可能就会很大
其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产 access token
,而私有云不需要 access token
。一方面,AliyunImageStore
中定义的 generateAccessToken()
方法不能照抄到 PrivateImageStore
中;另一方面,我们在使用 AliyunImageStore
上传、下载图片的时候,代码中用到了 generateAccessToken()
方法,如果要改为私有云的上传下载流程,这些代码都需要做调整。
那这两个问题该如何解决呢?解决这个问题的根本方法就是,在编写代码的时候,要遵从“基于接口而非实现编程”的原则
,具体来讲,我们需要做到下面这 3 点。
函数的命名不能暴露任何实现细节。比如,前面提到的 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);}
}
除此之外,很多人在定义接口的时候,希望通过实现类来反推接口的定义。先把实现类写好,然后看实现类中有哪些方法,照抄到接口定义中。如果按照这种思考方式,就有可能导致接口定义不够抽象,依赖具体的实现。这样的接口设计就没有意义了。不过,如果你觉得这种思考方式更加顺畅,那也没问题,只是将实现类的方法搬移到接口定义中的时候,要有选择性的搬移,不要将跟具体实现相关的方法搬移到接口中,比如 AliyunImageStore 中的 generateAccessToken() 方法。
总结一下,我们在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。在定义接口的时候,不要暴露任何实现细节
。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。
是否需要为每个类定义接口?
看了刚刚的讲解,你可能会有这样的疑问:为了满足这条原则,我是不是需要给每个实现类都定义对应的接口呢?在开发的时候,是不是任何代码都要只依赖接口,完全不依赖实现编程呢?做任何事情都要讲求一个“度”,过度使用这条原则,非得给每个类都定义接口,接口满天飞,也会导致不必要的开发负担。至于什么时候,该为某个类定义接口,实现基于接口的编程,什么时候不需要定义接口,直接使用实现类编程,我们做权衡的根本依据,还是要回归到设计原则诞生的初衷上来。只要搞清楚了这条原则是为了解决什么样的问题而产生的,你就会发现,很多之前模棱两可的问题,都会变得豁然开朗。
前面我们也提到,这条原则的设计初衷是,将接口和实现相分离,封装不稳定的实现,暴露稳定的接口
。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。
从这个设计初衷上来看,如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。
除此之外,越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫
。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。
回顾
.“
基于接口而非实现编程”,这条原则的另一个表述方式,是“基于抽象而非实现编程”
。后者的表述方式其实更能体现这条原则的设计初衷。我们在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性、可维护性。我们在定义接口的时候,一方面,命名要足够通用,不能包含跟具体实现相关的字眼;另一方面,与特定实现有关的方法不要定义在接口中。
“基于接口而非实现编程”这条原则,不仅仅可以指导非常细节的编程开发,还能指导更加上层的架构设计、系统设计
等。比如,服务端与客户端之间的“接口”设计、类库的“接口”设计。
总结
- 如果需求后续可能会变,就应该使用
接口
- 接口可以提高程序的
扩展性
- 如果预计需求不会变,就可以不用接口,但这是不可能的
参考
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
java 抽象类与接口的区别及其在jdk中的应用_鸭梨的博客-CSDN博客
为什么基于接口而非实现编程?相关推荐
- 软件工程 / 为什么基于接口而非实现编程?
基于接口而非实现编程(基于抽象而非实现编程)的目的是解耦. 这里面接口的含义可以理解为 dll 或者 so 文件对应的头文件中提供的函数列表,或者理解为C++中的抽象类. 该原则可以将接口和实现分离, ...
- Eclipse 基于接口编程的时候,快速跳转到实现类的方法(图文)
Eclipse 基于接口编程的时候,要跳转到实现类很麻烦,其实Eclipse已经实现该功能. 只要按照Ctrl键,把鼠标的光标放在要跳转的方法上面,第一个是跳转到接口里面,第二个方法是跳转到实现类的位 ...
- 基于python的modbus协议编程_通往未来的网络可编程之路:Netconf协议与YANG Model
近年来,随着全球云计算领域的不断发展与业务的不断增长,促使网络技术也不断发展,SDN技术应运而生,从最初的基于Openflow的转发与控制分离的核心思想,人们不断的去扩展SDN的外延,目前,人们可以达 ...
- C/C++非专家级编程
C/C++非专家级编程 0.关于定义,那是只有编译器才喜欢的语法--Peter Van Der Linden <C专家编程> 需要注意的是指针运算符*是右结合的,你最好从右往左读 cons ...
- 基于标准库函数与基于HAL库函数的stm32编程方式
文章目录 基于标准库函数与基于HAL库函数的stm32编程方式 一.基于标准库 1. 介绍 2. 库函数的STM32串口程序编写 二.基于HAL库 1.介绍 2.HAL库STM32串口程序编写 三.差 ...
- 基于标准库函数与基于HAL库函数的stm32编程方式对比
基于标准库函数与基于HAL库函数的stm32编程方式对比 一.标准库函数 二.HAL库函数 三.差异 四.stdunio IDE试玩 参考资料 一.标准库函数 1.标准库的解释 标准外设库(Stand ...
- 乐鑫esp8266学习rtos3.0笔记第7篇:我又来了,基于rtos3.0版本 SDK编程 SPI 驱动 ws2812b 七彩灯,代码全部开源奉献给你们!
本系列博客学习由非官方人员 半颗心脏 潜心所力所写,不做开发板.仅仅做个人技术交流分享,不做任何商业用途.如有不对之处,请留言,本人及时更改. 序号 SDK版本 内容 链接 1 nonos2.0 搭建 ...
- 基于MATLAB与VC混合编程的数字均衡器设计
1.概述 随着数字化技术的快速.深入发展,人们对数字化电子产品所产生的图像.图形以及声音等质量的要求越来越高.在实时数字处理过程中,与D/A和A/D转换相关的模拟信号重构过程是决定数字系统输出质量的关 ...
- SQL Servr 2008空间数据应用系列六:基于SQLCRL的空间数据可编程性
友情提示,您阅读本篇博文的先决条件如下: 1.本文示例基于Microsoft SQL Server 2008 R2调测. 2.具备 Transact-SQL 编程经验和使用 SQL Server Ma ...
最新文章
- django定时任务实现(言简意赅) Django折腾记之启动定时任务(转)
- C# 调用颜色的RGB值_RGB颜色转换十六进制颜色
- 区块链100讲:EOS环境搭建入门(私链节点-钱包-密钥-账号)
- Visual Studio 2022 预览版2 发布啦
- 安卓案例:帧式布局演示(切换颜色)
- linux图形界面装mysql_ubuntu 安装图形界面
- SDCMS 1.1sp1的XSS漏洞的挖掘与利用
- 问题六:C++中是干嘛用的(引用类型)
- ajax传单参数接受不了,Choropleth传单ajax
- 端口扫描 -- Masscan-Gui
- 企业微信对接CRM销售系统,助力企业客户增长
- [报错]CXF动态客户端报错:No operation was found with the name
- Python 操作Mongodb 聚合前过滤筛选
- 俞敏洪:人生最重要的两件事是什么?
- 这些前端资源,你值得拥有
- java版汉字转换拼音(大小写)
- Pr 入门教程如何个性化“时间轴”面板?
- 基于codesys开发的多轴运动控制程序框架,将逻辑与运动控制分离,将单轴控制封装成功能块,对该功能块的操作包含了所有的单轴控制
- 学习高博SLAM(1)
- 关于Zion真实性问题的图文分析及其他 V1.06
热门文章
- 水晶报表中对某一栏位值进行处理_终于有人讲清楚了,BI和报表的差异!
- Ubuntu18.04下QSqlDatabase: QMYSQL driver not loaded
- Android Stduio启动模拟器运行项目时做了什么
- c语言 wchar_t,一个【wchar_t】引发的学案
- jvm maxgcpausemillis 默认值_Tomcat和JVM的性能调优总结
- oracle的解析計劃,Oracle中获取执行计划的几种方法分析
- php 变量存活期,php 变量生命周期:PHP源码分析-PHP的生_php
- 对比Hashtable、HashMap、TreeMap有什么不同(转)
- li ul vue 滚动显示_vue ul循环滚动的问题
- android性能调优的工具,神兵利器-Android 性能调优工具 Hugo