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

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

相关文章

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

OpenCV实现实时颜色检测的示例

《OpenCV实现实时颜色检测的示例》本文主要介绍了OpenCV实现实时颜色检测的示例,通过HSV色彩空间转换和色调范围判断实现红黄绿蓝颜色检测,包含视频捕捉、区域标记、颜色分析等功能,具有一定的参考... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间

Python实现精准提取 PDF中的文本,表格与图片

《Python实现精准提取PDF中的文本,表格与图片》在实际的系统开发中,处理PDF文件不仅限于读取整页文本,还有提取文档中的表格数据,图片或特定区域的内容,下面我们来看看如何使用Python实... 目录安装 python 库提取 PDF 文本内容:获取整页文本与指定区域内容获取页面上的所有文本内容获取

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal

Nexus安装和启动的实现教程

《Nexus安装和启动的实现教程》:本文主要介绍Nexus安装和启动的实现教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、Nexus下载二、Nexus安装和启动三、关闭Nexus总结一、Nexus下载官方下载链接:DownloadWindows系统根

SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程

《SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程》LiteFlow是一款专注于逻辑驱动流程编排的轻量级框架,它以组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑,下面给大... 目录一、基础概念1.1 组件(Component)1.2 规则(Rule)1.3 上下文(Conte

MySQL 横向衍生表(Lateral Derived Tables)的实现

《MySQL横向衍生表(LateralDerivedTables)的实现》横向衍生表适用于在需要通过子查询获取中间结果集的场景,相对于普通衍生表,横向衍生表可以引用在其之前出现过的表名,本文就来... 目录一、横向衍生表用法示例1.1 用法示例1.2 使用建议前面我们介绍过mysql中的衍生表(From子句