接口隔离原则:接口里的方法,你都用得到吗?

news2024/9/21 4:29:43

文章目录

  • 前言
  • 接口隔离原则
    • 1、角色的合理划分
    • 2、定制服务
    • 3、接口污染
  • 胖接口减肥
  • 总结



前言

在前面几篇文章中中,我们讲的设计原则基本上都是关于如何设计一个类。SRP 告诉我们,一个类的变化来源应该是单一的;OCP 说,不要随意修改一个类;LSP 则教导我们应该设计好类的继承关系。

而在面向对象的设计中,接口设计也是一个非常重要的组成部分。我们一直都在强调面向接口编程,想实现 OCP 也好,或者是下一讲要讲的 DIP 也罢,都是要依赖于接口实现的。

也许你会说,接口不就是一个语法吗?把需要的方法都放到接口里面,接口不就出来了吗?顶多是 Java 用 interface,C++ 都声明成纯虚函数。这种对于接口的理解,显然还停留在语法的层面上。这样设计出来的只能算作是有了一个接口,但想要设计出好的接口,还要有在设计维度上的思考。

那什么样的接口算是一个好接口呢?这就需要我们了解接口隔离原则。

接口隔离原则

不应强迫使用者依赖于它们不用的方法。
No client should be forced to depend on methods it does not use.

这个表述看上去很容易理解,就是指在接口中,不要放置使用者用不到的方法。站在使用者的角度,这简直再合理不过了。每个人都会觉得,我怎么会依赖于我不用的方法呢?相信作为设计者,你也会同意这种观点。然而,真正在设计的时候,却不是人人都能记住这一点的。

那么到底该怎么去理解这个接口隔离原则呢?我觉得可以从三个方面去理解这个事情。

1、角色的合理划分

将“接口”理解为一个类所提供的所有的方法的特征集合,也就是一种在逻辑上才存在的概念,这样的话,接口的划分其实就是直接在类型上的划分。

其实可以这么想,一个接口就相当于剧本中的一个角色,而这个角色在表演的过程中,决定由哪一个演员来进行表演就相当于是接口的实现,因此,一个接口代表的应当是一个角色而不是多个角色,如果系统涉及到多个角色的话,那么每一个角色都应当由一个特定的接口代表才对。

而为了避免我们产山混淆的想法,这时候我们就可以把接口隔离原则理解成角色隔离原则。

2、定制服务

将接口理解成我们开发中狭义的JAVA接口的话,这样子,接口隔离原则讲的就是为同一个角色提供宽窄不同的接口,来应对不同的客户端内容,我画一个简单的图示,大家就完全能明白了。

15672.jpg

上面这个办法其实就可以称之为定制服务,在上面的图中有一个角色service以及三个不同的客户端,这三个Client需要的服务是不一样的,所以我给他分成了是三个接口,也就是Service1,Service2和Service3,显而易见,每一个JAVA接口,都仅仅是将Cilent需要的行为暴露给Client,而没有将不需要的方法暴露出去。

其实了解设计模式的很容易就想到这是适配器模式的一个应用场景,我不细聊适配器模式,设计模式我们在知识星球中会进行讲解。

3、接口污染

这句话的意思就是过于臃肿的接口就是对接口的污染。

由于每一个接口都代表一个角色,实现一个接口对象,在他的整个生命周期中,都扮演着这个角色,因此将角色分清就是系统设计的一个重要的工作。因此一个符合逻辑的判断,不应该是将几个不同的角色都交给一个接口,而是应该交给不同的接口来进行处理。

准确而恰当的划分角色以及角色所对应的接口,就是我们面向对象设计中的一个重要的组成部分,如果将没有关系或者关系不大的接口整合到一起去的话,那就是对角色和接口的污染。

胖接口减肥

假设有一个银行的系统,对外提供存款、取款和转账的能力。它通过一个接口向外部系统暴露了它的这些能力,而不同能力的差异要通过请求的内容来区分。所以,我们在这里设计了一个表示业务请求的对象,像下面这样:

class TransactionRequest {
  // 获取操作类型
  TransactionType getType() {
    ...
  }
  
  // 获取存款金额
  double getDepositAmount() {
    ...
  }
  
  // 获取取款金额
  double getWithdrawAmount() {
    ...
  }
  
  // 获取转账金额
  double getTransferAmount() {
    ...
  }
}

每种操作类型都对应着一个业务处理的模块,它们会根据自己的需要,去获取所需的信息,像下面这样:

interface TransactionHandler {
  void handle(TransactionRequest request)}

class DepositHandler implements TransactionHandler {
  void handle(final TransactionRequest request) {
    double amount = request.getDepositAmount();
    ...
  }
}

class WithdrawHandler implements TransactionHandler {
  void handle(final TransactionRequest request) {
    double amount = request.getWithdrawAmount();
    ...
  }
}

class TransferHandler implements TransactionHandler {
  void handle(final TransactionRequest request) {
    double amount = request.getTransferAmount();
    ...
  }
}

这样一来,我们只要在收到请求之后,做一个业务分发就好了:

TransactionHandler handler = handlers.get(request.getType());
if (handler != null) {
  handler.handle(request);
}

一切看上去都很好,不少人在实际工作中也会写出类似的代码。然而,在这个实现里,有一个接口就太’‘胖’'了,它就是 TransactionRequest。

TransactionRequest 这个类包含了相关的请求内容,虽然这是无可厚非的。但是在这里,我们容易直觉地把它作为参数传给 TransactionHandler。于是,它作为一个请求对象,摇身一变,变成了业务处理接口的一部分。

正如我在前面所说的,虽然你没有设计特定的接口,但具体类可以变成接口。不过,作为业务处理中的接口,TransactionRequest 就显得“胖”了:

  • getDepositAmount 方法只在 DepositHandler 里使用;
  • getWithdrawAmount 方法只在 WithdrawHandler 里使用;
  • getTransferAmount 只在 TransferHandler 使用。

然而,传给它们的 TransactionRequest 却包含所有这些方法。

也许你会想,这有什么问题吗?问题就在于,一个“胖”接口常常是不稳定的。比如说,现在要增加一个生活缴费的功能,TransactionRequest 就要增加一个获取生活缴费金额的方法:

class TransactionRequest {
  ...
  
  // 获取生活缴费金额
  double getLivingPaymentAmount() {
    ...
  }
}

相应地,还需要增加业务处理的方法:

class LivingPaymentHandler implements TransactionHandler {
  void handle(final TransactionRequest request) {
    double amount = request.getLivingPaymentAmount();
    ...
  }
} 

虽然这种做法看上去还挺符合 OCP 的,但实际上,由于 TransactionRequest 的修改,前面几个写好的业务处理类:DepositHandler、WithdrawHandler、TransferHandler 都会受到影响。为什么这么说呢?

如果我们用的是一些现代的程序设计语言,你的感觉可能不明显。假如这段代码是用 C/C++ 这些需要编译链接的语言写成的,TransactionRequest 的修改势必会导致其它几个业务处理类重新编译,因为它们都引用了 TransactionRequest。

实际上,C/C++ 的程序在编译链接上常常需要花很多时间,除了语言本身的特点之外,因为设计没做好,造成本来不需要重新编译的文件也要重新编译的现象几乎是随处可见的

你可以理解为,如果一个接口修改了,依赖它的所有代码全部会受到影响,而这些代码往往也有依赖于它们实现的代码,这样一来,一个修改的影响就传播出去了。用这种角度去评估,你就会发现,不稳定的“胖”接口影响面是非常之广的,所以,我们说“胖”接口不好。

怎样修改这段代码呢?既然这个接口是由于“胖”造成的,给它减肥就好了。根据 ISP,只给每个使用者提供它们关心的方法。所以,我们可以引入一些“瘦”接口:

interface TransactionRequest {
}

interface DepositRequest extends TransactionRequest {
  double getDepositAmount();
}

interface WithdrawRequest extends TransactionRequest {
  double getWithdrawAmount();
}

interface TransferRequest extends TransactionRequest {
  double getTransferAmount();
}

class ActualTransactionRequest implements DepositRequest, WithdrawRequest, TransferRequest {
  ...
}

这里,我们把 TransactionRequest 变成了一个接口,目的是给后面的业务处理进行统一接口,而 ActualTransactionRequest 则对应着原来的实现类。我们引入了 DepositRequest、WithdrawRequest、TransferRequest 等几个“瘦”接口,它们就是分别供不同的业务处理方法使用的接口。

有了这个基础,我们也可以改造对应的业务处理方法了:

interface TransactionHandler<T extends TransactionRequest> {
  void handle(T request)}


class DepositHandler implements TransactionHandler<DepositRequest> {
  void handle(final DepositRequest request) {
    double amount = request.getDepositAmount();
    ...
  }
}


class WithdrawHandler implements TransactionHandler<WithdrawRequest> {
  void handle(final WithdrawRequest request) {
    double amount = request.getWithdrawAmount();
    ...
  }
}


class TransferHandler implements TransactionHandler<TransferRequest> {
  void handle(final TransferRequest request) {
    double amount = request.getTransferAmount();
    ...
  }
}

经过这个改造,每个业务处理方法就只关心自己相关的业务请求。那么,新增生活缴费该如何处理呢?你可能已经很清楚了,就是再增加一个新的接口:

interface LivingPaymentRequest extends TransactionRequest {
  double getLivingPaymentAmount();
}

class ActualTransactionRequest implements DepositRequest, WithdrawRequest, TransferRequest, LivingPaymentRequest {
}

然后,再增加一个新的业务处理方法:

class LivingPaymentHandler implements TransactionHandler<LivingPaymentRequest> {
  void handle(final LivingPaymentRequest request) {
    double amount = request.getLivingPaymentAmount();
    ...
  }
}

我们可以对比一下两个设计,只有 ActualTransactionRequest 做了修改,而因为这个类表示的是实际的请求对象,在现在的结构之下,它是无论如何都要修改的。而其他的部分因为不存在依赖关系,所以,并不会受到这次需求增加的影响。相对于原来的做法,新设计改动的影响面变得更小了。

总结

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

其实接口隔离原则其实也算是“看人下菜碟”,它的意思就是要看客人是谁,在提供不同档次的饭菜。

从接口隔离原则的角度出发的话,要根据客户不同的需求,去指定不同的服务,这就是接口隔离原则中推荐的方式。

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

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

相关文章

Fiddler抓包工具之fiddler设置断点和简单的并发测试

断点有两种方式&#xff1a; 1、全局断点 2、局部断点 全局断点 全局断点的特点是&#xff1a;不能针对一个请求&#xff0c;是给所有抓到的请求打断点 全局断点如何设置&#xff1a; 1、快速设置断点&#xff1a;直接点击底部状态栏断点处 &#xff1b;点击第一下是请求…

比赛记录:Educational Codeforces Round 149 (Rated for Div. 2) A~D

传送门:CF 前提提要:这场狠狠的掉分.C题刚开始少了一个特判,导致自己对自己的构造方法产生了疑问,然后就一直在做无用思考,后来交的时候排名就贼后面,然后D题的题面简直稀烂(虽然D题看懂之后极其简单…),赛时根本看不懂D题意,最终rating掉完.不亏是教育场,被狠狠的教育了 A题…

Web的基本漏洞--命令执行漏洞

目录 一、命令执行漏洞 1.命令执行漏洞的原理 2.命令执行漏洞分类 3.命令执行漏洞的危害 4.命令执行漏洞的防范措施 5.命令执行漏洞的绕过 一、命令执行漏洞 命令执行漏洞是指攻击者可以随意执行系统命令。它属于高危漏洞之一&#xff0c;也属于代码执行的范畴。命令执行…

内置工具横向移动

IPCSchtasks IPC: IPC$是共享"命令管道"的资源&#xff0c;它是为了让进程通信而开放的命名管道&#xff0c;连接双方可以建立安全的通道并以此通道进行加密数据交换&#xff0c;从而实现对远程计算机的访问。 利用条件&#xff1a; 1、开放139、445 2、目标开启…

Node.js V10.24.1 安装步骤(node、cnpm、yarn、vue)

一、下载node.js 下载地址&#xff1a;Download | Node.js 要下载历史低版本请点击“Previous Releases” Previous Releases 本文章以V10.24.1为例 &#xff0c;下载64位msi 二、安装 下载完成后&#xff0c;一直点击Next直到安装完成&#xff0c;可以自己修改安装位置。…

09.二叉树

09.二叉树 1.树型结构 1.1概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。它具有以下的特点&…

收到字节offer,我却拒绝了...

前言&#xff1a; 大四快毕业了&#xff0c;在等待读研的期间无事可做&#xff0c;所以打算暑期找个实习。 忠告&#xff1a; 本人投了字节某测试岗&#xff0c;看到要求是测开的要求&#xff08;科班出身需要熟悉一种语言&#xff09;&#xff0c;就以为面完发技术岗的offe…

note注解

元注解 注解在注解上面的注解称为元注解。主要有以下五种。 Retention 表明注解存活时间 Documented 将注解元素放到Javadoc文档中 Target 注解可以使用到的地方 在ElementType[]中主要有以下几种类型 TYPE&#xff1a;类型&#xff08;比如类、注解、枚举&#xff09; FIELD&…

Refresh解析

目录 后置处理器PostProcessor PostProcessor的种类 Aware 事件监听模式 Spring容器的刷新逻辑 ApplicationContext refresh的流程概述 1---prepareRefresh 2---obtainFreshBeanFactory 3---prepareBeanFactory 4---postProcessBeanFactory 5---invokeBeanFactoryPo…

【正点原子STM32连载】 第二十一章 通用定时器实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第二十…

海康萤石摄像头本地局域网拉流保存

海康萤石的视频cp1型号&#xff0c;获取局域网的视频流&#xff1a;rtsp 拉流方法 首先需要在软件中打开rtsp开关&#xff0c;然后使用nmap工具测试下摄像头开放的端口号&#xff0c;一定会有一个554端口开放 $ nmap 192.168.0.105 PORT STATE SERVICE 554/tcp open r…

什么是Flink CDC,以及如何使用

什么是Flink CDC&#xff0c;以及如何使用 CDC介绍Flink CDC适用于场景&#xff1f;Flink CDC 的简单用例数据库配置创建数据库和相应的表开启mysql数据库bin-log日志1.如果是服务器2.如果在Windows使用小皮 搭建Flink CDC java环境添加maven相关pom构建Sinkmain配置运行 操作数…

盘点中国开发的有影响力的编程技术产品,道阻且长,行则将至

计算机诞生之后&#xff0c;程序员需要给计算机发送指令&#xff0c;能够准确地定义计算机所需要使用的数据&#xff0c;并精确地定义在不同情况下所应当采取的行动。计算机需要能够识别并执行指令。这个“指令”就是我们今天的“编程语言”&#xff0c;一种计算机和人都能识别…

【十】设计模式~~~结构型模式~~~享元模式(Java)

【学习难度&#xff1a;★★★★☆&#xff0c;使用频率&#xff1a;★☆☆☆☆】 5.1. 模式动机 面向对象技术可以很好地解决一些灵活性或可扩展性问题&#xff0c;但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时&#xff0c;将导致运行代价过高&#xff0…

路径规划算法:基于自私羊群优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于自私羊群优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于自私羊群优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

图解max{X,Y}和min{X,Y}并求相关概率

图解max{X,Y}和min{X,Y}并求相关概率 对max{X,Y}或min{X,Y}进行分解再求解 P ( m a x { X , Y } ≥ c ) P [ ( X ≥ c ) ∪ ( Y ≥ c ) ] P ( m a x { X , Y } ≤ c ) P [ ( X ≤ c ) ∩ ( Y ≤ c ) ] P ( m i n { X , Y } ≥ c ) P [ ( X ≥ c ) ∩ ( Y ≥ c ) ] P ( m i…

2022年12月1日郑州大学新闻与传播学院副院长博导郑*侠—社科申报注意事项

郑老师&#xff08;1项重点社科&#xff0c;2项青年社科&#xff09;&#xff0c; 一、选题 随时关注 最新国家层面信息&#xff1a;二十大报告&#xff0c;重要文件&#xff0c;重点项目信息 选题方向&#xff1a;问题导向 时代问题—学术问题—现实问题 题目命名&#xf…

基于微信小程序的社区生活管理

一&#xff1a;系统使用到的技术栈 SSMJSP原生JSMysql微信小程序 二&#xff1a;表结构 三&#xff1a;功能截图 四.源码获取

【工具学习】- Python通过dxfgrabber库获取CAD信息

Python - dxfgrabber库获取CAD信息 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 一个有梦有戏的人 怒放吧德德 &#x1f31d;分享学习心得&#xff0c;欢迎…

如何利用Stable Diffusion WebUI快速制作漫画开源

今天给大家带来的是如何利用Stable Diffusion WebUI快速制作漫画。 欢迎大家可以关注我的公众号《乔说科技》&#xff0c;关于如何制作动漫前置学习&#xff0c;请点击如下文章查看相关技术&#xff1a; &#xff08;1&#xff09;如何安装绘图工具Stable Diffusion WebUI&…