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

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

相关文章

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

go中空接口的具体使用

《go中空接口的具体使用》空接口是一种特殊的接口类型,它不包含任何方法,本文主要介绍了go中空接口的具体使用,具有一定的参考价值,感兴趣的可以了解一下... 目录接口-空接口1. 什么是空接口?2. 如何使用空接口?第一,第二,第三,3. 空接口几个要注意的坑坑1:坑2:坑3:接口-空接口1. 什么是空接

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.