【设计模式】桥接模式
参考资料:
Java 设计模式:实战桥接模式
一起来学设计模式之桥接模式
《设计模式之美》设计模式与范式(结构型-桥接模式)
桥接模式在项目中的应用
文章目录
- 【设计模式】桥接模式
- 一、桥接模式概述
- 二、案例场景模拟
- 2.1、场景概述
- 2.2、用一坨坨代码实现
- 2.3、桥接模式重构代码
- 2.3.1、桥接模式模型结构
- 2.3.2、代码实现
- 2.3.3、测试验证
- 三、总结
一、桥接模式概述
在创建型模式、结构性模式和行为型模式分类中,桥接模式归类为创建型模式。整个桥接模式的核心就是将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接模式的主要作用就是通过将抽象部分与实现部分分离,把多种可匹配的使用进行组合。说白了核心实现也就是在A类中含有B类接口,通过构造函数传递B类的实现,这个B类就是设计的桥
。
桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。 因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿。因此,在设计模式中,有一个原则为优先使用组合/聚合,而不是继承。
二、案例场景模拟
2.1、场景概述
随着市场的竞争在支付服务行业出现了微信和支付宝还包括一些其他支付服务,但是对于商家来说并不希望改变用户习惯。就像如果我的地摊只能使用微信或者只能使用支付宝付款,那么就会让我顾客伤心,鸡蛋灌饼也卖不动了。
在这个时候就出现了第三方平台,把市面上综合占据市场90%以上的支付服务都集中到自己平台中,再把这样的平台提供给店铺、超市、地摊使用,同时支持人脸、扫描、密码多种方式。
我们这个案例就模拟一个这样的第三方平台来承接各个支付能力,同时使用自家的人脸让用户支付起来更加容易。那么这里就出现了多支付与多模式的融合使用,如果给每一个支付都实现一次不同的模式,即使是继承类也需要开发好多。而且随着后面接入了更多的支付服务或者支付方式,就会呈爆炸似的扩展。
2.2、用一坨坨代码实现
既然你逼我那就别怪我无情,还没有我一个类写不完的需求!反正写完就完事了,拿完绩效也要走了天天逼着写需求,代码越来越乱心疼后面的兄弟3秒。
public class PayController {
private Logger logger = LoggerFactory.getLogger(PayController.class);
public boolean doPay(String uId, String tradeId, BigDecimal amount, int channelType, int modeType) {
// 微信支付
if (1 == channelType) {
logger.info("模拟微信渠道支付划账开始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
if (1 == modeType) {
logger.info("密码支付,风控校验环境安全");
} else if (2 == modeType) {
logger.info("人脸支付,风控校验脸部识别");
} else if (3 == modeType) {
logger.info("指纹支付,风控校验指纹信息");
}
}
// 支付宝支付
else if (2 == channelType) {
logger.info("模拟支付宝渠道支付划账开始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
if (1 == modeType) {
logger.info("密码支付,风控校验环境安全");
} else if (2 == modeType) {
logger.info("人脸支付,风控校验脸部识别");
} else if (3 == modeType) {
logger.info("指纹支付,风控校验指纹信息");
}
}
return true;
}
}
- 上面的类提供了一个支付服务功能,通过提供的必要字段;
用户ID
、交易ID
、金额
、渠道
、模式
,来控制支付方式。 - 以上的
ifelse
应该是最差的一种写法,即使写ifelse
也是可以优化的方式去写的。
测试验证
@Test
public void test_pay() {
PayController pay = new PayController();
System.out.println("\r\n模拟测试场景;微信支付、人脸方式。");
pay.doPay("weixin_1092033111", "100000109893", new BigDecimal(100), 1, 2);
System.out.println("\r\n模拟测试场景;支付宝支付、指纹方式。");
pay.doPay("jlu19dlxo111","100000109894",new BigDecimal(100), 2, 3);
}
- 以上分别测试了两种不同的支付类型和支付模式;微信人脸支付、支付宝指纹支付
测试结果
模拟测试场景;微信支付、人脸方式。
23:05:59.152 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟微信渠道支付划账开始。uId:weixin_1092033111 tradeId:100000109893 amount:100
23:05:59.155 [main] INFO o.i.demo.design.pay.mode.PayCypher - 人脸支付,风控校验脸部识别
23:05:59.155 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟微信渠道支付风控校验。uId:weixin_1092033111 tradeId:100000109893 security:true
23:05:59.155 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟微信渠道支付划账成功。uId:weixin_1092033111 tradeId:100000109893 amount:100
模拟测试场景;支付宝支付、指纹方式。
23:05:59.156 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟支付宝渠道支付划账开始。uId:jlu19dlxo111 tradeId:100000109894 amount:100
23:05:59.156 [main] INFO o.i.demo.design.pay.mode.PayCypher - 指纹支付,风控校验指纹信息
23:05:59.156 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟支付宝渠道支付风控校验。uId:jlu19dlxo111 tradeId:100000109894 security:true
23:05:59.156 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟支付宝渠道支付划账成功。uId:jlu19dlxo111 tradeId:100000109894 amount:100
Process finished with exit code 0
- 从测试结果看已经满足了我们的不同支付类型和支付模式的组合,但是这样的代码在后面的维护以及扩展都会变得非常复杂。
2.3、桥接模式重构代码
接下来使用桥接模式来进行代码优化,也算是一次很小的重构。
从上面的ifelse
方式实现来看,这是两种不同类型的相互组合。那么就可以把支付方式和支付模式进行分离通过抽象类依赖实现类的方式进行桥接,通过这样的拆分后支付与模式其实是可以单独使用的,当需要组合时候只需要把模式传递给支付即可。
桥接模式的关键是选择的桥接点拆分,是否可以找到这样类似的相互组合,如果没有就不必要非得使用桥接模式。
2.3.1、桥接模式模型结构
- 左侧
Pay
是一个抽象类,往下是它的两个支付类型实现;微信支付、支付宝支付。 - 右侧
IPayMode
是一个接口,往下是它的两个支付模型;刷脸支付、指纹支付。 - 那么,
支付类型
×支付模型
= 就可以得到相应的组合。 - 注意,每种支付方式的不同,刷脸和指纹校验逻辑也有差异,可以使用适配器模式进行处理,这里不是本文重点不做介绍,可以看适配器模式章节。
2.3.2、代码实现
支付类型桥接抽象类
public abstract class Pay {
protected Logger logger = LoggerFactory.getLogger(Pay.class);
protected IPayMode payMode;
public Pay(IPayMode payMode) {
this.payMode = payMode;
}
public abstract String transfer(String uId, String tradeId, BigDecimal amount);
}
- 在这个类中定义了支付方式的需要实现的划账接口:
transfer
,以及桥接接口;IPayMode
,并在构造函数中用户方自行选择支付方式。 - 如果没有接触过此类实现,可以重点关注
IPayMode payMode
,这部分是桥接的核心。
微信支付
public class WxPay extends Pay {
public WxPay(IPayMode payMode) {
super(payMode);
}
public String transfer(String uId, String tradeId, BigDecimal amount) {
logger.info("模拟微信渠道支付划账开始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
boolean security = payMode.security(uId);
logger.info("模拟微信渠道支付风控校验。uId:{} tradeId:{} security:{}", uId, tradeId, security);
if (!security) {
logger.info("模拟微信渠道支付划账拦截。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
return "0001";
}
logger.info("模拟微信渠道支付划账成功。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
return "0000";
}
}
支付宝支付
public class ZfbPay extends Pay {
public ZfbPay(IPayMode payMode) {
super(payMode);
}
public String transfer(String uId, String tradeId, BigDecimal amount) {
logger.info("模拟支付宝渠道支付划账开始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
boolean security = payMode.security(uId);
logger.info("模拟支付宝渠道支付风控校验。uId:{} tradeId:{} security:{}", uId, tradeId, security);
if (!security) {
logger.info("模拟支付宝渠道支付划账拦截。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
return "0001";
}
logger.info("模拟支付宝渠道支付划账成功。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
return "0000";
}
}
- 这里分别模拟了调用第三方的两个支付渠道;微信、支付宝,当然作为支付综合平台可能不只是接了这两个渠道,还会有其很跟多渠道。
- 另外可以看到在支付的时候分别都调用了风控的接口进行验证,也就是不同模式的支付(
刷脸
、指纹
),都需要过指定的风控,才能保证支付安全。
定义支付模式接口
public interface IPayMode {
boolean security(String uId);
}
- 任何一个支付模式;刷脸、指纹、密码,都会过不同程度的安全风控,这里定义一个安全校验接口。
刷脸
public class PayFaceMode implements IPayMode{
protected Logger logger = LoggerFactory.getLogger(PayCypher.class);
public boolean security(String uId) {
logger.info("人脸支付,风控校验脸部识别");
return true;
}
}
指纹
public class PayFingerprintMode implements IPayMode{
protected Logger logger = LoggerFactory.getLogger(PayCypher.class);
public boolean security(String uId) {
logger.info("指纹支付,风控校验指纹信息");
return true;
}
}
密码
public class PayCypher implements IPayMode{
protected Logger logger = LoggerFactory.getLogger(PayCypher.class);
public boolean security(String uId) {
logger.info("密码支付,风控校验环境安全");
return true;
}
}
- 在这里实现了三种支付模式(刷脸、指纹、密码)的风控校验,在用户选择不同支付类型的时候,则会进行相应的风控拦截以此保障支付安全。
2.3.3、测试验证
编写测试类
@Test
public void test_pay() {
System.out.println("\r\n模拟测试场景;微信支付、人脸方式。");
Pay wxPay = new WxPay(new PayFaceMode());
wxPay.transfer("weixin_1092033111", "100000109893", new BigDecimal(100));
System.out.println("\r\n模拟测试场景;支付宝支付、指纹方式。");
Pay zfbPay = new ZfbPay(new PayFingerprintMode());
zfbPay.transfer("jlu19dlxo111","100000109894",new BigDecimal(100));
}
- 与上面的ifelse实现方式相比,这里的调用方式变得整洁、干净、易使用;
new WxPay(new PayFaceMode())
、new ZfbPay(new PayFingerprintMode())
- 外部的使用接口的用户不需要关心具体的实现,只按需选择使用即可。
- 目前以上优化主要针对桥接模式的使用进行重构
if
逻辑部分,关于调用部分可以使用抽象工厂
或策略模式
配合map结构,将服务配置化。因为这里主要展示桥接模式
,所以就不在额外多加代码,避免喧宾夺主。
测试结果
模拟测试场景;微信支付、人脸方式。
23:14:40.911 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟微信渠道支付划账开始。uId:weixin_1092033111 tradeId:100000109893 amount:100
23:14:40.914 [main] INFO o.i.demo.design.pay.mode.PayCypher - 人脸支付,风控校验脸部识别
23:14:40.914 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟微信渠道支付风控校验。uId:weixin_1092033111 tradeId:100000109893 security:true
23:14:40.915 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟微信渠道支付划账成功。uId:weixin_1092033111 tradeId:100000109893 amount:100
模拟测试场景;支付宝支付、指纹方式。
23:14:40.915 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟支付宝渠道支付划账开始。uId:jlu19dlxo111 tradeId:100000109894 amount:100
23:14:40.915 [main] INFO o.i.demo.design.pay.mode.PayCypher - 指纹支付,风控校验指纹信息
23:14:40.915 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟支付宝渠道支付风控校验。uId:jlu19dlxo111 tradeId:100000109894 security:true
23:14:40.915 [main] INFO o.i.demo.design.pay.channel.Pay - 模拟支付宝渠道支付划账成功。uId:jlu19dlxo111 tradeId:100000109894 amount:100
Process finished with exit code 0
- 从测试结果看内容是一样的,但是整体的实现方式有了很大的变化。所以有时候不能只看结果,也要看看过程
三、总结
桥接模式的原理核心:
抽象与抽象的分离,具体的实现类依赖抽象而不是依赖具体,简介完成了具体类与具体类间的解耦,它们之间使用抽象来进行组合或聚合,而不再使用继承。
应用场景:
- 1、类存在两个或多个独立变化的维度,而且都需要独立进行扩展;
- 2、不希望使用继承或因多层继承,导致系统内类个数的急剧增加;
- 3、需要在某种统一协议下增加更多组件(如支付场景,期望支持微信、支付宝等支付组件,统一协议就是收款、支付、扣款);
- 4、基于消息驱动的场景(消息行为比较统一,包括发送、接收、处理和回执,但不同APP的实现通常各不相同);
- 5、需要提供平台独立性的应用程序(如不同数据库的JDBC驱动程序、硬盘驱动程序等);
优点:分离实体与行为,更好的可扩展性,代替多层继承方案,极大减少子类数量; 缺点
- 增加设计难度:一开始就要针对抽象层进行,正确识别两个独立维度需要一定的经验积累;
- 增加维护成本:组合和聚合不像继承那样容易找到对象简单的调用关系;
- 导致性能下降:组合或聚合关系在OOP中使用委托的事项方式,调用对象变多,自然影响程序性能;