桥接模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系替代继承关系,而从降低了抽象和实现这两个可变维度的耦合度。
桥接模式不是一个使用频率很高的模式,但是熟悉这个模式对于理解面向对象的设计原则,包括 开-闭原则(OCP) 以及 组个/聚合原则(CARP)都很有帮助。理解好这两个原则,有助于形成正确的设计思想和培养良好的设计风格。
模式定义
桥接(Bridge)模式:将抽象(Abstraction)与实现(Implementiation)解耦,使二者可以独立变化。桥接模式又称为桥梁模式,它是一种对象结构型模式。
桥接模式是通过使用 聚合关系 代替 继承关系,把强耦合 转换为 弱耦合,实现 抽象层 与 实现层 之间关系的动态绑定。
白话解释:实现系统可能有多个维度分类,每一个纬度都可能变化,那么把这种多纬度分类给分离出来让他们独立变化,减少他们之间耦合。如下示例:
银行日志系统管理,对日志进行分类。
- 格式分类:操作日志、交易日志、异常日志
- 距离分类:本地记录日志、异地记录日志
模式分析
桥接模式理解关键词:抽象(Abstraction),实现(Implementor),脱耦。
抽象
抽象化:存在于多个实体中的共同的概念联系,就是抽象化。作为一个过程,抽象化就是忽略一些信息,从而把不同的实体当做同样的实体对待。
通常情况下,一组对象如果具有相同的概念性联系,那么它们就可以通过一个共同的类来描述。
如果一些类具有相同的概念性联系,往往可以通过一个共同的抽象类来描述。
实现
实现化:抽象化给出的具体实现。
一个类的实例就是这个类的实现化,一个具体子类是它的抽象超类的实现化。
解耦
耦合是指两个实体的行为是某种强关联。而将它们的强关联去掉,就是解解耦。
在这里,解耦是指将 抽象 与 实现 之间的耦合分离开,或者说是将它们之间的强关联改换成弱关联。
关联关系
强关联:就是在编译时期已经确定的,无法在运行时期动态改变的关联。
弱关联:就是可以动态地确定并且可以在运行时期动态地改变的关联。
在 Java 语言中,继承关系是强关联,而聚合关系是弱关联。
将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改换成弱关联。因此,桥梁模式中的所谓解耦,就是指在一个软件系统的抽象和实现之间使用 组合/聚合 关系而不是继承关系,从而使两者可以相对象立地变化。这就是桥梁模式的用意。
继承:是一种强耦合,它在一开始便把抽象角色和实现角色的关系绑定(binding),使得两个层次之间相互限制,无法独立演化。
桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。
模式优缺点
优点:
- 抽象和实现分离
- 优秀的扩展能力,抽象和实现两个变化纬度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
缺点:
- 由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
模式结构
桥接(Bridge)模式包含以下主要角色。
抽象化(Abstraction)角色:定义抽象类,包含一个对实现化对象的引用。
扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,通过组合关系调用实现化角色中的业务方法。
实现化(Implementiator)角色:定义实现化角色的接口,供扩展抽象化角色调用。
必须指出的是,这个接口不一定和抽象化角色的接口定义相同,可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
具体实现化(Concrete Implementiator)角色:给出实现化角色接口的具体实现。
结构图与代码
完整版
- 抽象化角色 和 扩展抽象化角色 组成了抽象化等级结构。
- 实现化角色 和 具体实现化角色 组成了实现化等级结构。
对象是对行为的封装,而行为是由方法实现的。在此模式的结构图中,抽象化等级结构中的类封装了 operation() 方法;而实现化等级结构中的类封装的是 operationImpl() 方法。在实际的系统中往往会多于一个的方法。
抽象化等级结构中的方法委派给对应的实现化对象来实现自己的功能,这意味着抽象化角色可以通过向不同的实现化对象委派,来达到动态地转换自己的功能的目的。(将抽象角色的行为委派给实现角色实现)
一般而言,实现化角色中的每一个方法都应当有一个抽象化角色中的某一个方法与之相对应,但是,反过来则不一定。换而言之,抽象化角色的接口要比实现化角色的接口宽。抽象化角色除了提供与实现化角色相关的方法之外,还有可能提供其它的方法;而实现化角色则往往仅为实现抽象化角化的相关行为而存在。
使用桥接模式的关键在于准确地找出这个系统的抽象化角色和具体化角色。
抽象角色:定义一个抽象类,引用实现角色接口,在抽象层建议与实现角色的聚合关系。
public abstract class Abstraction {
protected Implementor implementor;
public Abstraction(Implementor implementor) {
this.implementor = implementor;
}
abstract void operation();
}
抽象角色扩展:定义一个抽象类的扩展类,继承抽象角色,实现抽象类的方法,实例依赖实现角色并传递给父类。
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
@Override
public void operation() {
System.out.println("-----> 访问扩展抽象化");
super.implementor.operationImpl();
}
}
实现化角色:定义一个实现化的接口,定义接口行为(方法)。
public interface Implementor {
void operationImpl();
}
具体实现角色:实现实现化角色的接口。
public class ConcreteImplementorA implements Implementor {
@Override
public void operationImpl() {
System.out.println("-----> 具体实现操作 ConcreteImplementorA");
}
}
public class ConcreteImplementorB implements Implementor {
@Override
public void operationImpl() {
System.out.println("-----> 具体实现操作 ConcreteImplementorB");
}
}
客户端调用:面向接口和抽象编程。
- 创建实现化角色类型的具体实例
- 创建抽象角色的扩展实例,将实现化的具体实例传递给 抽象角色的 扩展实例
- 使用抽象角色对象调用方法
public class Client {
public static void main(String[] args) {
Implementor implementor = new ConcreteImplementorA();
Abstraction abstraction = new RefinedAbstraction(implementor);
abstraction.operation();
}
}
输出:
-----> 访问扩展抽象化
-----> 具体实现操作 ConcreteImplementorA
简化版
桥接模式简化版忽略了 抽象实现角色,只有一个具体实现角色;抽象角色里直接引用具体实现角色。
结构图:
抽象化角色:引用依赖具体化角色,定义操作方法。
/**
* @desc 抽象化角色
*/
public abstract class Abstraction {
protected ConcreteImplementor concreteImplementor;
public Abstraction(ConcreteImplementor concreteImplementor) {
this.concreteImplementor = concreteImplementor;
}
abstract void operation();
}
扩展抽象化角色:依赖具体化角色对象,实现抽象化角色方法。
/**
* @desc 抽象化角色扩展
*/
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(ConcreteImplementor concreteImplementor) {
super(concreteImplementor);
}
@Override
public void operation() {
System.out.println("-----> 访问扩展抽象化");
super.concreteImplementor.operationImpl();
}
}
具体实现放角色:
/**
* @desc 只有一个具体实现化类, 可以省略掉抽象实现化类
*/
public class ConcreteImplementor {
public void operationImpl() {
System.out.println("-----> 具体实现操作 Implementor");
}
}
客户端
public class Client {
public static void main(String[] args) {
ConcreteImplementor concreteImplementor = new ConcreteImplementor();
Abstraction abstraction = new RefinedAbstraction(concreteImplementor);
abstraction.operation();
}
}
结果输出
-----> 访问扩展抽象化
-----> 具体实现操作 Implementor
适用场景
桥接模式的一个常见使用场景就是替换继承。继承可以很好实现代码复用的,但这也是继承的一大缺点,子类都会继承父类拥有的方法,无论子类需不需要,这说明继承具有强侵入性(父类代码侵入子类),同时会导致子类臃肿,且父类一变时子类必须跟着变。
因此,在设计模式中,有一个原则为优先使用组合/聚合,而不是继承。
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行动态扩展,且不能影响客户端,桥接模式就非常合适。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时,桥接模式尤为适用。
- 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时,避免在两个层次之间建立静态的继承联系(强耦合),通过桥接模式可以使它们在抽象层建立一个关联关系。
- 一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。
注意事项:不要一涉及继承就考虑使用该模式,尽可能把变化的因素封装到最细、最小的逻辑单元中,避免风险扩大。
应用示例
现在设计一个消息系统,消息类型:正常消息和告警消息,消息发送方式:微信、SMS、邮件。
分析:这里消息有两个独立的变化纬度,有消息类型 和 消息发送方式,随着业务的发展可能增加其它消息类型,还可能增加消息发送方式,例如发送 钉钉消息等。使用桥接模式的实现。
消息抽象角色:引用消息角色,定义发送消息方法。
/**
* @desc 消息抽象(消息类型 : 正常消息, 告警消息)
*/
public abstract class AbstractMessage {
protected IMessage iMessage;
public AbstractMessage(IMessage iMessage) {
this.iMessage = iMessage;
}
abstract void sendMessage(String content);
}
消息扩展抽象角色:正常消息,告警消息,继承消息抽象,实现发送消息方法,实际是委托给实现角色来实现发送消息这个功能。
/**
* @desc 常规消息
*/
public class NomalMessage extends AbstractMessage {
public NomalMessage(IMessage iMessage) {
super(iMessage);
}
@Override
void sendMessage(String content) {
System.out.println("----->正常消息类型");
super.iMessage.send(content);
}
}
/**
* @desc 告警消息
*/
public class WarnMessage extends AbstractMessage {
public WarnMessage(IMessage iMessage) {
super(iMessage);
}
@Override
void sendMessage(String content) {
System.out.println("----->告警消息类型");
super.iMessage.send(content);
}
}
消息实现角色:发送消息接口
/**
* @desc 消息接口(消息渠道 : 微信消息, 短信消息, 邮件消息)
*/
public interface IMessage {
void send(String content);
}
消息具体实现角色:发送消息具体实现
/**
* @desc 发送SMS消息
*/
public class SmsMessage implements IMessage {
@Override
public void send(String content) {
System.out.println("----->发送SMS消息," + content);
}
}
/**
* @desc 发送微信消息
*/
public class WxMessage implements IMessage {
@Override
public void send(String content) {
System.out.println("----->发送微信消息," + content);
}
}
/**
* @desc 发送邮件消息
*/
public class EmailMessage implements IMessage {
@Override
public void send(String content) {
System.out.println("----->发送邮件信息," + content);
}
}
客户端:有可以发送微信正常消息,有可发送短信告警消息。
public class Client {
public static void main(String[] args) {
// 发送SMS正常消息
IMessage iMessage = new SmsMessage();
AbstractMessage message = new NomalMessage(iMessage);
message.sendMessage("消息已发送");
}
}
模式扩展
与适配器模式
对象形式的适配器模式看上去与桥梁模式很像,但两个的设计理念是存在差异的。
适配器模式:目的是改变已有的接口,使源接口与目标接口可能兼容,以使没有关系的两个类能在一起工作。
在系统设计时,当发现现系与已有类无法协同工作时,可以采用适配器模式。特别是那些涉及到大量第三方接口的情况。
桥接模式:是分离抽象化与实现化,使得两者的接口可以不同,两者可以独立变化。
在系统设计时,对于存在两个独立变化纬度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化。
在软件设计时,有时 桥接模式 可与 适配器模式 联合使用。当 桥接模式的实现化角色的接口与现有类的接口不一致时,可以在二者中间定义一个适配器将二者连接起来。
与适配器模式一起使用结构图:
与适配器模式一起使用代码示例:
抽象角色:抽象类,依赖 实现角色
/**
* @desc 抽象角色
*/
public abstract class Abstraction {
protected Implementor implementor;
public Abstraction(Implementor implementor) {
this.implementor = implementor;
}
abstract void operation();
}
扩展抽象角色:继承抽象角色
/**
* @desc 扩展抽象角色
*/
public class RefinedAbstract extends Abstraction {
public RefinedAbstract(Implementor implementor) {
super(implementor);
}
@Override
void operation() {
super.implementor.operationImpl();
}
}
实现角色:
/**
* @desc 实现化角色
*/
public interface Implementor {
void operationImpl();
}
具体实现角色:
/**
* @desc 具体实现角色
*/
public class ConcreteImplementor implements Implementor {
@Override
public void operationImpl() {
System.out.println("-----> 具体实现角色");
}
}
适配者:现有类的接口与 具体实现角色的接口不一致
/**
* @desc 适配者
*/
public class Adaptee {
public void specificRequest() {
System.out.println("-----> 适配者业务");
}
}
适配器角色:创建适配器
/**
* @desc 适配器
*/
public class ObjectAdapter extends ConcreteImplementor {
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void operationImpl() {
adaptee.specificRequest();
}
}
客户端:
public class Client {
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();
Implementor implementor = new ObjectAdapter(adaptee);
Abstraction abstraction = new RefinedAbstract(implementor);
abstraction.operation();
}
}
// 输出结果
-----> 适配者业务
与策略模式
- 策略模式:将每个算法封装起来,使它们可以相互替换,选择具体的算法(策略)由客户端决定,是将责任与算法分离,算法的变化不影响使用算法的客户。
- 桥接模式:目的是把抽象化角色和实现化角色的强耦合解除,为同一个抽象化角色担不同的实现。
与装饰模式
两者的用意有相似之处,都要避免太多的子类。都是继承的良好替换方案。
装饰模式:把每个子类中比基类多出来的行为放到单独的类里面。这样当这些描述额外行为的对象被封装到基类对象里面时,就得到了所需要子类对象。将这些描述额外行为的类,排列组合可以造出很多的功能组合来。
桥接模式:把原来两个基类的实现化细节抽出来,再建造到一个实现化的等级结构中;然后再把原有的基类改造成一个抽象化等级结构。
桥接模式中抽象化角色里面的子类不能象装饰模式那嵌套。桥接模式即可以连续使用,即实现化角色可以成为下一步桥接模式的抽象化角色。
与状态模式
- 桥接模式:描述抽象与实现两个等级结构之间的关系。
- 状态模式:描述一个对象与其(外部化的)状态对象之间的关系。
状态模式可以看做是桥接模式的一个退化后的特殊情况,而在使用状态模式的系统中,类型与子类型的变化会使得系统向桥接模式演化。
与抽象工厂模式
在抽象工厂模式中,产品可以按照等级结构和产品族来分类。等级结构是静态的关系。等级结构是静态的关系,处于同一个等级结构中的产品之间的关系是静态的关系;而产品族是动态的关系,系统使用哪一个产品族的产品是可以在运行时期决定的。这是两个模式的相似之处。
抽象工厂中的工厂等级结构与产品等级结构没有哪一个是抽象化,哪一个是实现化的区别;相反,前者是后者的创建者。
因此,抽象工厂模式可以为桥接模式提供产品创建的结构。
关于桥接模式的实现
实现化角色的退化
当具体实现化类只有一个的情况下,抽象实现化角色就变得没有意义了,可以省略掉。
但抽象化角色与实现化角色的划分仍有意义。例如,如果实现化角色发生改变,则只需把抽象化角色重新连接到新的实现化角色即可,而不必重新编译客户端。这在一些分散在不同地理位置的系统里,客户端不宜经常改变的情况下非常有用的。
抽象化角色的行为
在很多情况下,Abstraction 与 RefinedAbstraction 并没有区别。也就是说,扩展抽象化角色 没有 扩展 抽象化角色的行为,这当然是允许出现的情况。
多个实现类的情况
当有多于一个实现类时,如果抽象化角色知道具体实现化角色的所有信息,那么它可以在构造方法里根据传进来的参数来决定创建哪一个具体实现化角色类的实例。
共享具体实现化角色
可以有几个抽象化角色类合用相同的具体实现化角色类。既然抽象与实现已经分离,那么当色允许有几个不同的抽象化共享相同的实现的情况。这些也本模式的优越性所在。
典型应用
在 JDBC API 中,就有用到桥接模式。
客户端
在使用 JDBC 进行数据库操作时,全都是基于抽象的操作。
以 MySQL 为例,具体客户端应用代码如下:
// 1. 注册JDBC驱动
// 反射机制加载驱动类
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接Connection
conn = DriverManager.getConnection(DB_URL,USER,PASS);
// 3. 获取sql语句的对象Statement
Statement stmt = conn.createStatement();
// 4. 执行sql语句,并返回结果
ResultSet rs = stmt.executeQuery(sql);
注意:通过反射加载驱动后,下一步就 通过 DriverManager 获取连接了。
实现角色
实现角色是个抽象接口,JDBC 的 Driver 是个接口,没有具体实现的,具体实现由具体的数据库产商的驱动完成。
package java.sql;
import java.util.logging.Logger;
public interface Driver {
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;
}
具本实现角色
具体实现由具体的数据库产商的驱动完成。如下是 MySQL 数据库驱动实现的 Driver。
MySQL 的 Driver 在类加载时执行静态代码块,将 Driver 注册到 JDBC 的 DriverManager(驱动管理器),注册驱动后,就会开始调用 DriverManager 中的 getConnection 方法 。
当客户端执行到 Class.forName("com.mysql.jdbc.Driver")
方法的时候,就会执行 com.mysql.jdbc.Driver
类的静态块中的代码。静态块中的代码只是调用了一下 DriverManager 的 registerDriver() 方法,然后将 Driver 对象注册到 DriverManager 中。
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 {
// Required for Class.forName().newInstance()
}
}
DriverManager
DriverManager 在这里扮演的是桥接者的角色,驱动加载 和 获取 connection 通过 DriverManager 连接。
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<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
//............其它省略.................
/**
* 根据给定的数据库url创建 Connection
*/
@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()));
}
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
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());
// 这里又是调用数据库厂商实现的 Driver 的 connect()方法获取链接对象。
Connection con = aDriver.driver.connect(url, info);
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());
}
}
// if we got here nobody could connect.
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");
}
/**
* 注册驱动
*/
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) {
// 将 Driver对象 封装成 DriverInfo
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
}
总结思考
网上很多文章用 JDBC 的例子来说明 桥接模式 在源码上的使用。 把 DriverManager 的使用避开了使用继承,为不同的数据库提供了相同的接口的这种方式理解为桥接模式的应用。
但个人认为此理解值得商榷,至少不是完整的桥接模式的应用,或最多只能算是退化的桥接模式,因为与完整是桥接模式定义的几个角色不匹配
特别是 DriverManager 的角色定义,如果硬要以桥接模式来理解 DriverManager ,那 DriverManager 只能算是 扩展抽象角色,在此 JDBC 示例中省略了 抽象角色,但 DriverManager 的 getConnection() 又是个静态方法,不需要创建 抽象角色对象,也就不存在 两个或更多维度的 独立变化的情况。那这里的 DriverManager 是否更倾向于 抽象工厂 的角色,待讨论。
相关参考
注意:本文归作者所有,未经作者允许,不得转载