桥接模式概念
桥接模式,是将抽象部分与它的具体实现部分解耦,使得它们都可以独立的变化。
- 桥接模式的意义
桥接模式的主要目的是通过组合的方式而不是继承的方式建立起两个类之间的联系;桥接模式类似于多重集继承方案,但是多重继承方式往往违背了类的单一职责原则(通过继承获取了一个类本不该它具有的功能,且父类修改会直接影响到子类),复用性较差,桥接模式是比多重继承更好的替代方案,它的核心在于将抽象和实现进行解耦。
此处的抽象不是值抽象类或者接口这种概念,实现也不是继承或接口实现,抽象与实现指的是两种不同维度的变化,其中抽象包实现,因此一个抽象类的变化可能涉及到多种维度的变化导致的。
桥接模式组成部分
从UML类图可以看到,桥接模式包含四种角色:
- 抽象(Abstraction)
一般为抽象类,该类持有一个对实现角色的引用,并通过构造方式进行赋值,抽象角色中的方法需要实现角色来实现
- 修正抽象( Abstraction)
Abstraction的实现类,对Abstraction的方法进行扩展和完善
- 实现(IImplementor)
一般为接口或抽象类,定义实现维度的基本操作,提供给Abstraction使用
- 具体实现(ConcreteImplementor)
IImplementor的具体实现
- 通用写法:
package com.gupaoedu.vip.pattern.birdge.general;
// 抽象实现
public interface IImplementor {
void operationImpl();
}
Abstraction 持有IImplementor 的引用,并通过构造进行注入
package com.gupaoedu.vip.pattern.birdge.general;
public class Abstraction {
private IImplementor implementor;
public Abstraction(IImplementor implementor){
this.implementor = implementor;
}
public void operation() {
this.implementor.operationImpl();
}
}
RefinedAbstraction 继承Abstraction,扩展父类中定义的方法
package com.gupaoedu.vip.pattern.birdge.general;
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(IImplementor implementor) {
super(implementor);
}
@Override
public void operation() {
super.operation();
System.out.println("refined operation");
}
}
IImplementor 的具体实现:
package com.gupaoedu.vip.pattern.birdge.general;
public class ConcreteImplementorA implements IImplementor {
@Override
public void operationImpl() {
System.out.println("I'm ConcreteImplementor A");
}
}
package com.gupaoedu.vip.pattern.birdge.general;
public class ConcreteImplementorB implements IImplementor {
@Override
public void operationImpl() {
System.out.println("I'm ConcreteImplementor B");
}
}
调用:
package com.gupaoedu.vip.pattern.birdge.general;
public class Test {
public static void main(String[] args) {
// 实现化角色
IImplementor implementor = new ConcreteImplementorA();
// 抽象化角色
Abstraction abs = new RefinedAbstraction(implementor);
// 执行操作
abs.operation();
}
}
桥接模式应用场景
当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使得代码架构趋于稳定,桥接模式适用于下面的场景:
- 在抽象和具体实现之间需要增加更多灵活性的场景
- 一个类存在两个/多个独立变化的维度,而这两个/多个维度都需要进行独立扩展
- 不希望使用继承,或因为多重继承导致系统类的个数剧增
桥接模式的常用场景就是为了替换继承。继承有很多优点,比如抽象,封装,多态等,父类封装了共性,子类实现特性,通过继承可以实现代码复用,但是这同时也是继承最大的缺点,因为父类拥有的方法,子类也会继承到,无论子类是否需要,因此,设计模式中,有一个原则就是邮箱使用组合/聚合方式而不是使用继承
桥接模式示例
日常办公时经常通过发送邮件,短信消息,或者系统内消息与同事进行沟通,尤其是一些审批流程时,还需要记录这些过程,如果按照消息的类别,可以分为邮件消息,短信消息,系统内消息,如果按照紧急程度来划分,消息又可以分为普通消息,紧急消息,特级消息等,此时,消息系统就具备了两个维度的变化,一个消息,可以是普通的邮件消息,或者是加急的邮件消息,也可以是普通的系统消息或者是特急的系统消息。如果使用继承来实现这样一个消息系统,将会变得复杂并且很难扩展,修改代码牵一发动全身,使用桥接模式则很好的解决这个问题:
- 创建一个IMessage接口
package com.gupaoedu.vip.pattern.birdge.message;
public interface IMessage {
// 发送消息:消息内容和接收者
void send(String msg, String reciever);
}
- 接口的具体实现(消息在消息类别上的不同实现)
邮件消息:
package com.gupaoedu.vip.pattern.birdge.message;
public class EmailMessage implements IMessage {
@Override
public void send(String msg, String reciever) {
System.out.println(String.format("使用邮件发送消息 %s 给 %s ", msg, reciever));
}
}
系统消息:
package com.gupaoedu.vip.pattern.birdge.message;
public class SystemMessage implements IMessage {
@Override
public void send(String msg, String reciever) {
System.out.println(String.format("使用内部消息系统发送消息 %s 给 %s ", msg, reciever));
}
}
- 桥接抽象角色:AbstractMessage类
package com.gupaoedu.vip.pattern.birdge.message;
public abstract class AbstractMessage {
private IMessage message;
public AbstractMessage(IMessage message) {
this.message = message;
}
void send(String msg, String reciever){
this.message.send(msg, reciever);
}
}
- AbstractMessage的实现类(消息在重要程度这个维度上的不同实现)
普通消息:
package com.gupaoedu.vip.pattern.birdge.message;
public class NormalMessage extends AbstractMessage {
public NormalMessage(IMessage message) {
super(message);
}
@Override
void send(String msg, String reciever) {
// 普通消息,直接调用父类方法发送消息
super.send(msg, reciever);
}
}
加急消息:
package com.gupaoedu.vip.pattern.birdge.message;
public class UrgencyMessage extends AbstractMessage {
public UrgencyMessage(IMessage message) {
super(message);
}
@Override
void send(String msg, String reciever) {
// 加急消息特殊处理
String urgencyMessage = "【加急】" + msg;
super.send(urgencyMessage, reciever);
}
}
- 调用:
package com.gupaoedu.vip.pattern.birdge.message;
public class Test {
public static void main(String[] args) {
IMessage message = new EmailMessage();
AbstractMessage abstractMessage = new NormalMessage(message);
abstractMessage.send("便携式测温仪采购申请","采购经理");
message = new SystemMessage();
abstractMessage = new UrgencyMessage(message);
abstractMessage.send("便携式测温仪采购申请","公司老板");
}
}
结果如下:
UML类图对比:未展示类之间的关系时,消息类型和消息紧急程度之间是两个维度,相互之间没有联系
通过AbstractMessage这个桥梁,把消息的类别和消息的紧急程度之间联系了起来,让消息可以同时具备这两个维度的变化!!这个示例中,桥接模式的精髓在于AbstractMessage持有一个消息类别维度的抽象的引用,消息紧急程度维度的具体实现,通过继承AbstractMessage,具备了发送不同类别消息的能力,并且这两个维度可以分别去独立扩展,互不影响。如果后续有更多的消息类型,比如微信消息,钉钉消息,那么直接去实现IMessage接口即可,如果是紧急程度需要新增,那么只要创建一个新类实现AbstractMessage类即可!完美解决
桥接模式在源码中的应用
package com.gupaoedu.vip.pattern.birdge;
import lombok.Data;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class JDBCBridge {
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "lchadmin");
String sql = "select * from user where id= ?";
PreparedStatement pstmt = connection.prepareStatement(sql); // 预编译sql
pstmt.setInt(1, 1); // 参数下标1,参数值1
pstmt.execute(); // 执行查询
ResultSet rs = pstmt.getResultSet(); // 获取结果集
while (rs.next()){
User user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
System.out.println(user.toString()); //User(id=1, username=lisi, password=12345, age=19)
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Data
class User {
private int id;
private String username;
private String password;
private int age;
}
/*CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) NOT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4*/
如上代码,JDBC中的Driver类就是一个桥接对象,通过 Class.forName就可以加载各个数据库厂商实现的Driver类;
java.sql.Driver 接口定义如下:
public interface Driver {
// Attempts to make a database connection to the given URL.
Connection connect(String url, java.util.Properties info) throws SQLException;
// Retrieves whether the driver thinks that it can open a connection
boolean acceptsURL(String url) throws SQLException;
// Gets information about the possible properties for this driver.
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
Driver在JDBC中只进行了约束,具体的功能由各个厂商实现。以mysql为例:
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
执行Class.forName的时候,就会先加载com.mysql.jdbc.Driver中的静态方法 DriverManager.registerDriver(new Driver()); 将Dirver对象注册到DriverManager中,DriverManager相关代码如下:
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
registerDriver(driver, null);
}
//Registers the given driver with the {@code DriverManager}.
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);
}
注册方法registerDriver中,将传递进来的Driver封装成了一个DriverInfo对象,然后进行注册,接下来,调用DriverManager的getConnetion方法获取连接对象,在getConnection()中,又会调用各厂商实现的Driver的connect()方法 Connection con = aDriver.driver.connect(url, info) 获取连接对象,这样就避免了使用继承,为不同的数据库提供了相同的接口。JDBCAPI中的DriverManager就是一个桥,如下图所示
@CallerSensitive
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {
return (getConnection(url, info, Reflection.getCallerClass()));
}
@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()));
}
@CallerSensitive
public static Connection getConnection(String url)
throws SQLException {
java.util.Properties info = new java.util.Properties();
return (getConnection(url, info, Reflection.getCallerClass()));
}
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());
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");
}
桥接模式优缺点
- 优点:
分离抽象部分与具体实现部分
提高系统可扩展性
符合开闭原则
符合合成复用原则
- 缺点
增加系统理解与设计难度;
需要正确识别系统中多个独立变化的维度