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

目录

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.mzph.cn/news/843687.shtml

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

相关文章

SpringCloud之SSO单点登录-基于Gateway和OAuth2的跨系统统一认证和鉴权详解

单点登录(SSO)是一种身份验证过程,允许用户通过一次登录访问多个系统。本文将深入解析单点登录的原理,并详细介绍如何在Spring Cloud环境中实现单点登录。通过具体的架构图和代码示例,我们将展示SSO的工作机制和优势&a…

HCIP-Datacom-ARST自选题库__BGP多选【22道题】

1.BGP认证可以防止非法路由器与BGP路由器建立邻居,BGP认证可以分为MD5认证和Keychain认证,请问以下哪些BGP报文会携带BCGP Keychain认证信息?(报头携带) open Update Notication Keepalive 2.传统的BGP-4只能管理IPv4单播路由信息,MP-B…

Spring-Cloud-OpenFeign源码解析-04-调用流程分析

在Spring-Cloud-OpenFeign源码解析-03-FeignClientFactoryBean分析到,通过Autowired或者Resource注入FeignClient实例的时候,实际上返回的是JDK动态代理对象,具体的实现逻辑在InvocationHandler的invoke方法中 回看ReflectiveFeign.newInsta…

AI大模型日报#0528:Greg专访 | 为什么OpenAI最先做出GPT-4、xAI获60亿美元融资、李飞飞经典对话Hinton

导读:AI大模型日报,爬虫LLM自动生成,一文览尽每日AI大模型要点资讯!目前采用“文心一言”(ERNIE 4.0)、“零一万物”(Yi-34B)生成了今日要点以及每条资讯的摘要。欢迎阅读&#xff0…

git 查看远程分支地址

要查看 Git 远程仓库的地址(包括远程分支的 URL),你可以使用 git remote 命令结合其他选项。以下是一些常用的命令来查看远程仓库的信息: 查看所有远程仓库: 使用 git remote -v 或 git remote --verbose 命令可以列出…

YOLOv8/YOLOv7/YOLOv5+CRNN-车牌识别、车牌关键点定位、车牌检测(毕业设计)

目录 一、前言1、项目介绍2、图片测试效果展示 二、项目环境配置1、pytorch安装(gpu版本和cpu版本的安装)2、pycocotools的安装3、其他包的安装 三、yolov8/yolov7/yolov5CRNN-中文车牌识别、车牌关键点定位、车牌检测算法1、yolov8算法介绍2、CRNN算法介绍3、算法流…

【加密与解密(第四版)】第十三章笔记

第十三章 HOOK技术 13.1 Hook概述 IAT HOOK(改地址) BOOL IAT_InstallHook(){BOOL bResult FALSE ;HMODULE hCurExe GetModuleHandle(NULL);PULONG_PTR pt ;ULONG_PTR OrginalAddr;bResult InstallModuleIATHook(hCurExe,"user32.dll",&qu…

韩顺平0基础学Java——第13天

p264-p284 安装IDEA,熟悉一下软件。 尴尬了,难道是这个版本的idea不支持jdk17,难受住了 成功了,顺便跑一下昨天的作业: 这都要跑2秒?是电脑的问题还是谁的问题?控制台里跑的好快的哦 设置id…

Thingsboard规则链:Message type switch节点详解

在物联网解决方案中,数据的高效处理与自动化决策流程是实现智能化管理的基础。Thingsboard,作为一个强大的开源物联网平台,通过其规则引擎为用户提供了一系列灵活的节点来定制复杂的业务逻辑。其中,Message Type Switch节点是构建…

BookxNote Pro 宝藏 PDF 笔记软件

一、简介 1、BookxNote Pro 是一款专为电子书阅读和学习笔记设计的软件,支持多种电子书格式,如PDF和EPUB,能够帮助用户高效地管理和阅读电子书籍,同时具备强大的笔记功能,允许用户对书籍内容进行标注、摘录和思维导图绘…

PYTHON exec() 函数 变量作用域问题浅析总结

1. exec(‘拼接字符串’,globals, locals)函数作用 exec() 可在python 中通过传入字符串的方式,从而执行字符串内的各种命令或表达式 ---eval() 函数 与exec() 基本功能相同,唯一的区别,eval() 只可用于表达式计算并…

Springboot启动时报错Property ‘mapperLocations‘ was not specified.

这几天没整boot 晚上直接运行不了了 本想是在表现层写点代码测测接口的 localhost8080找半天 结果404 先考虑好久 是不是url输入错了 然后 就发现 结果boot都不能启动了 JUnit也测不出来 找了半天 结果是开关机导致数据库没开 手动打开服务 找到MySQL启动 IDEA连接数据…

正确解决java.util.EmptyStackException异常的有效解决方法

正确解决java.util.EmptyStackException异常的有效解决方法 文章目录 报错问题报错原因解决方法 报错问题 java.util.EmptyStackException异常 报错原因 java.util.EmptyStackException 是 Java 标准库中的一个异常,通常在使用 java.util.Stack 类时抛出。这个异常在…

ssm/springoot养老院问诊服务预约系统_96316老年人服务系统

2.管理员: (1)登入注册页面:管理员进行操作时需要是已注册登入的 (2)权限管理:管理员登入后可以运用权限进行相应的操作管理。 (3)用户管理:对用户进行删除、…

国产数据库替代加速 助力数字中国建设

5月24日,随着第七届数字中国建设峰会在福州的成功举办,释放数据要素价值、发展新质生产力成为当下热议的话题。 数据作为新型生产要素,是数字化、网络化、智能化的重要基础。北京人大金仓信息技术股份有限公司(以下简称人大金仓&a…

【quarkus系列】解决native包反射问题之RegisterForReflection 注解

背景 在使用 Quarkus 等框架时,反射机制可能是我们剥离spring框架之后做native包需要的解决问题。 首先先了解讨论为什么原生包(native image)不支持传统的反射机制呢?扩展一下知识点,两者之间的区别。 反射机制&…

论文阅读》通过混合潜在变量实现多样化、相关和连贯的开放领域对话生成 AAAI 2023

《论文阅读》通过混合潜在变量实现多样化、相关和连贯的开放领域对话生成 AAAI 2023 前言简介CVAECVAE 在 Transformer 中的应用模型架构Continuous Latent VariablesDiscrete Latent VariablesHybrid Latent Variables with Transformer损失函数Theoretical Results实验结果

C#面:用.NET做B/S结构的系统,是用几层结构来开发,每一层之间的关系以及为什么要这样分层

一般为3层: 表示层,业务逻辑层,数据层。 表示层(Presentation Layer): 表示层是用户与系统交互的界面,通常是通过 Web 页面或者桌面应用程序来实现。它负责接收用户的输入,展示数据…

OpenHarmony实战开发——宿舍全屋智能开发指南

项目说明 基于OpenAtom OpenHarmony(以下简称“OpenHarmony”)、数字管家开发宿舍全屋智能,实现碰一碰开门、碰一碰开灯、碰一碰开风扇以及烟感检测。因为各项目开发流程大体相似,本文主要以碰一碰开门为例介绍如何在现有OpenHar…

西储大学数据集学习

数据集下载地址:CWRU凯斯西储大学轴承数据数据集——附:下载链接_西储大学轴承数据集下载-CSDN博客 最近研究故障诊断,先对使用比较多的西储大学数据集研究。以资料【1】中的内容展开研究。 1、轴承的结构 轴承分为外圈、内圈、保持架和滚珠…