设计模式之美-为什么基于接口而非实现编程?有必要为每个类都定义接口吗?

news2024/11/25 3:46:48

        我愿意称之为最强设计书籍之一。看完这篇文章使我对代码编写有了新的思考。值得注意的是文章全篇写的是伪代码,并没有真正实现方法的逻辑,不过这样反而有利于逻辑的理解。

        在上一节课中,我们讲了接口和抽象类,以及各种编程语言是如何支持、实现这两个语法概念的。设计模式之美-接口vs抽象类的区别

        今天,我们继续讲一个跟“接口”相关的知识点:基于接口而非实现编程。这个原则非常重要。这个原则比较难理解此处的接口,作者将其定义为一种抽象。实际上,“基于接口而非实现编程”这条原则的另一个表述方式,是“基于抽象而非实现编程”。归根结底,是将不稳定或者是可能会随着时间推移业务代码要变更的部分封装起来,暴露稳定的,不会随着时间推移而变化的接口

如何将这条原则应用到实战中?

对于这条原则,我们结合一个具体的实战案例来进一步讲解一下。

假设我们的系统设计中有很多涉及图片处理存储的业务逻辑。图片经过处理之后被上传到阿里云上。为了代码复用,我们封装了图片存储相关的代码逻辑,提供了一个统一的AliyunImageStore类,供整个系统来使用。具体的代码实现如下所示:

整个上传流程包含三个步骤:创建bucket(你可以简单理解为存储目录)、生成access token访问凭证携带access token上传图片到指定的bucket中。代码实现非常简单,类中的几个方法定义得都很干净,用起来也很清晰,乍看起来没有太大问题,完全能满足我们将图片存储在阿里云的业务需求。

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

        然而,软件开发中唯一不变的就是变化。了一段时间后,我们自建了私有云,不再将图片存储到阿里云了,而是将图片存储到自建私有云上。为了满足这样一个需求的变化,我们该如何修改代码呢?

        我们需要重新设计实现一个存储图片到私有云的PrivateImageStore类,并用它替换掉项目中所有的AliyunImageStore类对象。这样的修改听起来并不复杂,只是简单替换而已,对整个代码的改动并不大。不过,我们经常说,“细节是魔鬼”。这句话在软件开发中特别适用。实际上,刚刚的设计实现方式,就隐藏了很多容易出问题的“魔鬼细节”,我们一块来看看都有哪些。

  • 首先,AliyunImageStore类中有些函数命名暴露了实现细节,比如,uploadToAliyun()和downloadFromAliyun()。如果开发这个功能的同事没有接口意识、抽象思维,那这种暴露实现细节的命名方式就不足为奇了,毕竟最初我们只考虑将图片存储在阿里云上。而我们把这种包含“aliyun”字眼的方法,照抄到PrivateImageStore类中,显然是不合适的。如果我们在新类中重新命名uploadToAliyun()、downloadFromAliyun()这些方法,那就意味着,我们要修改项目中所有使用到这两个方法的代码,代码修改量可能就会很大。
  • 其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产access token,而私有云不需要access token。一方面,AliyunImageStore中定义的generateAccessToken()方法不能照抄到PrivateImageStore中;另一方面,我们在使用AliyunImageStore上传、下载图片的时候,代码中用到了generateAccessToken()方法,如果要改为私有云的上传下载流程,这些代码都需要做调整。

        那这两个问题该如何解决呢?解决这个问题的根本方法就是,在编写代码的时候,要遵从“基于接口而非实现编程”的原则,具体来讲,我们需要做到下面这3点。

  1. 函数的命名不能暴露任何实现细节。比如,前面提到的uploadToAliyun()就不符合要求,应该改为去掉aliyun这样的字眼,改为更加抽象的命名方式,比如:upload()。
  2. 封装具体的实现细节。比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者。我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法,给调用者使用。
  3. 为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。

        总的来说,由编程者进行封装后,仅仅将upload和download的方法给调用者使用,其他方法不对外开放。

 首先,我们要定义一个接口,这个接口包含两个方法,upload和download。

public interface ImageStore {
  String upload(Image image, String bucketName);
  Image download(String url);
}

其次,我们对AliyunImageStore这个类进行重构。

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
  }
}

值得注意的是,upload和download的方法访问权限是public,其他的方法权限是private对调用者屏蔽。

然后,PrivateImageStore 私有云代码实现

// 上传下载流程改变:私有云不需要支持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的使用举例。

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

与上文的区别:不麻烦你们在翻到上面了。

  

        至此,对代码的重构已经完成,如果随时间变化,你们的私有云不再使用了,那么仅仅将new PrivateImageStore(...); 改变为new AliyunImageStore(...);即可。

        从侧面验证了“基于抽象而非实现编程”这一原则,具体我的理解是基于抽象编程,是体现了面向对象编程的思维,基于实现编程体现了面向过程编程的思维。

        也体现了虽然你的你使用了面向对象的语言,但你的编程思维还是基于过程的,因此我们要提高自己的抽象能力。

        总结一下,我们在做软件开发的时候,一定要有抽象意识封装意识接口意识!!!。在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。 

        现在我们回到题目的问题:是否需要为每个类定义接口

        看了刚刚的讲解,你可能会有这样的疑问:为了满足这条原则,我是不是需要给每个实现类都定义对应的接口呢?在开发的时候,是不是任何代码都要只依赖接口,完全不依赖实现编程呢?

        做任何事情都要讲求一个“度”,过度使用这条原则,非得给每个类都定义接口,接口满天飞,也会导致不必要的开发负担。至于什么时候,该为某个类定义接口,实现基于接口的编程,什么时候不需要定义接口,直接使用实现类编程,我们做权衡的根本依据,还是要回归到设计原则诞生的初衷上来。只要搞清楚了这条原则是为了解决什么样的问题而产生的,你就会发现,很多之前模棱两可的问题,都会变得豁然开朗。

        前面我们也提到,这条原则的设计初衷是,将接口和实现相分离,封装不稳定的实现,暴露稳定的接口上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/584952.html

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

相关文章

如何跑通一个java项目

查找项目代码的途径:github,码云,掘金网 以小说精品屋项目(掘金网)为例: 先读Readme(这里会介绍项目结构和技术选型),这里还会告诉你们怎么跑起来这个项目,比如让你先安装数据库,然…

从业者指南:专业编辑和校对技巧

在写作领域,编辑和校对是确保高质量作品的关键步骤。作为从业者,你需要掌握专业的编辑和校对技巧,以提高客户满意度和自己的市场竞争力。以下是一些值得关注的专业编辑和校对技巧。 1.建立良好的沟通 与客户保持良好的沟通是提高编辑和校对质…

如何更改 Linux 文件和目录权限?

在Linux系统中,文件和目录权限是安全性和访问控制的关键组成部分。正确设置文件和目录的权限可以确保只有授权的用户能够读取、写入或执行这些文件和目录。 本文将详细介绍如何在Linux系统中更改文件和目录的权限。 1. 文件和目录权限概述 在Linux系统中&#xff…

解决Kubernetes就绪检查导致网关不可用的问题

引言 在K8s环境中,由于就绪检查设置不合理的问题,导致出现网关不可用的情况。 本文将详细探讨这个问题的原因,并提供一些解决方案,帮助有需要的同学解决类似的问题。 注:网关使用 spring-cloud-gateway 问题描述 描…

记录两个Windows和Mac上部署阿里Canal无法启动的神坑

目录 一、问题列表 二、解决方案 三、参考资料 四、配置详解 五、数据库相关操作 一、问题列表 1、问题一:点击 startup.bat 窗口出现后立马闪退的问题。 2、问题二:启动后日志文件报错: ERROR com.alibaba.otter.canal.deployer.Cana…

办公OA系统性能分析案例

前言 信息中心老师反应,用户反馈办公系统有访问慢的情况,需要通过流量分析系统来了解系统的运行情况,此报告专门针对系统的性能数据做了分析。 信息中心已部署NetInside流量分析系统,使用流量分析系统提供实时和历史原始流量&am…

多项创新技术加持,实现零COGS的Microsoft Editor语法检查器

编者按:Microsoft Editor 是一款人工智能写作辅助工具,其中的语法检查器(grammar checker)功能不仅可以帮助不同水平、领域的用户在写作过程中检查语法错误,还可以对错误进行解释并给出正确的修改建议。神经语法检查器…

自动化测试框架的秘密,资深8年测试带你揭开,跟上测试“潮流“...

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 Python自动化测试&…

携手共创开源新格局|2023开放原子全球开源峰会将于6月11日在京隆重开幕

6月11-13日,2023开放原子全球开源峰会将在全球数字经济大会期间召开。本次峰会将以“开源赋能,普惠未来”为主题,通过开幕式暨高峰论坛、分论坛、主题展览、开源活动周等多种形式,聚集政、产、学、研、用、创、投、金等各领域优势…

RPC学习笔记【一】:概述

文章目录 一、简介1.1 引言1.2 架构的演变过程 二、RPC 的设计2.1 设计目标2.2 核心问题01 通信方式02 协议03 序列化04 远程代理类 2.3 衍生方案 - 注册中心 一、简介 1.1 引言 RPC 是远程过程调用 (Remote Procedure Call)的缩写形式,是一…

一文搞懂Python时间序列预测(步骤,模板,python代码)

预测包括,数值拟合,线性回归,多元回归,时间序列,神经网络等等 对于单变量的时间序列预测:模型有AR,MA,ARMA,ARIMA,综合来说用ARIMA即可表示全部。 数据和代码链接:数据和Jupyter文…

ArcGIS10.8下载及安装教程(附安装步骤)

谷歌云: https://drive.google.com/drive/folders/10igu7ZSMaR0v0WD7-2W-7ADJGMUFc2ze?uspsharing ArcGIS10.8 百度网盘: https://pan.baidu.com/s/1s5bL3QsCP5sgcftCPxc88w 提取码:kw4j 阿里云: https://www.aliyundriv…

Linux—实操篇:远程登录到linux服务器

远程登录客户端工具有 Xshell7(远程登录),Xftp7(文件传输),这里介绍Xshell和Xftp,其他的远程工具大同小异 1、远程登录Linux—Xshell 介绍:Xshell是目前最好的远程登录到Linux的软件,流畅的速度并且完美解…

如何制作污水处理流程图?简单方式说明

污水处理是指对污水进行处理,以使其能够满足环境保护和人类生产生活用水的需要。污水处理流程图是整个污水处理过程的图解,能够直观地展现污水处理的步骤和流程。 有很多方式可以制作流程图,比如一些站点可以在线制作,还兼具了思维…

chatgpt赋能python:Python下载代码:探索更快、更简单的方式

Python下载代码:探索更快、更简单的方式 Python是一个功能强大的编程语言,可以用来开发各种应用程序,从Web应用程序到数据科学和机器学习。作为一个高级语言,它通常看起来更易于理解和编写,相比其他编程语言更容易维护…

现阶段检验检测认证行业到底是一个什么样的行业?

为企业创造不一样的价值! TIC行业研究先行者、行业信息送水人! 内容摘要 此文章重点讲述了现阶段检验检测认证行业到底是一个什么样的行业,以及分析这个行业好与不好的明显特点。 此文章重点分析了现阶段检验检测认证行业的驱动力、竞争格…

Socket通信讲解及C/S结构实现UDP协议通信

Sokcet 一. Socket套接字 1.1 什么是套接字 所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制,是支持TCP/IP协议的路通…

java:map集合的应用,含代码以及输出样例

目录 1.Map集合 创建 基本的map使用方法 添加数据&#xff0c;打印数据 获取长度&#xff0c;删除元素&#xff0c;判断元素是否存在 map中常用的函数 1.Map集合 创建 Map<String,String> map new HashMap<>(); 基本的map使用方法 添加数据&#xff0c;打…

五.Glide

文章目录 前言1.with方法1.1 如何监听Glide的生命周期1.2 生命周期作用域Application、Activity、Fragment1.3 生命周期的绑定1.4 生命周期的监听1.5生命周期的回调 2.load方法3.into方法3.1 into方法的源码中流程走向3.2 Glide活动缓存的由来 4.Glide缓存机制4.1 资源封装4.2 …

(一)、Arcgis Server等一系列软件安装前准备

文章目录 &#xff08;一&#xff09;、win10修改机器名&#xff08;1&#xff09;、打开&#xff1a;设置->重命名这台电脑&#xff08;2&#xff09;、在弹出的系统属性框点击 "更改按钮"&#xff08;3&#xff09;、在弹出的计算机名/域更改点击 "其他&qu…