结构型模式 ————顺口溜:适装桥组享代外
目录
1、桥接模式
桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。
- 意图:将抽象部分与实现部分分离,使它们都可以独立的变化。
- 主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
- 何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
- 如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
- 关键代码:抽象类依赖实现类。
使用桥接模式,将各维度抽象出来,各维度独立变化,之后可通过组合关系代替继承关系,减少了各维度间的耦合,并减少子类数。
实际上桥接模式就是使用合成复用原则设计出来的。
1.1 桥接模式UML图
1.2 日常生活中看桥接模式与应用实例
- 猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。
- 墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。
- 在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
1.3 Java代码实现
还是老规矩,直接用实际的代码例子引入
背景:相机品牌(索尼,佳能等)和相机类型(单反,微单,卡片机等)两种维族的组合结果
我们想要做的就是取消他们的继承关系,而使用组合
- 先创建一个抽象的相机品牌类
/**
* 相机品牌类
*/
public interface CameraBrand {
void showInfo();
}
- 接着就是两个具体的实现类,这里举了索尼和佳能两个品牌
/**
* 索尼品牌
*/
public class Sony implements CameraBrand {
@Override
public void showInfo() {
System.out.print("【索尼】");
}
}
/**
* 佳能品牌
*/
public class Canon implements CameraBrand {
@Override
public void showInfo() {
System.out.print("【佳能】");
}
}
- 下面再将相机产品类抽象出来,为了实现组合,引入相机品牌成员,为了能让在子类中也能访问到,我用了 protected
/**
* 抽象相机类
*/
public abstract class Camera {
// 将品牌组合进来
protected CameraBrand cameraBrand;
public Camera(CameraBrand cameraBrand) {
this.cameraBrand = cameraBrand;
}
public void showInfo(){
cameraBrand.showInfo();
}
}
- 下面就是两个相机的产品具体类型
/**
* 单反相机
*/
public class SlrCameras extends Camera {
public SlrCameras(CameraBrand cameraBrand) {
super(cameraBrand);
}
@Override
public void showInfo() {
super.showInfo();
System.out.println("单反相机");
}
}
/**
* 卡片相机(数字相机)
*/
public class DigitalCamera extends Camera {
public DigitalCamera(CameraBrand cameraBrand) {
super(cameraBrand);
}
@Override
public void showInfo() {
super.showInfo();
System.out.println("卡片相机(数字相机)");
}
}
测试一下
public class Test {
public static void main(String[] args) {
// 索尼单反相机
Camera camera = new SlrCameras(new Sony());
camera.showInfo();
// 佳能卡片相机
Camera camera2 = new DigitalCamera(new Canon());
camera2.showInfo();
}
}
运行结果:
【索尼】单反相机
【佳能】卡片相机(数字相机)从上述代码中可以看出,我们现在已经可以对不同类型和不同产品的相机进行任意的组合了,例如索尼单反相机,或者佳能单反相机都是可以的,如果想要增加一个品牌或者产品只需要增加具体的实现类就可以了
2、桥接模式在JDK源码中体现
JDBC中又是怎么实现桥接模式的呢?
我们对Driver接口一定不陌生。如果从桥接模式来看,Driver就是一个接口,下面可以有MySQL的Driver,Oracle的Driver,这些就可以当做实现接口类。那么我们现在来看看MySQL中的Driver类
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!");
}
}
}
特别简短的代码,其实只调用了DriverManager中的registerDriver方法来注册驱动。当驱动注册完成后,我们就会开始调用DriverManager中的getConnection方法了
public class DriverManager {
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()));
}
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");
}
}
}
上面是简化的代码,可以看到需要返回的是Connection对象。在Java中通过Connection提供给各个数据库一样的操作接口,这里的Connection可以看作抽象类。可以说我们用来操作不同数据库的方法都是相同的,不过MySQL有自己的ConnectionImpl类,同样Oracle也有对应的实现类。这里Driver和Connection之间是通过DriverManager类进行桥接的,不是像我们上面说的那样用组合关系来进行桥接。
3、桥接模式优缺点
3.1 优点
- 抽象和实现的分离。
- 优秀的扩展能力。
- 实现细节对客户透明。
- 经常遇到一些可以通过两个或多个维度划分的事物,第一种解决方式就是多层继承,但是复用性比较差,同时类的个数也会很多,桥接模式是改进其的更好办法
- 桥接模式增强了系统的扩展性,在两个维度中扩展任意一个维度都不需要修改原有代码,符合开闭原则
3.2 缺点
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式增加了系统的理解与设计难度:因为聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度
3.3 使用场景
- 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
3.4 注意事项
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
4、桥接模式与适配器模式
4.1 适配器模式与桥接模式的区别和联系
适配器模式和桥接模式都是间接引用对象,因此可以使系统更灵活,在实现上都涉及从自身以外的一个接口向被引用的对象发出请求。
两种模式的区别在于使用场合的不同,适配器模式主要解决两个已经有接口间的匹配问题,这种情况下被适配的接口的实现往往是一个黑匣子。我们不想,也不能修改这个接口及其实现。同时也不可能控制其演化,只要相关的对象能与系统定义的接口协同工作即可。适配器模式经常被用在与第三方产品的功能集成上,采用该模式适应新类型的增加的方式是开发针对这个类型的适配器,如下图所示:
桥接模式则不同,参与桥接的接口是稳定的,用户可以扩展和修改桥接中的类,但是不能改变接口。桥接模式通过接口继承实现或者类继承实现功能扩展。如下图所示:
按照GOF的说法,桥接模式和适配器模式用于设计的不同阶段,桥接模式用于设计的前期,即在设计类时将类规划为逻辑和实现两个大类,是他们可以分别精心演化;而适配器模式用于设计完成之后,当发现设计完成的类无法协同工作时,可以采用适配器模式。然而很多情况下在设计初期就要考虑适配器模式的使用,如涉及到大量第三方应用接口的情况。
4.2 适配器模式与桥接模式的联合
在实际应用中,桥接模式经常和适配器模式同时出现,如下图所示:
这种情况经常出现在需要其他系统提供实现方法时,一个典型的例子是工业控制中的数据采集。不同工控厂家提供的底层数据采集接口通常不同,因此在做上层软件设计无法预知可能遇到何种接口。为此需要定义一个通用的采集接口,然后针对具体的数据采集系统开发相应的适配器。数据存储需要调用数据采集接口获得的数据,而数据可以保存到关系数据库、实时数据库或者文件中,。数据存储接口和数据采集结构成了桥接,如下图所示:
同样的结构也经常出现在报表相关的应用中,报表本身结构和报表输出方式完全可以分开,如下图所示:
如上图所示,报表输出可以单独抽象出来与报表的具体形式分开。但报表输出又依赖于具体的输出方式,如果需要输出为PDF格式,则需要调用与PDF相关的API,而这是设计所无法控制的,因此这里需要适配器模式。
5、总结
桥接模式其实特别好理解,只需要看一眼上面的UML类图,也许你就知道桥接模式的使用方法。JDBC这里使用桥接模式可以让Driver和Connection下面的类根据不同数据库来实现不同的发展。就像我们适用场景中的第二点。当然正如我们标题所说的结合着JDK源码来看设计模式。也许看完这篇博客你有自己的理解JDBC这里为什么要用桥接模式。
参考文章:
https://www.cnblogs.com/Cubemen/p/10678692.html
https://www.cnblogs.com/ideal-20/p/14023222.html
https://my.oschina.net/u/2003960/blog/534507
https://www.runoob.com/design-pattern/bridge-pattern.html
https://www.cnblogs.com/peida/archive/2008/08/01/1257574.html