基于接口而非实现编程:有没有必要为每个类都定义接口

2024-05-29 07:20

本文主要是介绍基于接口而非实现编程:有没有必要为每个类都定义接口,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1.引言

2.接口的多种理解方式

3.设计思想实战应用

4.避免滥用接口

5.思考题


1.引言

        本节介绍一种与“接口”相关的设计思想;基于接口而非实现编程,它非常重要且在平时的开发中经常被用到。

2.接口的多种理解方式

     “基于接口而非实现编程”设计思想的英文描述是:“program to an interface, not an implementation”在理解这个设计思想的时候,我们不要一开始就与具体的编程语言挂钩,否则会局限在编语言的“接口”语法(如Java中的接口语法)中。这个设计思想最早出现在1994年出版的Erich Gamma 等4人合著的 Design Patterns: Elements of Reusable Object-Oriented Sofware 一书中。它先于很多编程语言诞生(如Java语言诞生于1995年),是一种抽象、泛化的设计思想。

        实际上,理解这个设计思想的关键,就是理解其中的“接口”两字。还记得我们在前面讲到的“接口”的定义吗?从本质上来看,“接口”就是一组“协议”或“约定”,是功能提供者提供给使用者的一个“功能列表”。“接口”在不同的应用场景下会有不同的解读,如服务端与客户端之间的“接口”,类库提供的“接口”,甚至,一组通信协议也可以称为“接口”。不过,这些对“接口”的理解都是偏上层和偏抽象的理解,与实际的代码编写关系不大。落实到具体的代码编写上,“基于接口而非实现编程”设计思想中的“接口”可以被理解为编程语言中的接口或抽象类。

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

        实际上,“基于接口而非实现编程”设计思想的另一个表述方式是“基于抽象而非实现编程”。后者其实更能体现这个设计思想的设计初衷。在软件开发中,比较大的挑战是如何应对需求的不断变化。抽象、顶层和脱离具体某一实现的设计能够提高代码的灵活性,从而可以更好地应对未来的需求变化。好的代码设计,不但能够应对当下的需求,而且在将来需求发生变化时,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象恰恰就是提高代码的扩展性、灵活性和可维护性的有效手段。        

3.设计思想实战应用

        我们通过一个具体的例子来介绍其如何应用“基于接口而非实现编程”设计思想,报设系统中多处涉及图片的处理和存铺相关逻辑、图片经过处理之后,被上传到阿里云中。为了代码复用,我们将图片存储相关的代码逻辑封装为统一的AliyunlmgeStore类,供整个系统使用。具体的代码实现如下。

public class AliyunImageStore {//.省略属性,构造函数等..public void createBucketIfNotExisting(String bucketName){//..省略刻建bucket的代码逻辑,失败时会抛出异常}public String generateAccessToken(){//...省路生成access Token的代码逻辑}public String uploadToAliyun(Image image, String bucketName, String accessToken){//...上传图片到阿里云}public Image downloadFromAliyun(String url, String accessToken){//...从阿里云下载图片}
}//AliyunImageStore类的使用示例
public class ImageProcessingJob{private static final String BUCKET_NAME = "ai_images_bucket";//...省略其他无关代码.public vid process(){Image image = ...;//处理图片,并封装为Image类的对象AliyunImageStore imageStore = new AliyunImageStore(/*省略参数*/);imagestore.createBucketIfNotExisting(BUCKET_NAME);String accessToken = imageStore.generateAccessToken();imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);}
} 

        图片的整个上传流程包含3个步骤:创建bucket(可以简单理解为存储目录)、生成accessToken访问凭证、携带 access Token 上传图片到指定的 bucket。

        上述代码简单、结构清晰,完全能够满足将图片存储到阿里云的业务需求。不过,软件开发中唯一不变的就是变化。过了一段时间,如果我们自建了私有云,不再将图片存储到阿里云,而是存储到自建私有云上,那么,为了满足这一需求变化,我们应该如何修改代码呢?我们需要重新设计实现一个存储图片到私有云的PrivateImageStore 类,并用它替换项目中所有用到 AliyunImageStore 类的地方。为了尽量减少替换过程中的代码改动,PivatelmageSiore类中需要定义与 AliyunImageStore 类相同的 public 方法,并且按照上传私有云的逻辑重新实现。但是,这样做存在下列两个问题。

        第一个问题: AliyunImageStore 类中有些函数的命名暴露了实现细节,如uploadToAliyun()和downloadFromAliyun()。如果我们在开发这个功能时没有接口意识、抽象思维,那么这种暴飞实现细节的命名方式并不足为奇,毕竟最初我们只需要考虑将图片存储到阿里云上。如果我们把这种包含“aliyun”字眼的方法照搬到 PrvateImageStore 类中,那么显然是不合适的。如果在新类中重新命名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();//1...省略上传图片到阿里云的代码逻辑.}public Image download(String url){String accessToken = generateAccessTokcn();//...省略从阿里云中下线图片的代码逻辑..}private void createBucketIMotExisting(String bucketName){//...省略创建bucket的代码逻辑,失败时会出异常。.}private String generateAccessToken(){//...省路生成accessToken的代码逻辑.}
}//上传和下载流程改变:私有云不需要支持access Token
public class PrivateImageStore implements ImageStore{pubiic String upload(Image image, string bucketName){createBucketINotExisting(bucketName);//1.省略上传图片到私有云的代码逻辑...}public Image download(String url){//..,省略从私有云中下载图片的代码逻辑.}private void cresteBucketIfotExisting(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 Privatelmagestore(...);imagestore.upload(image, BUCKET_NAME);}
}

        在定义接口时,很多工程师希望通过实现类来反推接口的定义,即先把实现类写好,再看实现类中有哪些方法,并照搬到接口定义中。如果按照这种思考方式,就有可能导致接口定义不够抽象、依赖具体的实现。这样的接口设计新没有意义了,不过,如果读者认为这种思考方式顺畅,那么可以接受, 但要注意,在将实现类中的方法搬移到接口定义中时,要有选择性地进行搬移,不要搬移与具体实现相关的方法,如AliyunImageStore类中的generateAccessToken()方法就不应该被搬移到接口中。

        总结一下,在编写代码时,我们一定要有抽象意识、封装意识和接口意识。接口定义不暴露任何实现细节。接口定义只表明做什么,不表明怎么做。而且,在设计接口时,我好细思考接口的设计是否通用,是否能够在将来某一天替换接口实现时,不需要改动任何定义。

4.避免滥用接口

        看了上面的讲解,读者可能有如下疑问:为了满足这个设计思想,是不是需要给每个实现类都定义对应的接口?是不是任何代码都要只依赖接口,不依赖实现编程呢?

        做任何事情都要讲求一个“度”。如果过度使用这个设计思想,非要给每个类都定义接口,接口“满天飞”,那么会产生不必要的开发负担。关于什么时候应该为某个类定义接口,以及什么时候不需要定义接口,我们进行权衡的根本还是“基于接口而非实现编程”设计思想产生的初衷。

        “基于接口而非实现编程”设计思想产生的初衷是,将接口和实现分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样,当实现发生变化时,上游系统的代码基本不需要做改动,以此降低代码的耦合性,提高代码的扩

        从这个设计思想的产生初衷来看,如果在业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那么没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类即可。还有,基于接口而非实现编程的另一种表述是基于抽象而非实现编程,即便某个功能的实现方式未来可能变化,如果不会有两种实现方式同时在被使用,就可以在原实现类中进行实现方式的修改。函数本身也是一种抽象,它封装了实现细节,只要函数定义足够抽象,不用接口也可以满足基于抽象而非实现的设计思想要求。

5.思考题

        在本节最终重构之后的代码中,尽管我们通过接口隔离了两个具体的类现。但是,项目中很地方都是通过类似下面的方式使用接口。这就会产生一个问题:如果需要替换图片存储方式,那么还是需要修改很多代码。对此,读者有什么好的实现思路吗?

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

这篇关于基于接口而非实现编程:有没有必要为每个类都定义接口的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1013010

相关文章

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

C#实现文件读写到SQLite数据库

《C#实现文件读写到SQLite数据库》这篇文章主要为大家详细介绍了使用C#将文件读写到SQLite数据库的几种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录1. 使用 BLOB 存储文件2. 存储文件路径3. 分块存储文件《文件读写到SQLite数据库China编程的方法》博客中,介绍了文

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

JAVA利用顺序表实现“杨辉三角”的思路及代码示例

《JAVA利用顺序表实现“杨辉三角”的思路及代码示例》杨辉三角形是中国古代数学的杰出研究成果之一,是我国北宋数学家贾宪于1050年首先发现并使用的,:本文主要介绍JAVA利用顺序表实现杨辉三角的思... 目录一:“杨辉三角”题目链接二:题解代码:三:题解思路:总结一:“杨辉三角”题目链接题目链接:点击这里

基于Python实现PDF动画翻页效果的阅读器

《基于Python实现PDF动画翻页效果的阅读器》在这篇博客中,我们将深入分析一个基于wxPython实现的PDF阅读器程序,该程序支持加载PDF文件并显示页面内容,同时支持页面切换动画效果,文中有详... 目录全部代码代码结构初始化 UI 界面加载 PDF 文件显示 PDF 页面页面切换动画运行效果总结主

SpringBoot实现基于URL和IP的访问频率限制

《SpringBoot实现基于URL和IP的访问频率限制》在现代Web应用中,接口被恶意刷新或暴力请求是一种常见的攻击手段,为了保护系统资源,需要对接口的访问频率进行限制,下面我们就来看看如何使用... 目录1. 引言2. 项目依赖3. 配置 Redis4. 创建拦截器5. 注册拦截器6. 创建控制器8.