- 一、什么是桥接模式
- 二、为什么要使用桥接模式
- 三、代码示例
- 1.代码示例01
- 四、在源码中的应用
- 1.桥接模式在Mybatis框架中的应用
- 五、总结
一、什么是桥接模式
桥接模式就是把抽象部分与实现部分抽离,使其可以匹配多种组合
说白了就是将俩种不同维度的事物进行任意组合使用,防止产生笛卡尔积
这里使用支付宝与微信的支付方式来举例
点击跳转至参考文章
通过图解可以看出来,支付平台与支付方式的组合为[支付宝-密码,支付宝-指纹,支付宝-人脸,微信-密码,微信-指纹,微信-人脸]
下面将通过代码示例01来演示不同的实现方案之间的区别
三、代码示例 1.代码示例01通过if来实现(使用switch,抽取方法等本质上还是if逻辑)
好处:开发初期方便,不用过多考虑,咔咔一顿写就完事了
坏处:后期代码不易维护,扩展起来困难
@Slf4j
public class PayDemo01 {
public static void main(String[] args) {
pay("01", "02");
}
public static void pay(String payPlatform, String payType) {
if ("01".equals(payPlatform)) {
if ("01".equals(payType)) {
log.info("支付宝-密码支付!");
} else if ("02".equals(payType)) {
log.info("支付宝-人脸识别支付!");
} else if ("03".equals(payType)) {
log.info("支付宝-指纹支付!");
}
} else if ("02".equals(payPlatform)) {
if ("01".equals(payType)) {
log.info("微信-密码支付!");
} else if ("02".equals(payType)) {
log.info("微信-人脸识别支付!");
} else if ("03".equals(payType)) {
log.info("微信-指纹支付!");
}
} else {
log.error("未知的支付平台!");
}
}
}
通过继承或者接口来实现(上层可通过策略模式来决定使用哪个子类)
好处:对比if来说做了适度的拆分以及抽取,代码维护起来方便,后期扩展只需要继续实现接口就好
坏处:
1.类变多了(设计模式的通病)
2.如果再增加一个维度,就需要针对性的增加很多子类,导致类数量过多,例如新增一个支付平台(银联),那么就需要创建子类[银联-密码,银联-人脸识别,银联-指纹]
不过我感觉这种实现方式反而是我所接触到的大部分项目的实现逻辑,因为比较简单,易理解,也容易实现.
//只用一个微信密码支付做案例,其他方式省略...
public interface IPay {
void pay(String userId, BigDecimal money);
}
@Slf4j
public class WeiXinPasswordPay implements IPay {
@Override
public void pay(String userId, BigDecimal money) {
log.info("微信-密码支付-userId:{},money:{}", userId, money);
}
}
public static void main(String[] args) {
IPay pay = new WeiXinPasswordPay();
pay.pay("123", new BigDecimal("500"));
}
//10:41:54.142 [main] INFO com.example.demo.design.bridging.one.WeiXinPasswordPay - 微信-密码支付-userId:123,money:500
使用桥接模式实现
由于本人并没有支付开发相关经验,所以这里代码中支付逻辑可能存在误差,但是这里只研究桥接模式,所以不去关注业务的实现
好处:
1.通过代码案例可以看出,使用了桥接模式的情况下,就把俩个维度给拆开了,其中支付方式相关的接口可单独使用,不必跟支付平台捆绑在一起.
2.在新增维度的情况下,只需要增加对应维度的实现就好了,最终由调用方去自由组合.
坏处:
1.对比使用单纯的继承方式实现,复杂度增加了,例如其中的支付方式的实现需要去判断是何种支付平台,可能需要使用到其他的设计模式(策略或适配器…)
这里再说一句,设计模式往往不是单独使用的,实际开发中经常是N种设计模式混合使用来达到实现功能的目的.
//支付方式接口
public interface IPayType {
//支付校验
Boolean payCheck(String userId, BigDecimal money);
}
//支付平台抽象类-必须持有支付方式的对象引用
public abstract class PayPlatform {
protected final IPayType payType;
public PayPlatform(IPayType payType) {
this.payType = payType;
}
protected abstract void pay(String userId, BigDecimal money);
}
//支付方式实现-密码支付
@Slf4j
public class PasswordPayType implements IPayType {
@Override
public Boolean payCheck(String userId, BigDecimal money) {
log.info("密码支付校验!");
return true;
}
}
//支付方式实现-人脸识别支付
@Slf4j
public class FacePayType implements IPayType {
@Override
public Boolean payCheck(String userId, BigDecimal money) {
log.info("人脸支付校验!");
return true;
}
}
//支付平台子类-微信支付
@Slf4j
public class WeiXinPayPlatform extends PayPlatform {
public WeiXinPayPlatform(IPayType payType) {
super(payType);
}
@Override
protected void pay(String userId, BigDecimal money) {
Boolean result = payType.payCheck(userId, money);
log.info("校验结果:{}", result);
log.info("执行微信支付逻辑~~~");
}
}
//调用方
public static void main(String[] args) {
PayPlatform weiXinPwsPay = new WeiXinPayPlatform(new PasswordPayType());
weiXinPwsPay.pay("123", new BigDecimal("500"));
PayPlatform weiXinFacePay = new WeiXinPayPlatform(new FacePayType());
weiXinFacePay.pay("123", new BigDecimal("500"));
}
四、在源码中的应用
1.桥接模式在Mybatis框架中的应用
桥接模式在Mybatis框架中的经典实现为JDBC的驱动模块(不是完全规范的桥接模式,网上说是桥接模式的变种).
首先Mybatis框架是针对JDBC做了相当的封装,然后规范了一系列的接口,由不同的厂商去实现(Mysql,Oracle…),这样的好处就是对于调用者来说只需要掌握Mybatis的相关知识就可以轻松的调用Mysql或者是Oracle来实现功能,极大的降低了学习成本.
然后JDBC有俩个核心模块,驱动模块(Driver)跟连接模块(Connection),也可看做俩个维度Driver-Connection(类比上图的支付平台-支付方式),
这样Driver跟Connection就有多种组合(相同的Driver可以有多个不同Connection实现).
所以Mybatis框架中使用桥接模式,将Driver与Connection连接起来进行组合使用.
以下说明皆使用Mysql来举例(Oracle没怎么用过…)
首先看一下Driver接口的定义
package java.sql;
import java.util.logging.Logger;
public interface Driver {
//通过这个方法可知,可以通过Driver对象来获取Connection对象(重点方法)
Connection connect(String url, java.util.Properties info)
throws SQLException;
//以下方法不做关注
//---------------------------------------------------------------------------
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
Driver的UML图(这里的驱动类为Mysql)
Driver类
请注意俩个Driver类的包名,后面会解释为什么会这样.
//className:com.mysql.cj.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!");
}
}
public Driver() throws SQLException {
}
}
这个Driver类继承的是上图的Driver类
//className:com.mysql.jdbc.Driver
package com.mysql.jdbc;
import java.sql.SQLException;
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
super();
}
static {
System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
+ "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
}
}
由第一个Driver类可知,在类实例化的时候会执行静态代码块中的逻辑,调用DriverManager.registerDriver()方法去注册驱动,并且将自身实例化传递过去.
接下来重点关注DriverManager类(可以理解为DriverManager类就是那个桥)
这里为了重点关注桥接模式,所以与之不相关的代码都删除了,避免喧宾夺主
首先DriverManager主要做俩件事情(保存对象与返回对象)
1.通过一个List集合保存注册进来的Driver对象
2.提供一个方法,返回注册进来的Driver对象
3.提供一个方法,返回注册进来的Driver对象的连接对象
具体逻辑请看代码注释:
package java.sql;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.CopyOnWriteArrayList;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
public class DriverManager {
private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();
private DriverManager(){}
//-----------------------------------------------Driver相关代码------------------------------------------------
//注册逻辑相对简单,就是把Driver对象将添加至集合中保存起来
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
if(driver != null) {
//将Driver包装成DriverInfo对象(DriverInfo在该类的最下方,为内部类),保存至集合
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
//这个方法就是将保存Driver对象的集合转为枚举返回,在别的类中会调用此方法
@CallerSensitive
public static java.util.Enumeration getDrivers() {
java.util.Vector result = new java.util.Vector<>();
Class> callerClass = Reflection.getCallerClass();
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerClass)) {
result.addElement(aDriver.driver);
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
return (result.elements());
}
//-----------------------------------------------Connection相关代码------------------------------------------------
//这个方法为获取Connection对象的核心方法
private static Connection getConnection(
String url, java.util.Properties info, Class> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection("" + url + "")");
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
}
class DriverInfo {
final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
}
@Override
public boolean equals(Object other) {
return (other instanceof DriverInfo)
&& this.driver == ((DriverInfo) other).driver;
}
@Override
public int hashCode() {
return driver.hashCode();
}
@Override
public String toString() {
return ("driver[className=" + driver + "]");
}
DriverAction action() {
return da;
}
}
NonRegisteringDriver类(只关注connect()方法)
package com.mysql.cj.jdbc;
import static com.mysql.cj.util.StringUtils.isNullOrEmpty;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;
import com.mysql.cj.Constants;
import com.mysql.cj.Messages;
import com.mysql.cj.conf.ConnectionUrl;
import com.mysql.cj.conf.ConnectionUrl.Type;
import com.mysql.cj.conf.HostInfo;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.conf.url.LoadbalanceConnectionUrl;
import com.mysql.cj.conf.url.ReplicationConnectionUrl;
import com.mysql.cj.exceptions.CJException;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.UnableToConnectException;
import com.mysql.cj.exceptions.UnsupportedConnectionStringException;
import com.mysql.cj.jdbc.ha.FailoverConnectionProxy;
import com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy;
import com.mysql.cj.jdbc.ha.ReplicationConnectionProxy;
import com.mysql.cj.util.StringUtils;
public class NonRegisteringDriver implements java.sql.Driver {
@Override
public java.sql.Connection connect(String url, Properties info) throws SQLException {
try {
if (!ConnectionUrl.acceptsUrl(url)) {
return null;
}
//这个方法主要是根据spring.datasource.url配置返回连接类型(详细枚举见下图)
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
case LOADBALANCE_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);
case FAILOVER_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);
case REPLICATION_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);
default:
return null;
}
} catch (UnsupportedConnectionStringException e) {
return null;
} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}
}
如下图,根据不同的配置前缀,返回不同的类型-枚举
只需要我们修改配置文件中的相关配置,即可实现连接对象的切换
由上诉代码可知
1.当我们配置了driver-class-name: com.mysql.jdbc.Driver之后,Mybatis框架将会根据反射实例化此类,也就是将这个Driver类注册到DriverManager中
2.因为Driver 继承了NonRegisteringDriver类,实现了Driver 接口,所以在获取Driver对象之后自然就能获取Connection对象的具体实现了
3.至此,DriverManager就把Driver与Connection俩个维度给串起来了
这里说明一下 俩个Driver类有啥不同
1.因为咱们配置datasource的时候通常都是:driver-class-name: com.mysql.jdbc.Driver
2.由上诉代码可知com.mysql.jdbc.Driver 继承至com.mysql.cj.jdbc.Driver
3.所以即使咱们配置了com.mysql.jdbc.Driver,但是实际上注册的为com.mysql.cj.jdbc.Driver
4.可以理解为兼容了一下老版本的配置吧
桥接模式在实际开发中可能用的并不算很多,但是理解起来的话不算很复杂,最重要的是需要从业务中提炼出可变的维度.
至此桥接模式就整理完毕啦~



