桥接模式:如何实现支持不同类型和渠道的消息推送系统?

        上一节课我们学习了第一种结构型模式:代理模式。它在不改变原始类(或者叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。代理模式在平时的开发经常被用到,常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。

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

桥接模式的原理

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

当然,这其中“最纯正”的理解方式,当属GoF的《设计模式》一书中对桥接模式的定义。毕竟,这23种经典的设计模式,最初就是由这本书总结出来的。在GoF的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化。”

关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,我们之前讲过的“组合优于继承”设计原则,所以,这里我就不多解释了。我们重点看下GoF的理解方式。

其关键在于避免继承层次的指数级爆炸。

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

Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
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换成oracle.jdbc.driver.OracleDriver就可以了。当然,也有更灵活的实现方式,我们可以把需要加载的Driver类写到配置文件中,当程序启动的时候,自动从配置文件中加载,这样在切换数据库的时候,我们都不需要修改代码,只需要修改配置文件就可以了。

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

package com.mysql.jdbc;
import java.sql.SQLException;/*** The Java SQL framework allows for multiple database drivers. Each driver should supply a class that implements the Driver interface** When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a* driver by doing Class.forName("foo.bah.Driver")*/
public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager//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的原因。

 这里仅仅展示DriverManager类的一些重要属性和方法,其他的进行省略方便展示。

public class DriverManager {private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();//...static {loadInitialDrivers();println("JDBC DriverManager initialized");}//...public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {if (driver != null) {registeredDrivers.addIfAbsent(new DriverInfo(driver));} else {throw new NullPointerException();}}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来执行。

有时候,我们可以简单的理解为抽象的是接口,实现是接口的实现。

 

具体可以学习:JDBC和桥接模式 - 咕~咕咕 - 博客园 (cnblogs.com)

桥接模式的应用举例。

API接口监控告警的例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知相关人员。

一个变化的维度是通知渠道:邮件、短信、微信和自动语言电话等等

一个变化的维度是通知紧急程度:严重、紧急、普通和无关紧要。

在我们来一块实现一下。我们先来看最简单、最直接的一种实现方式。代码如下所示:

public enum NotificationEmergencyLevel {SEVERE, URGENCY, NORMAL, TRIVIAL
}public class Notification {private List<String> emailAddresses;private List<String> telephones;private List<String> wechatIds;public Notification() {}public void setEmailAddress(List<String> emailAddress) {this.emailAddresses = 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.SEVERE)) {//...自动语音电话} 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);}@Overridepublic void check(ApiStatInfo apiStatInfo) {if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {notification.notify(NotificationEmergencyLevel.SEVERE, "...");}}
}

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

public interface MsgSender {void send(String message);
}public class TelephoneMsgSender implements MsgSender {private List<String> telephones;public TelephoneMsgSender(List<String> telephones) {this.telephones = telephones;}@Overridepublic void send(String message) {//...}}public class EmailMsgSender implements MsgSender {// 与TelephoneMsgSender代码结构类似,所以省略...
}public class WechatMsgSender implements MsgSender {// 与TelephoneMsgSender代码结构类似,所以省略...
}public abstract class Notification {protected MsgSender msgSender;public Notification(MsgSender msgSender) {this.msgSender = msgSender;}public abstract void notify(String message);
}public class SevereNotification extends Notification {public SevereNotification(MsgSender msgSender) {super(msgSender);}@Overridepublic void notify(String message) {msgSender.send(message);}
}public class UrgencyNotification extends Notification {// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {// 与SevereNotification代码结构类似,所以省略...
}

这样重构以后,就可以进行两个维度的扩展,继续增加通知渠道和通知的紧急类型。

总结:桥接模式的应用场景主要是针对于;一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。

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

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

相关文章

深度神经网络剪枝算法基础理论

非结构化剪枝可获得更高的剪枝率与精度&#xff0c;但是其非结构化特征带来的随机连接使得往往需要专门的软、硬件设计来支持其推理加速&#xff0c;而在现有的边缘硬件上难以满足其应用条件。鉴于此&#xff0c;目前在剪枝领域的研究多集中在结构化剪枝上&#xff0c;如图1.11…

2023-07-08:RabbitMQ如何做到消息不丢失?

2023-07-08&#xff1a;RabbitMQ如何做到消息不丢失&#xff1f; 答案2023-07-08&#xff1a; 1.持久化 发送消息时设置delivery_mode属性为2&#xff0c;使消息被持久化保存到磁盘&#xff0c;即使RabbitMQ服务器宕机也能保证消息不丢失。同时&#xff0c;创建队列时设置du…

DRF+Vue.JS前后端分离项目实例(下) --- Vue.js 前端实现代码

本文上篇请 点击阅读 1. 需求说明 本文以学生信息查询功能为例&#xff0c;采用前后端分离架构&#xff0c;后端提供RESTFul 接口&#xff0c;前端代码用Vue.js Bottstrap实现。 1.1 本例要求提供如下查询功能&#xff1a; 列表查询、单条查询 添加学生信息 更改学生信息 删…

JDK,JRE,JVM的区别

1.JVM JVM&#xff0c;也叫java虚拟机&#xff0c;用来运行字节码文件&#xff0c;可将字节码翻译为机器码&#xff0c;JVM是实现java跨平台的关键&#xff0c;可以让相同的java代码在不同的操作系统上运行出相同的结果。 2.JRE JRE&#xff0c;也叫java运行时环境&#xff…

基于Spring Boot的社区适龄青年征兵系统设计与实现(Java+spring boot+MySQL)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于Spring Boot的社区适龄青年征兵系统设计与实现&#xff08;Javaspring bootMySQL&#xff09; 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 后端&#xff1a;Java springboot框架 …

在工作与生活中保持情绪稳定的艺术

强烈的情绪波动&#xff1a;工作中的挑战 在我的职业生涯中&#xff0c;我经历过许多情绪波动的时刻。其中一个最具挑战性的时刻是在我负责一个重要项目的时候。我需要在短时间内完成大量的工作&#xff0c;同时还要管理一个由不同背景和技能的人组成的团队。这个项目的压力让…

flutter聊天界面-聊天列表 下拉加载更多历史消息

flutter聊天界面-聊天列表 下拉加载更多历史消息 在之前实现了flutter聊天界面的富文本展示内容、自定义表情键盘实现、加号【➕】更多展开相机、相册等操作Panel、消息气泡展示实现Flexible。这里把实现的聊天界面的滑动列表及下拉加载更多历史消息记录一下 聊天界面的列表使…

python与深度学习——基础环境搭建

一、安装jupyter notebook Jupyter Notebook是一个开源的交互式笔记本环境&#xff0c;可以用于编写和执行代码、创建可视化效果、展示数据分析结果等。我们在这里用它实现代码运行和观察运行结果。安装jupyter notebook实质上是安装Anaconda,后续还要在Anaconda Prompt中使用c…

QTday2

第一个界面头部的代码 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPushButton> #include<QLabel> #include<QIcon> #include<QLineEdit> #include<QDebug> class Widget : public QWidget {Q_OBJECTsignals:void j…

Unity:sentinel key not found (h0007)

SSD换电脑&#xff0c;unity 编辑器无法打开&#xff1b; 具体步骤&#xff1a; 删除这个路径下的文件 C:\ProgramData\SafeNet 下 Sentinel LDK 打开Windows 的Cmd 命令行&#xff0c;输入编辑器版本下Unity.exe的路径&#xff0c; CD E:\Dev_Env\Unity\Hub\Editor\2020.3.3…

Java Excel 打开文件报发现“xx.xlsx”中的部分内容有问题。是否让我们尽量尝试恢复问题解决

问题描述&#xff1a; 发现“文件.xlsx”中的部分内容有问题。是否让我们尽量尝试恢复&#xff1f; 问题分析&#xff1a; 1、后端的导出接口写的不对&#xff0c;又返回流数据&#xff0c;又返回响应体数据&#xff0c;导致前端将流数据和响应体数据都下载到了excel文件中。…

leetcode 106. 从中序与后序遍历序列构造二叉树

2023.7.8 让我很难受的一道题&#xff0c;个人感觉难度不止中等。 首先要知道的是知道了前序/后序 中序 之后&#xff0c;是可以构造出相应且唯一的二叉树的。 本道题的思路通过递归的方式根据中序遍历数组和后序遍历数组构建二叉树&#xff0c;并返回根节点。递归的结束条…