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

news2024/9/23 15:30:24

概述

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


桥接模式的原理解析

桥接模式,也叫做 “桥梁模式”,它是 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 的理解方式,弄懂定义中的 “抽象” 和 “实现” 两个概念,是理解它的关键。

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

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

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

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

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

相关文章

Spring文件配置以及获取

前言 我们都知道很多应用都是有配置文件的,可以对应用的一些参数进行配置,如conf... 本篇我们讲解关于Spring的配置文件以及程序怎么获取其中写入的参数 Spring中的配置文件主要有三种 还有yml和ymal文件 下面我们将介绍关于常用的两种 preoperties 和 yml文件的格式和读取…

2024年适合个人和普通企业用户的阿里云服务器推荐,最低仅需61元1年

现在不论是个人还是企业&#xff0c;只要有建站&#xff0c;做APP&#xff0c;存储数据等需要就需要一台云服务器。通常来说&#xff0c;购买阿里云服务器的用户主要分为三类&#xff1a;一是个人用户&#xff0c;二是普通企业用户&#xff0c;三是对云服务器性能有特殊需求的集…

libVLC 视频缩放

libvlc是一个常用的开源多媒体框架&#xff0c;它可以用来播放和处理各种类型的音频和视频文件。如果想要缩放视频&#xff0c;可以通过libvlc提供的API来实现。 //设置视频的缩放比例。 libvlc_video_set_scale() 以下是如何使用 libVLC 设置视频缩放的基本步骤&#xff1a;…

【研发管理】研发管理规范

研发管理规范 目的定义工作职责产品经理项目经理运维负责人研发负责人研发工程师 基本原则研发过程描述需求分析分析设计研发实现测试验收发布上线线上监控 目的 软件研发相关管理&#xff0c;有效控制技术风险&#xff0c;提高研发和运行质量 定义 包括需求分析、分析设计…

智慧公厕的全域感知、全网协同、全业务融合和全场景智慧赋能

公共厕所是城市的重要组成部分&#xff0c;为市民提供基本的生活服务。然而&#xff0c;传统的公厕管理模式存在诸多问题&#xff0c;如排队等候时间长、卫生状况差、空气质量差等&#xff0c;严重影响市民的出行和生活质量。为了解决这些问题&#xff0c;智慧公厕应运而生&…

WebClient上载文件——实现将本地文件同步到远端服务器上

问题描述 用户上传产品示例图片到服务器端上&#xff0c;客户端在请求图片资源时&#xff0c;当服务端架设了多个节点的情况下&#xff0c;由于没有负载均衡请求到保存图片资源的服务器&#xff0c;出现图片访问404的问题。 这里保存上传文件时&#xff0c;同时需要将该文件保…

【学习心得】神经网络知识中的符号解释

这里我对我学到的神经网络知识中&#xff0c;常见的符号做一下记录和总结&#xff0c;方便自己在后面学习中复习。下图二分类识别图像识别猫为例。为了保存一张图片&#xff0c;需要三个矩阵&#xff0c;它们分别对应图片中的红、绿、蓝三种颜色通道&#xff0c;如果图片大小为…

vitepress builld报错

问题&#xff1a;build时报错&#xff1a;document/window is not defined。 背景&#xff1a;使用vitepress展示自定义的组件&#xff0c;之前build是没有问题了&#xff0c;由于新增了qr-code以及quill富文本组件&#xff0c;导致打包时报错。 原因&#xff1a;vitepress官…

邮件接口与第三方平台的集成的方式有哪些?

邮件接口如何实现高效通信&#xff1f;怎么有效地利用邮件接口&#xff1f; 邮件接口与第三方平台的集成已经成为了企业提升工作效率、优化用户体验的关键环节。那么&#xff0c;邮件接口与第三方平台的集成方式究竟有哪些呢&#xff1f;接下来&#xff0c;AokSend就来探讨一下…

力扣由浅至深 每日一题.15 删除排序链表中的重复元素

没关系的&#xff0c;昨天的暴雨不会淋湿今天的自己 —— 24.3.26 删除排序链表中的重复元素 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出…

VR全景展示:传统制造业如何保持竞争优势?

在结束不久的两会上&#xff0c;数字化经济和创新技术再度成为了热门话题。我国制造产业链完备&#xff0c;但是目前依旧面临着市场需求不足、成本传导压力加大等因素影响&#xff0c;那么传统制造业该如何保持竞争优势呢&#xff1f; 在制造行业中&#xff0c;VR全景展示的应用…

markdown 编辑工具Typora的使用

简介 Typora是一款由Abner Lee开发的轻量级Markdown编辑器&#xff0c;它以其简洁美观的界面、实时预览的功能以及强大的Markdown语法支持而受到用户的喜爱。 Typora的编辑方式与众不同&#xff0c;它采用了所见即所得的编辑方式&#xff0c;这意味着用户在输入Markdown语法标…

OpenCV4.9关于矩阵上的掩码操作

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:如何使用OpenCV扫描图像、查找表和时间测量 下一篇:OpenCV4.9的是如何进行图像操作 引言&#xff1a; 矩阵上的掩码操作非常简单。这个想法是&#xff0c;我们根据掩码矩阵&#xff08…

简易电路设计,PW1605芯片实现24V/30V/48V限流过压保护功能

一般描述 PW1605 是一款电流限制开关&#xff0c;具有可编程输入过压保护和输出电压箝位功能。集成保护 N 沟道 FET 具有极低的 RDS&#xff08;ON&#xff09; 功能&#xff0c;PW1605有助于降低正常工作期间的功率损耗。可编程软启动时间控制启动期间输出电压的压摆率。独立的…

本周四Techtalk技术交流社区邀请吕海波老师为大家带来精彩技术分享

欢迎您关注我的公众号【尚雷的驿站】 **************************************************************************** 公众号&#xff1a;尚雷的驿站 CSDN &#xff1a;https://blog.csdn.net/shlei5580 墨天轮&#xff1a;https://www.modb.pro/u/2436 PGFans&#xff1a;ht…

Docker - 哲学 默认网络和 自定义网络 与 linux 网络类型 和 overlay2

默认网络&#xff1a;不指定 --nerwork 不指定 网络 run 一个容器时&#xff0c;会直接使用默认的网络桥接器 &#xff08;docker0&#xff09; 自定义网络&#xff1a;指定 --nerwork 让这两台容器互相通信 的前提 - 共享同一个网络 关于 ip addr 显示 ens160 储存驱动 ov…

智慧公厕,运用大数据提升公共厕所管理水平

在现代社会&#xff0c;科技的发展给我们带来了诸多便利&#xff0c;而智慧公厕就是其中之一。智慧公厕运用数据和技术&#xff0c;提升公共厕所的管理水平&#xff0c;为社会生活服务。本文将以智慧公厕源头实力厂家广州中期科技有限公司&#xff0c;遍布全国的众多标杆性案例…

macOS Sonoma 14.4.1 (23E224) 正式版发布,ISO、IPSW、PKG 下载

macOS Sonoma 14.4.1 (23E224) 正式版发布&#xff0c;ISO、IPSW、PKG 下载 2024 年 3 月 26 日凌晨&#xff0c;macOS Sonoma 14.4.1 更新修复了一个可能导致连接到外部显示器的 USB 集线器无法被识别的问题。它还解决了可能导致 Java 应用程序意外退出的问题&#xff0c;并修…

【电力监控保护】AM5SE-IS防孤岛保护装置/35kV、10kV、380V分布式光伏并网供电/什么是孤岛效应/孤岛效应的危害

什么是孤岛效应&#xff01;&#xff01;&#xff01; 安科瑞薛瑶瑶18701709087 在电力系统中&#xff0c;孤岛效应指的是当电网突然断电时&#xff0c;并网光伏发电系统仍然保持对电网中部分线路的供电状态。这种情况下&#xff0c;这些线路与其他电网断开&#xff0c;形成了…

设置远程访问 jupyter Notebook Lab

安装Anaconda / Miniconda 进入conda环境&#xff0c;安装jupyter https://jupyter.org/install 生成notebook config C:\Users\***>jupyter notebook --generate-config Writing default config to: C:\Users\***\.jupyter\jupyter_notebook_config.py创建密码 jupyter…