设计模式学习笔记 - 设计模式与范式 -结构型:4.适配器模式

概述

前面我们学习了代理模式、桥接模式、装饰器模式,本章再来学习一个比较常用的结构性模式:适配器模式。这个模式相对来说比较简单,应用场景也很具体。

关于适配器模式,有类适配器和对象适配器两种实现方式,以及常见的 5 种常见的应用场景。同时还会通过剖析 sl4j 日志框架,来给你展示这个模式在真实项目中的应用。此外,在本章的最后,还会对代理模式、桥接模式、装饰器模式和适配器模式,这四种结构非常相似的设计模式做简单的对比,对这几节的内容做一个简单的总结。


适配器模式的原理与实现

适配器模式(Adapter Design Pattern),就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是 USB 转接头充当适配器,把两种不兼容的接口,通过转换变得可以一起工作。

原理很简单,再看下它的代码实现。适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。具体的代码实现如下。其中 ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,AdaptorAdaptee 转化成一组符合 ITarget 接口定义的接口。

// 类适配器:基于继承
public interface ITarget {void f1();void f2();void fc();
}public class Adaptee {public void fa() { /*...*/ }public void fb() { /*...*/ }public void fc() { /*...*/ }
}public class Adaptor extends Adaptee implements ITarget {@Overridepublic void f1() {super.fa();}@Overridepublic void f2() {// 重新实现f2()...}// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}// 对象适配器:基于组合
public interface ITarget {void f1();void f2();void fc();
}public class Adaptee {public void fa() { /*...*/ }public void fb() { /*...*/ }public void fc() { /*...*/ }
}public class Adaptor implements ITarget {private Adaptee adaptee;public Adaptor(Adaptee adaptee) {this.adaptee = adaptee;}@Overridepublic void f1() {adaptee.fa(); // 委托给Adaptee}@Overridepublic void f2() {// 重新实现f2()...}@Overridepublic void fc() {adaptee.fc();}
}

针对这两种实现方式,在实际开发中,该如何选择呢?判断的标准主要有两个,一个是 Adaptee 接口的个数,另一个是 AdapteeITarget 的契合程度。

  • 如果 Adaptee 接口并不多,那两种实现方式都可以。
  • 如果 Adaptee 接口很多,而且 AdapteeITarget 接口定义大部分都相同,那推荐使用类适配器,因为 Adaptor 可以复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
  • 如果 Adaptee 接口很多,而且 AdapteeITarget 接口定义大部分都不同,那推荐使用对象适配器,因为组合结构相对于继承更加灵活。

适配器模式的应用场景

一般来说,适配器模式可以看做是一种 “补偿模式”,用来补救设计的缺陷。应用这种设计模式算是 “无奈之举”。如果在设计初期,就能协调规模接口不兼容的问题,那这种设计模式就没有应用的机会了。

适配器模式的应用场景是 “接口不兼容”。那实际的开发中,什么情况下才会出现接口不兼容呢?

1.封装有缺陷的接口设计

假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量的静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

具体代码如下所示:

public class CD { // 这个类来自外部SDK,无权修改// ...public static void staticFunction1() { /*...*/ }public void uglyNamingFunction2() { /*...*/ }public void toManyParamsFunction3(int paramA, int paramB, ...) { /*...*/ }public void lowPerformanceFunction1() { /*...*/ }
}// 使用适配器模式进行重构
public interface ITarget {void function1();void function2();void function3(ParamsWrapperDefinition paramsWrapper);void function4();
}// 注意:适配器类的命名不一定非得末尾带Adaptor
public class CDAdaptor extends CD implements ITarget {@Overridepublic void function1() {CD.staticFunction1();}@Overridepublic void function2() {super.uglyNamingFunction2();}@Overridepublic void function3(ParamsWrapperDefinition paramsWrapper) {super.toManyParamsFunction3(paramsWrapper.getParamA(), ...);}@Overridepublic void function4() {// reimplement it...}
}

2.统一多个类的接口设计

某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。

假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是每个系统提供的接口都是不同的,这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口都适配为统一的接口定义,这样我们就可以复用调用敏感词过滤的代码。

public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口// text是原始文本,函数输出用***替换敏感词之后的文本public String filterSexyWords(String text) {// ...}public String filterPoliticalWords(String text) {// ...}
}public class BSensitiveWordsFilter { // B敏感词过滤系统提供的接口// text是原始文本,函数输出用***替换敏感词之后的文本public String filter(String text) {// ...}
}public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口// text是原始文本,函数输出用***替换敏感词之后的文本public String filter(String text, String mask) {// ...}
}// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();public String filterSensitiveWords(String text) {String maskText = aFilter.filterSexyWords(text);maskText = aFilter.filterPoliticalWords(text);maskText = bFilter.filter(text);maskText = cFilter.filter(text, "***");return maskText;}
}// 使用适配器模式进行改造
public interface ISensitiveWordsFilter {String filter(String text);
}
public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {private ASensitiveWordsFilter aFilter;@Overridepublic String filter(String text) {String maskText = aFilter.filterSexyWords(text);maskText = aFilter.filterPoliticalWords(text);return maskText;}
}// 省略BSensitiveWordsFilterAdaptor、CASensitiveWordsFilterAdaptor...// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统
// 这个类完全不用动;而且基于接口而非实现编程,代码的可测试性更好public class RiskManagement {private List<ISensitiveWordsFilter> filters = new ArrayList<>();public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {filters.add(filter);}public String filterSensitiveWords(String text) {String maskText = text;for (ISensitiveWordsFilter filter : filters) {maskText = filter.filter(maskText);}return maskText;}
}

3.替换依赖的外部系统

当我们把项目中依赖的一个外部系统替换为另一个外部系统时,利用适配器模式,可以减少对代码的改动。具体的代码如下所示:

// 外部系统A
public interface IA {// ...void fa();
}public class A implements IA {// ...@Overridepublic void fa() { /*...*/ }
}// 在项目中使用外部系统A
public class Demo {private IA a;public Demo(IA a) {this.a = a;}// ...
}
Demo d = new Demo(new A());// 将外部系统A替换成外部系统B
public class BAdaptor implements IA {private B b;public BAdaptor(B b) {this.b = b;}@Overridepublic void fa() { //...b.fb();}
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方无需改动,只需要将BAdaptor如下注入到Demo即可
Demo d = new Demo(new BAdaptor(new B()));

4.兼容老版本接口

在做版本升级时,对于要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。这也可以粗略地看作适配器的一个应用场景。

JDK 1.0 中包含一个比那里集合容器的类 Enumeration。JDK 2.0 对这个类进行了重构,将它改名为 Iterator 类,并对它的代码实现做了优化。但是考虑到如果将 Enumeration 直接从 JDK 2.0 中删除,那使用 JDK 1.0 的项目如果切换到 JDK 2.0,代码就会编译不通过。为了避免这种情况,必须把项目中所有使用到 Enumeration 的地方,都修改为 Iterator 才行。

单独一个项目做 EnumerationIterator 的替换,勉强还能接受。但是,使用 Java 开发的项目太多了,一次 JDK 升级,导致所有的项目不做代码修改就会编译报错,这显然是不合理的。这就是我们经常所说的不兼容升级。为了做到兼容使用低版本的 JDK 的老代码,可以暂时保留 Enumeration,并将其实现替换为直接调用 Iterator。代码如下所示:

public class Collections {// ...public static <T> Enumeration<T> enumeration(final Collection<T> c) {return new Enumeration<T>() {private final Iterator<T> i = c.iterator();public boolean hasMoreElements() {return i.hasNext();}public T nextElement() {return i.next();}};}// ...
}

5.适配不同格式的数据

前面讲过,适配器模式主要用于接口的适配,实际上,它还可以用在不同的数据之间的适配。比如,把不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java 中的 Arrays.asList() 也可以看做是一种数据适配器,将数组类型的数据转化为集合容器类型。

List<String> names = Arrays.asList("Larry", "John", "Mike");

剖析适配器模式在 Java 日志中的应用

Java 中 有很多的日志框架,在项目开发中,常常用它们来打印日志信息。其中,比较常用的有 log4j、logback、JDK 提供的 JUL(java.util.logging)、Apache 的 JCL(Jakarta Commons Logging)等。

大部分日志框架都提供了相似的功能,比如按照不同级别打印日志等,但它们去并没有实现统一的接口。这主要可能是历史原因,没有一开始就制定接口规范。

如果只是开发一个自己用的项目,那用什么日志框架都可以,随便选一个就好。但是,如果开发的是一个集成到其他系统的组件、框架、类库等,那日志框架的选择就没有那么随意了。

比如,项目中的某个组件使用 log4j 来打印日志,而项目本身使用的是 logback。将组件引入到项目后,我们的项目就相当于有了两套日志打印框架。每种日志打印框架都有自己特有的配置方式。所以,我们要针对每种日志框架编写不同的配置文件(比如,日志存储的文件地址、打印日志的格式)。如果引入多个组件,每个组件使用的日志框架都不一样,那日志本身的管理工作就变得非常复杂。所以,为了解决这个问题,我们需要统一日志打印框架。

如果你是 Java 开发工程师,那 sl4j 这个日志框架你肯定不陌生,它相当于 JDBC 规范,提供了一套打印日志的统一接口规范。不过它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback、…)来使用。

不仅如此,sl4j 的出现晚于 JUL、JCL、log4j 等日志框架,所以,这些日志框架也不可能牺牲掉版本兼容性,将接口改造成符合 sl4j 接口规范。sl4j 事先考虑了这个问题,所以,它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框架的接口进行二次封装,适配成统一的 sl4j 接口定义。

所以在开发业务系统或者开发框架、组件的时候,我们统一使用 sl4j 提供的接口来编写打印日志的代码,具体使用哪种日期框架实现(log4j、logback、…),是可以动态指定的(使用 Java 的 SPI 技术),只需要将相应的 SDK 导入到项目中即可。

代理、桥接、装饰器、适配器 4 种设计模式的区别

代理、桥接、装饰器、适配器,这 4 种设计模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,都可以成为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式的用意完全不同。这里简单说下它们之间的区别:

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义 一个代理类,主要的目的是控制访问,而非加强功能,这是它跟装饰器模式的最大不同。
  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
  • 装饰器模式:装饰器模式在不改变原始类接口的情况下,对原始类功能进行增强,并支持多个装饰器的嵌套使用。
  • 适配器模式:适配器模式一种时候的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰者模式提供的都是跟原始类相同的接口。

总结

适配器模式是用来做适配,它将不兼容的接口转换为可以兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。适配器模式有两种实现方式:类适配器和对象适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

一般来说,适配器可以看作是一种 “补偿模式”,用来补救设计上的缺陷。应用这种模式算式 “无奈之举”,如果在设计初期,就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

在实际的开发中,什么情况下才会出现接口不兼容呢?我总结了下面这 5 种场景:

  • 封装有缺陷的接口
  • 统一多个类的接口设计
  • 替换依赖的外部系统
  • 兼容老版本接口
  • 适配不同格式的数据

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

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

相关文章

SQLServer数据库使用Function实现根据字段内容的拼音首字母进行数据查询

实现SQL首字母查询分两步&#xff0c;第一步建Function&#xff0c;第二步引用新建的Function。 1. 首先需要自定义一个查询的Function&#xff0c;详细SQL如下&#xff1a; ALTER function [dbo].[GetDataByPY](str nvarchar(4000)) returns nvarchar(4000) as begin decla…

【Java】MyBatis快速入门及详解

文章目录 1. MyBatis概述2. MyBatis快速入门2.1 创建项目2.2 添加依赖2.3 数据准备2.4 编写代码2.4.1 编写核心配置文件2.4.2 编写SQL映射文件2.4.3 编写Java代码 3. Mapper代理开发4. MyBatis核心配置文件5. 案例练习5.1 数据准备5.2 查询数据5.2.1 查询所有数据5.2.2 查询单条…

通过一个简单的自定义指令,探索 Vue 3.0 自定义指令的魅力

探索 Vue 3.0 自定义指令的魅力 在 Vue 3.0 中&#xff0c;自定义指令是一个非常强大的特性&#xff0c;它允许开发者在 Vue 组件中直接定义和使用自己的指令&#xff0c;从而扩展 Vue 的行为和功能。本文将探讨 Vue 3.0 自定义指令的相关知识&#xff0c;并介绍如何在实际项目…

Cisco ISR 4000 Series IOS XE Release IOSXE-17.13.1a ED

Cisco ISR 4000 Series IOS XE Release IOSXE-17.13.1a ED 思科 4000 系列集成服务路由器系统软件 请访问原文链接&#xff1a;https://sysin.org/blog/cisco-isr-4000/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 无耻抄…

zookeeper --禁用ACL 与 设置super超级用户1

一、禁用ACL 默认情况下&#xff0c;zookeeper是开启了ACL 权限控制的&#xff0c;如果你想禁用ACL&#xff0c;可以在配置文件中设置如下参数&#xff1a; skipACLtrue或者使用java 系统变量设置 -Dzookeeper.skipACLtrue二、设置super超级用户权限 super超级用户权限 是一…

Redis数据结构的基础插入操作

数据结构与内部编码 Redis常见的数据结构 数据结构和内部编码 数据结构的插入操作 在Redis中&#xff0c;数据结构的插入操作取决于你要插入的数据类型。以下是一些常见的数据结构和它们的插入操作&#xff1a; 字符串 (String)&#xff1a;使用 SET 命令来插入字符串。例…

yolov5+关键点检测实现溺水检测与警报提示(代码+原理)

往期热门博客项目回顾&#xff1a; 计算机视觉项目大集合 改进的yolo目标检测-测距测速 路径规划算法 图像去雨去雾目标检测测距项目 交通标志识别项目 yolo系列-重磅yolov9界面-最新的yolo 姿态识别-3d姿态识别 深度学习小白学习路线 //正文开始&#xff01; 人…

小狐狸ChatGPT付费AI创作系统V2.8.0独立版 + H5端 + 小程序前端

狐狸GPT付费体验系统的开发基于国外很火的ChatGPT&#xff0c;这是一种基于人工智能技术的问答系统&#xff0c;可以实现智能回答用户提出的问题。相比传统的问答系统&#xff0c;ChatGPT可以更加准确地理解用户的意图&#xff0c;提供更加精准的答案。同时&#xff0c;小狐狸G…

vue 插槽(二)

渲染作用域​ 插槽内容可以访问到父组件的数据作用域&#xff0c;因为插槽内容本身是在父组件模板中定义的。举例来说&#xff1a; <span>{{ message }}</span> <FancyButton>{{ message }}</FancyButton> 这里的两个 {{ message }} 插值表达式渲染…

Java算法之哈希算法

Java算法之哈希算法 哈希表 哈希表&#xff08;Hash Table&#xff09;&#xff0c;也称为散列表&#xff0c;是一种根据关键码值&#xff08;Key Value&#xff09;直接进行访问的数据结构。它通过哈希函数&#xff08;Hash Function&#xff09;将关键码值映射到哈希表中的…

红队攻防渗透技术实战流程:红队目标信息收集之批量信息收集

红队资产信息收集 1. 自动化信息收集1.1 自动化信息收集工具1.2 自动域名转换IP工具1.3 自动企业信息查询工具1.4 APP敏感信息扫描工具1.5 自动化信息工具的使用1.5.1 资产灯塔系统(ARL)1.5.1.1 docker环境安装1.2.2.9.1 水泽-信息收集自动化工具1. 自动化信息收集 1.1 自动化…

boost::asio 启用 io_uring(Linux 5.10)队列支持

欲启用 boost::asio 对于 io_uring 的支持&#xff0c;这需要以下几个先决条件&#xff1b; 1、boost 1.78 及以上发行版本 Revision History - 1.78.0 (boost.org) 2、Linux kernel 5.10 及以上发行版本 3、在预定义头文件&#xff08;stdafx.h&#xff09;、或编译器预定义…

###用sh ``` 用sh ``json失败

###用sh 用sh如果你希望使用 Shell 脚本来实现同样的功能&#xff0c;你可以编写一个简单的 Shell 脚本来执行。以下是一个示例 Shell 脚本&#xff0c;它可以读取 JSON 文件&#xff0c;并将每个章节保存到单独的文本文件中&#xff1a; #!/bin/bash# JSON 文件路径 json_fi…

口语 3.31

spamming :乱发垃圾信息 i keep getting spammed by bots appreciate you 感谢 its been forever&#xff1a;好久没见 i’m a pretty laid back ive been pretty laid back you know .我最近很轻松 clout&#xff1a;名气 im broke &#xff1a;我很穷 got robbed bl…

队列 和 同步状态

文章目录 同步状态阻塞队列如何使用队列来实现广度优先搜索&#xff08;BFS&#xff09;算法条件队列如何使用条件队列实现生产者消费者模型 同步状态 在多线程编程中&#xff0c;同步状态是指用于控制并发访问共享资源的状态。同步状态的正确管理是确保多线程操作安全性和正确…

【Linux实践室】Linux用户管理实战指南:用户权限切换操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;图形化界面登录2.2 &#x1f514;使用login…

Redis 的 Bitmap详解和命令演示

文章目录 Redis 的 Bitmap详解和命令演示特点&#xff1a;常见操作命令&#xff1a;应用场景&#xff1a;优缺点&#xff1a;优点&#xff1a;缺点&#xff1a; 下面是对 Redis 中 Bitmap&#xff08;位图&#xff09;操作命令的详细演示。1. SETBIT&#xff1a;设置位值2. GET…

vue 透传 Attributes

Attributes 继承​ “透传 attribute”指的是传递给一个组件&#xff0c;却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id。 当一个组件以单个元素为根作渲染时&#xff0c;透传的 attribute 会自动被添加到根…

Collection与数据结构 链表与LinkedList (一):链表概述与单向无头非循环链表实现

1.ArrayList的缺点 上篇文章我们已经对顺序表进行了实现,并且对ArrayList进行了使用,我们知道ArrayList底层是使用数组实现的. 由于其底层是一段连续空间&#xff0c;当在ArrayList任意位置插入或者删除元素时&#xff0c;就需要将后序元素整体往前或者往后搬移&#xff0c;时…

【python】pygame游戏框架

文章目录 pygame常用模块pygame:主模块,包含初始化、退出、时间、事件等函数。pygame.cdrom 访问光驱pygame.cursors 加载光驱pygame.joystick 操作游戏手柄或者类似的东西pygame.mouse:鼠标模块,包含获取、设置、控制等函数。pygame.key 键盘模块pygame.display:显示模块…