Java设计模式-桥接模式(Bridge Pattern)
目录
- 什么是桥接模式
- 桥接模式的实现
- JavaSE中桥接模式的使用
- Struts2桥接模式的应用
将抽象与实现分离,使他们独立变化。解决继承带来的强耦合关系。
使用组合关系代替继承关系
一、什么是桥接模式
桥接模式(Bridge 模式)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。它是一种结构型设计模式。
Bridge 模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展。
UML关系图如下
- 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。
- 修正抽象化(RefinedAbstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。
- 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
- 具体实现化(ConcreteImplementor)角色:这个角色给出实现化角色接口的具体实现。
在JAVA原生的继承关系中,抽象和实现是父子关系,具有强耦合性,父类的方法子类是一定会有的,这减少了多余代码,子类不想要,可以重写来实现自己的特性,是标准的面向对象思想。但如果父类的子类变的非常多,子类多层继承,会让顶层抽象类、接口变的极为“烫手”,没有任何人敢动。因为顶层抽象类的修改会影响所有子类。
父类(抽象)和子类对应的是抽象和实现,如何让父类和子类的继承关系使用另一种更加松耦合的方式表达,是桥接模式要完成的任务。说到父类与子类关系,就必须要了解类与类之间的关系分类:
- **依赖(Dependency ) 😗*是类与类之间的连接,表示一个类依赖于另外一个类的定义;依赖关系仅仅描述了类与类之间的一种使用与被使用的关系;
- **关联(Association)😗*类与类之间的连结,关联关系使一个类知道另外一个类的属性和方法;通常含有“知道”,“了解”的含义。关联可以是双向的,也可以是单向的
- **聚合(aggregation)😗*是关联关系的一种,是一种强关联关系(has-a);聚合关系是整体和个体/部分之间的关系;关联关系的两个类处于同一个层次上,而聚合关系的两个类处于不同的层次上,一个是整体,一个是个体/部分;在聚合关系中,代表个体/部分的对象有可能会被多个代表整体的对象所共享
- **组合(Composition)😗*它也是关联关系的一种(is-a),但它是比聚合关系更强的关系.组合关系要求聚合关系中代表整体的对象要负责代表个体/部分的对象的整个生命周期;组合关系不能共享;在组合关系中,如果代表整体的对象被销毁或破坏,那么代表个体/部分的对象也一定会被销毁或破坏,而聚在合关系中,代表个体/部分的对象则有可能被多个代表整体的对象所共享,而不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏
对于继承、实现这两种关系比较简单,他们体现的是一种类与类、或者类与接口间的纵向关系;其他的四者关系则体现的是类与类、或者类与接口间的引用、横向关系。总的来说,这几种关系所表现的强弱程度依次为:组合>聚合>关联>依赖
在前面我们有提到桥接模式是:将抽象与实现分离,使他们独立变化。解决继承带来的强耦合关系。使用组合关系代替继承关系。这里的组合就是类间的组合关系,解决类的扩展性问题(类爆炸)。
二、桥接模式的实现
看下面的UML类图,红色代表需求变更。业务是这样的:AbstractCar抽象类是所有类的父类,子类们继承进行丰富功能(手动挡Manual、自动挡Auto),每个车型都有自动挡和手动挡,很正常的业务。但如果突然某一天想要加上混动模式,既可以自动挡又可以手动挡,怎么办?
是不是要和下图一样,每个车型中都要加一个Mix类型的类,需要增加3个类,而三个类的作用非常相似,这就会导致代码重复,违反单一职责原则。
使用继承来拓展子类功能,就造成抽象和实现耦合度极高、扩展困难的问题,所以类的继承关系如果超过3层就应该考虑是不是设计的有问题。在前面学习设计原则和设计模式时,解耦、高扩展性是我们努力的方向。扩展性代表的是变化,抽象代表的是稳定,所以一般抽象稳定部分,分离变化的部分。
以刚才汽车的案例,汽车是稳定的产品,产品间的不同是发动机变速箱引起的,所以把汽车抽象出来,把发动机解耦出去,因为汽车一定是要有发动机的,所以汽车会有一个抽象发动机的引用,那么可以推导出下面的类图
在构造方法中强制指明我的发动机父类类型(依赖倒置),但发动机到底如何实现我不关心,这交给AbstractEngine的子类,这是不是有点像继承?父类只管声明抽象方法,子类去完成具体实现。没错,这就是使用组合关系替代了继承关系,两者之间可以实现解耦,AbstractCar、AbstractEngine之间松耦合,两者可以独立发生变化(增加方法、增加子类等),两者仅仅通过对象引用(桥)产生联系。
如果将AbstractCar、AbstractEngine间的组合关系删除,那么两个类是非常独立的,没有任何联系。抽象化角色(AbstractCar)和实现化角色(AbstractEngine)可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
在深入理解下,抽象化角色(AbstractCar)和实现化角色(AbstractEngine)其实是两个事物,通过组合关系发生联系,这代表两个维度,例如抽象化角色(AbstractCar)有3个子类:BMW、BenZ、Audi,实现化角色(AbstractEngine)有三个子类;两个维度排列组合就是9个产品类型,但仅用到了6个类(不用桥接模式时是9个类),有效降低了代码的重复,类的爆炸增长,增强了扩展性。
看下实现代码
package org.BridgePattern.version2;
abstract class AbstractEngine{
public abstract String engine();
}
class AutoEngine extends AbstractEngine{
@Override
public String engine() {
return "自动档的";
}
}
class ManualEngine extends AbstractEngine{
@Override
public String engine() {
return "手动档的";
}
}
abstract class AbstractCar{
public AbstractEngine abstractEngine;
public AbstractCar(AbstractEngine abstractEngine){
this.abstractEngine = abstractEngine;
}
public abstract void run();
}
class BMW extends AbstractCar{
public BMW(AbstractEngine abstractEngine) {
super(abstractEngine);
}
@Override
public void run() {
System.out.println(super.abstractEngine.engine() + "BMW");
}
}
class BenZ extends AbstractCar{
public BenZ(AbstractEngine abstractEngine) {
super(abstractEngine);
}
@Override
public void run() {
System.out.println(super.abstractEngine.engine() + "BenZ");
}
}
class Audi extends AbstractCar{
public Audi(AbstractEngine abstractEngine) {
super(abstractEngine);
}
@Override
public void run() {
System.out.println(super.abstractEngine.engine() + "Audi");
}
}
public class Test {
public static void main(String[] args) {
Audi audiAuto = new Audi(new AutoEngine());
audiAuto.run();
BenZ manualBenZ = new BenZ(new ManualEngine());
manualBenZ.run();
}
}
// 运行结果
自动档的Audi
手动档的BenZ
桥接模式两个灵魂点:
- 在抽象层建立组合关系
- 依赖倒置
关键点就是这两点,也只有在抽象层建立关系,才能实现依赖倒置和扩展性。
桥接模式的使用场景:
- 当对象存在多种变化的因素时,考虑对其变化的因素和场景进行抽象,然后进行桥接;如汽车拥有不同的发动机。
- 当多个对象存在多种变化的因素时,考虑将这部分变化的部分进行抽象,然后在再聚合进来;比如不同品牌的车使用不同的发动机、底盘等,相当于将第一条进行横向扩展,增加桥接的数量。结合上面讲的案例,完全可以在AbstractCar中加入底盘的引用。
桥接模式的优缺点:
- 优点:
- 分离抽象接口及其实现部分:桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
- 单一职责:在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
- 桥接模式提高了系统的可扩展性:在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
- 缺点:
- 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。
三、JavaSE中桥接模式的使用
3.1 JDK的java.util.logging.Handler与java.util.logging.Formatter
java.util.logging是JDK自带的日志包,可以将日志输出到文件、内存或者控制台,先看下类图
Handle和Formatter类是两个抽象类,它们可以分别独立的变化;而Handle类中包含对Formatter类的引用。
package org.BridgePattern.version2;
import java.io.IOException;
import java.util.logging.*;
public class TestLogger {
public static void main(String[] args) {
Logger logger = Logger.getLogger("lavasoft");
try {
FileHandler fileHandler = new FileHandler("testLogger.log");
fileHandler.setLevel(Level.INFO);
fileHandler.setFormatter(new Formatter() {//定义一个匿名类
@Override
public String format(LogRecord record) {
return record.getLevel() + ":" + record.getMessage() + "\n";
}
});
logger.addHandler(fileHandler);
logger.info("测试");
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在编码时Handler与Formatter都是可变的,Handler通过setFormatter建立与Formatter的联系。
3.2 JDBC
先看一个demo代码(需要mysql数据库,我使用了5.6版本,不在讲解mysql安装过程,安装包私信我即可)
package org.BridgePattern.version2;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class TestJDBC {
public static void main(String[] args) throws Exception{
// 获取Connection对象
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?serverTimezone=GMT", "root", "");
// 通过Connection获取一个操作sql语句的对象Statement
Statement statement = connection.createStatement();
}
}
需要在Maven中添加依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
JDBC的强大之处在于Connection屏蔽数据库之间的差异,用户仅仅通过改变连接数据库的连接(对应的Driver不同)即可操作不同 的数据库。不同的数据库驱动Driver会返回不同的Connection,这个动作是由DriverManager来完成
// 判断驱动是否为空,如果空便加载一个
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
// 省略部分代码
// 使用所有注册的驱动连接数据库
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
// 使用驱动连接数据库
Connection con = aDriver.driver.connect(url, info);
// 如果连接成功就返回Connection,否则继续用下个驱动连接
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
// 省略部分代码
}
}
数据库驱动Driver与Connection是可以独立扩展的,自己的子类也可以自行增加,画一下UML
通过分析,Connection是接口,没有和Driver建立联系的接口,Driver返回的是Connection对象,那么Connection与Driver如何产生联系?谁负责将Driver返回的是Connection对象给Connection?
是DriverManager的getConnect方法,DriverManager就是Connection与Driver的桥梁
更新下UML
仔细看类图,可以发现几个问题:
- Driver与DriverManager是聚合关系,而Connection与DriverManager仅仅是依赖关系,并不符合桥接模式的定义和特点
- Connection与Driver是两个维度的一对一关系,增加数据库类型这两个类都要增加
回忆下桥接模式的四个要素:
- 抽象化(Abstraction)角色:无,不具备抽象化角色作为修正抽象化角色的父类
- 修正抽象化(RefinedAbstraction)角色:DriverManager,持有实现化角色对象,但应该是抽象化角色与实现化角色产生关联
- 实现化(Implementor)角色:Driver
- 具体实现化(ConcreteImplementor)角色:MySQLDriver
桥接模式的主要应用场景是某个类存在两个独立变化的维度,且这两个维度都需要进行扩展,而现在仅有Driver一个变化维度,DriverManager没有抽象化父类,它本身也没有任何子类,所以JDBC是一种简化的桥接模式
也有开发同学觉得JDBC明明是策略模式,我们回顾下策略模式的定义
**策略模式定义:**基本含义是针对一组算法或者行为特性,将他们抽象到具有共同接口函数的独立抽象类后者接口中,从而使得他们可以相互替换。这样就使得某一个特定的接口行为可以在不影响客户端的情况下发生变化。–取自《Struts2技术内幕》
下图是对策略模式的UML类图
策略模式的核心角色:
- Strategy(抽象策略):对算法、行为的抽象,一般为接口
- context(执行环境):被称为上下文,有点难理解,其实是对策略的二次封装,避免其它模块直接调用具体的策略。context中有一个Strategy类引用,在context中决定要调用哪个ConcreteStrategy
- ConcreteStrategy(具体策略):对具体的策略、算法的实现,由Context调用
如果把DriverManager当做策略模式的Context,两者确实很像,但两者之间大有不同
- 目的不一样:策略模式是对象行为模式,它的目的是封装不同算法,使算法相互可替代,在程序运行过程中可以变化不同的算法,而不影响客户端。但JDBC做不到,数据库类型确定后,驱动不能变化。
- 理念不一样:桥接模式目的是将抽象和实现分离,使多个维度独立变化,而策略模式仅一个维度变化。
四、Struts2桥接模式的应用
暂时没发现,如果读者有发现,还请私信我添加,谢谢