设计模式学习笔记 - 设计模式与范式 -结构型:2.桥接模式:如何实现支持不同类型和渠道的消息推送系统?

概述

今天学习另外一种结构型模式:桥接模式。桥接模式的代码实现非常简单,但是理解起来稍微优点难度,并且应用场景也比较局限,所以,相对于代理模式来说,桥接模式在实际的项目中并没有那么常用,你只需要简单了解即可,见到了能认识就可以了。


桥接模式的原理解析

桥接模式,也叫做 “桥梁模式”,它是 23 种设计模式中最难理解的模式之一了。对于这个模式有两个不同的理解方式。

  • 在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。” 翻译成中文就是:“MsgSender”。
  • 还有另一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(多多个)维度可以独立进行扩展。” 通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,之前讲过的 “组合优于继承” 设计原则,所以,这里就不多做解释了。

我们重点看下 GoF 的理解方式。GoF 给出的定义非常简短,单凭这一句话,估计没几个人能看懂事什么意思。所以,我们通过 JDBC 驱动的例子来解释下。JDBC 驱动是桥接模式的经典应用。我们来看一下,如何利用 JDBC 驱动来查询数据库。具体的代码如下所示。

Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
    rs.getString(1);
    rs.getInt(2);
}

如果我们想要把 MySQL 数据库换成 Oracle 数据库,只要把第一行代码中的 com.mysql.jdbc.Driver 就可以了。当然,也有更灵活的实现方式,我们可以把需要加载的 Driver 类写到配置文件中,当程序启动的时候,自动从配置文件中加兹安,这样在切换数据的时候,我们都不需要修改代码,只需要修改配置文件就可以了。

不管是改代码还是改配置,在项目中,从一个数据库切换到另一个种数据库,都只需要改动很少的代码,或者完全不需要改动代码,那如此优雅的数据库切换是如何实现的呢?

源码之下无秘密。要弄清楚这个问题,我们先从 com.mysql.jdbc.Driver 这个类的代码看起。下面是少部分相关代码,放到了这里,你可以看一下。

package com.mysql.cj.jdbc;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

结合 com.mysql.jdbc.Driver 的代码实现,可以发现,当执行 Class.forName("com.mysql.jdbc.Driver") 这条语句时,实际上是做了两件事情。

  • 第一件事情,是要求 JVM 查找并加载指定的 Driver 类,
  • 第二件事情,是执行改类的静态代码,也就是将 MySQL Driver 注册到 DriverManager 类中。

现在,再看下, DriverManager 类是干什么的。具体代码如下所示。当我们把具体的 Driver 实现类(比如 com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver),这也是可以灵活切换 Driver 的原因。

public class DriverManager {
    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    // ...
    
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

	@CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    
    // ...

}

桥接模式的定义是 “将抽象和实现解耦,让它们可以独立变化”。弄懂 “抽象” 和 “实现” 两个概念,是理解桥接模式的关键。

  • 在 JDBC 例子中,JDBC 本身相当于抽象。注意,这里说的 “抽象”,并非指 “抽象类” 或 “接口”,而是根具体的数据库无关的、被抽象出来的一套 “类库”。
  • 具体的 Driver (比如,com.mysql.jdbc.Driver)就相当于实现。这里说的 “实现”,并非指 “接口的实现类”,而是根具体的数据库相关的一套 “类库”。

JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。

下面画一张图帮你你理解。

在这里插入图片描述

桥接模式的应用举例

在 《设计原则 - 2.开闭原则》中,我们讲过一个 API 接口告警的例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知的紧急程度有多种类型,包括:SERVER(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道。比如,SERVER(严重)基本的消息会通过 “自动语音电话” 告知相关人员。

在当时的代码实现中,关于发送告警信息那部分代码,我们只给出了粗略的设计,现在我们来一块实现下。先来看最简单、最直接的一种实现方式。

public enum NotificationEmergencyLevel {
    SERVER, URGENCY, NORMAL, TRIVIAL
}

public class Notification {
    private List<String> emailAddress;
    private List<String> telephones;
    private List<String> wechatIds;

    public Notification() {}

    public void setEmailAddress(List<String> emailAddress) {
        this.emailAddress = emailAddress;
    }

    public void setTelephones(List<String> telephones) {
        this.telephones = telephones;
    }

    public void setWechatIds(List<String> wechatIds) {
        this.wechatIds = wechatIds;
    }

    public void notify(NotificationEmergencyLevel level, String message) {
        if (level.equals(NotificationEmergencyLevel.SERVER)) {
            // 自动语音电话...
        } else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
            // 自动发微信...
        } else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
            // 自动发邮件...
        } else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
            // 自动发邮件...
        }
    }
}

// API告警监控的例子中,通过如下方式来使用Notification类
public class ErrorAlertHandler extends AlertHandler {
    public ErrorAlertHandler(AlertRule rule, Notification notification) {
        super(rule, notification);
    }

    @Override
    public void check(ApiStatInfo apiStatInfo) {
        if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
            notification.notify(NotificationEmergencyLevel.SERVER, "...");
        }
    }
}

Notification 类的代码实现有一个最明显的问题,那就是有很多 if-else 分支逻辑。实际上,如果每个分支的代码不复杂,后期也没有无线膨胀的可能(增加更多的 if-else 分支判断),那这样的设计问题并不大,没有必要一定要摒弃 if-else 分支逻辑。

不过,Notification 的代码显然不符合这个条件。因为每个 if-else 分支中的代码逻辑都比较复杂,发送通知的所有逻辑都扎堆在 Notification 类中。我们知道,类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组合在一起。

针对 Notification 代码,可以将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender 相关类)。其中 Notification 类相当于抽象类,MsgSender 相当于实现类,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任何组合的意思是,不同紧急程度的消息和发送渠道的对应关系,不是在代码中固定写死,可以动态地去指定(比如,通过读取配置来获取对应的关系)。

按照这个设计思路,对代码进行重构。

public interface MsgSender {
    void send(String msg);
}

public class TelephoneMsgSender implements MsgSender {
    private List<String> telephones;

    public TelephoneMsgSender(List<String> telephones) {
        this.telephones = telephones;
    }

    @Override
    public void send(String msg) {
        // 自动语音电话...
    }
}

public class EmailMsgSender implements MsgSender {
    private List<String> emailAddress;

    public EmailMsgSender(List<String> emailAddress) {
        this.emailAddress = emailAddress;
    }

    @Override
    public void send(String msg) {
        // 自动发邮件...
    }
}

public class WechatMsgSender implements MsgSender {
    private List<String> wechatIds;

    public WechatMsgSender(List<String> wechatIds) {
        this.wechatIds = wechatIds;
    }

    @Override
    public void send(String msg) {
        // 自动发微信...
    }
}

public abstract class Notification {
    protected MsgSender msgSender;

    public Notification(MsgSender msgSender) {
        this.msgSender = msgSender;
    }

    public abstract void notify(String message);
}

public class ServerNotification extends Notification {
    public ServerNotification(MsgSender msgSender) {
        super(msgSender);
    }

    @Override
    public void notify(String message) {
        msgSender.send(message);
    }
}

public class UrgencyNotification extends Notification {
    public UrgencyNotification(MsgSender msgSender) {
        super(msgSender);
    }

    @Override
    public void notify(String message) {
        msgSender.send(message);
    }
}

public class NormalNotification extends Notification {
    public NormalNotification(MsgSender msgSender) {
        super(msgSender);
    }

    @Override
    public void notify(String message) {
        msgSender.send(message);
    }
}

public class TrivialNotification extends Notification {
    public TrivialNotification(MsgSender msgSender) {
        super(msgSender);
    }

    @Override
    public void notify(String message) {
        msgSender.send(message);
    }
}

总结

桥接模式的原理比较难理解,但代码实现相对简单些。

对于这个模式有两种不同的理解方式。

  • GoF 的《设计模式》中,桥接模式被定义为:“将抽象和实现解耦,让它们可以独立变化。
  • 在其他书籍和资料中,还有另一种更加简单的理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。

对于第一种 GoF 的理解方式,弄懂定义中的 “抽象” 和 “实现” 两个概念,是理解它的关键。

  • 定义中的 “抽象”,指的并非是抽象类或接口,而是被抽象出来的一套 “类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的 “实现” 来完成。
  • 而定义中的 “实现” 也并非接口的实现类,而是一套独立的 “类库”。

“抽象” 和 “实现” 独立开发,通过对象之间的组合关系,组装在一起。

对于第二种理解方式,它非常类似于我们之前讲过的 “组合优于继承” 设计原则,通过组合关系来替代继承关系,避免继承层次的指数级爆炸。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

全新Mistral-7B v0.2基础模型开源:32K上下文,开源界的性能巨兽

前言 在人工智能领域的发展历程中&#xff0c;开源大模型始终是推动技术进步与创新应用的关键力量。近日&#xff0c;Mistral AI再次引领开源潮流&#xff0c;发布了Mistral-7B v0.2基础模型&#xff0c;这不仅是对之前版本的升级&#xff0c;更是在性能与功能上的一次质的飞跃…

选择最佳图像处理工具OpenCV、JAI、ImageJ、Thumbnailator和Graphics2D

文章目录 1、前言2、 图像处理工具效果对比2.1 Graphics2D实现2.2 Thumbnailator实现2.3 ImageJ实现2.4 JAI&#xff08;Java Advanced Imaging&#xff09;实现2.5 OpenCV实现 3、图像处理工具结果 1、前言 SVD(stable video diffusion)开放了图生视频的API&#xff0c;但是限…

Mysql数据库:日志管理、备份与恢复

目录 前言 一、MySQL日志管理 1、存放日志和数据文件的目录 2、日志的分类 2.1 错误日志 2.2 通用查询日志 2.3 二进制日志 2.4 慢查询日志 2.5 中继日志 3、日志综合配置 4、查询日志是否开启 二、数据备份概述 1、数据备份的重要性 2、备份类型 2.1 从物理与…

【IJCAI‘23】港大提出社会推荐中的去噪自增强学习

论文标题&#xff1a; Denoised Self-Augmented Learning for Social Recommendation 收录会议&#xff1a; IJCAI 2023 论文链接&#xff1a; https://arxiv.org/abs/2305.12685 代码链接&#xff08;欢迎 ✨&#xff09;&#xff1a; https://github.com/HKUDS/DSL 港…

密码学及其应用1 —— 密码学概述

1 密码学的基本概念 1.1 网络安全的定义 网络安全是网络领域的一个专业领域&#xff0c;它涵盖了在基础计算机网络基础设施中所采取的措施、网络管理员为保护网络及网络可访问资源免受未授权访问而采纳的政策&#xff0c;以及对其有效性&#xff08;或无效性&#xff09;的持续…

Capture One Pro 23中文---颠覆性的图像编辑与色彩配置

Capture One Pro 23是一款功能强大且专业的RAW图像编辑处理软件。它拥有全球领先的色彩管理技术和精细的图像编辑工具&#xff0c;可以对图片进行多种精细调整&#xff0c;包括曝光、色温、对比度、锐度等&#xff0c;以满足用户特定的后期处理需求。此外&#xff0c;Capture O…

Linux离线安装mysql,node,forever

PS:本文是基于centos7实现的,要求系统能够查看ifconfig和unzip解压命令, 实现无网络可安装运行 首先现在百度网盘的离线文件包****安装Xftp 和 Xshell 把机房压缩包传到 home目录下****解压unzip 包名.zip 获取IP先获取到 linux 主机的ip ifconfig Xftp 连接输入IP,然后按照…

CentOS使用Docker部署Halo并结合内网穿透实现公网访问本地博客

文章目录 1. Docker部署Halo1.1 检查Docker版本如果未安装Docker可参考已安装Docker步骤&#xff1a;1.2 在Docker中部署Halo 2. Linux安装Cpolar2.1 打开服务器防火墙2.2 安装cpolar内网穿透 3. 配置Halo个人博客公网地址4. 固定Halo公网地址 本文主要介绍如何在CentOS 7系统使…

【Monero】Wallet RPC | Wallet CLI | 门罗币命令行查询余额、种子、地址等命令方法教程

ubuntu22.04 首先在运行daemon&#xff0c;详细安装运行教程可参考&#xff1a;The Monero daemon (monerod) ./monerodWallet CLI run ./monero-wallet-cli如果还没有钱包就根据提示创建钱包即可 输入密码 查询余额 balance查询种子 seed其他可执行命令操作&#xff1…

Spring Cloud - Openfeign 实现原理分析

OpenFeign简介 OpenFeign 是一个声明式 RESTful 网络请求客户端。OpenFeign 会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign 会将函数的参数值设置到这些请求模板中。虽然 OpenFeign 只能支持基于文本的网络请求,但是它可以极大简化网络请求的…

QT(3/22)

1>使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数&#xff0c;将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#…

【笔记】MJ Prompt

参数 --chaos 10 or --c 10, 0-10, defalut 0 --quality 1 or --q, 0.25-1, defalut 1 --iw 2, 0.5-2, --stylize 100 or --s 100, 0-1000, defalut 100 --cref URL --cw 100, 0-100stylize 风格化&#xff0c;MJ不同的出图模式&#xff0c;有默认的艺术风格&#xff0c;该值…

企业微信主体变更的公证书怎么办?

企业微信变更主体有什么作用&#xff1f; 企业微信推出到现在已经很多年了&#xff0c;但是之前一直不支持主体变更。于是很多公司好不容易积累的客户&#xff0c;因为换了营业执照经营&#xff0c;原来的客户就都只能流失了。近期企业微信终于放开了变更主体的功能&#xff0c…

C++细节

背景知识&#xff1a; 面向对象的编程中&#xff0c;类&#xff08;Class&#xff09;是创建对象的蓝图或模板&#xff0c;它包含了数据&#xff08;通常称为属性或变量&#xff09;和行为&#xff08;通常称为方法或函数&#xff09;。将数据封装为私有&#xff08;private&am…

babel起手式

Babel7 以下是各个 ECMAScript 版本引入的一些主要新语法和功能的汇总 ES5 / ECMAScript 5&#xff08;2009年&#xff09; 严格模式 "use strict"。JSON 对象。Array.prototype.forEach()、Array.prototype.map()、Array.prototype.filter()、Array.prototype.redu…

畅捷通T+ Ufida.T.DI.UIP.RRA.RRATableController 远程命令执行漏洞

一、漏洞信息 漏洞名称:畅捷通T+ Ufida.T.DI.UIP.RRA.RRATableController 远程命令执行漏洞 漏洞类别:远程命令执行漏洞 风险等级:高危 二、漏洞描述 畅捷通TPlus适用于异地多组织、多机构对企业财务汇总的管理需求;全面支持企业对远程仓库、异地办事处的管理需求;全…

2015年认证杯SPSSPRO杯数学建模A题(第二阶段)绳结全过程文档及程序

2015年认证杯SPSSPRO杯数学建模 A题 绳结 原题再现&#xff1a; 给绳索打结是人们在日常生活中常用的技能。对登山、航海、垂钓、野外生存等专门用途&#xff0c;结绳更是必不可少的技能之一。针对不同用途&#xff0c;有多种绳结的编制方法。最简单的绳结&#xff0c;有时称…

【手把手教学】如何可视化YOLOv8深度学习的网络结构并保存

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

Vue.js 3.4的新特性

Vue.js 3.4的新特性 目前&#xff0c;Vue.js的版本已经更新到3.4&#xff0c;这次更新不仅带来了性能上的飞跃&#xff0c;还引入了许多新特性&#xff0c;进一步优化了开发效率。 1. 性能提升 在性能方面&#xff0c;Vue.js 3.4 全新重写了模板解析器。与之前基于正则表达式…

如何将视频存储云端扫码调取?扫码看视频的在线制作方法

视频二维码是现在常用的一种分享视频的方法&#xff0c;其他人只需要扫描二维码就可以在手机上播放视频内容。采用这种方式可以获得更快的传播速度&#xff0c;而且视频存储在云端也不回占用扫码者的内容&#xff0c;通过点击消耗流量就可以查看视频内容&#xff0c;有效的提升…