您现在的位置是:首页 >技术交流 >设计模式之【桥接模式】,多用组合少用继承网站首页技术交流

设计模式之【桥接模式】,多用组合少用继承

秃了也弱了。 2024-06-14 17:17:37
简介设计模式之【桥接模式】,多用组合少用继承

一、什么是桥接模式

桥接模式(Bridge Pattern)也称为桥梁模式、接口(Interface)模式或柄体(Handle and Body)模式,是将抽象部分与它的具体实现部分分离,使它们都可以独立地变化,属于结构型模式。

在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化。”

关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,我们之前讲过的“组合优于继承”设计原则。

桥接模式主要目的是通过组合的方式建立两个类之间的联系,而不是继承。但又类似于多重继承方案,但是多重继承方案往往违背了类的单一职责原则,其复用性比较差,桥接模式是比多重继承更好的替代方案。桥接模式的核心在于解耦抽象和实现。

注:此处的抽象并不是指抽象类或接口这种高层概念,实现也不是继承或接口实现。抽象与实现其实指的是两种独立变化的维度。其中,抽象包含实现,因此,一个抽象类的变化可能涉及到多种维度的变化导致的。

1、使用场景

当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时

当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。

当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。

(1)JDBC驱动程序
(2)银行转账系统
转账分类:网上转账、柜台转账、ATM转账;
转账用户类型:普通用户、银卡用户、金卡用户
(3)消息管理
消息类型:即时消息、延时消息
消息分类:手机短信、邮件消息、QQ消息

2、代理、桥接、装饰器、适配器 4 种设计模式的区别

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

3、桥接模式的优缺点

优点:

  • 实现了抽象和实现部分的分离,从而极大地提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。
  • 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成。
  • 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
  • 符合开闭原则,符合合成复用原则。

缺点:

  • 增加了系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程。
  • 需要正确地识别系统中两个独立变化的维度(抽象和实现),因此其使用范围有一定的局限性,即需要有这样的应用场景。

4、桥接模式的四种角色

桥接模式包含四种角色:

  • 抽象(Abstraction):该类持有一个对实现角色的引用,抽象角色中的方法需要实现角色来实现。抽象角色一般为抽象类(构造函数规定子类要传入一个实现对象);
  • 修正对象(RefinedAbstraction):Abstraction的具体实现,对Abstraction的方法进行完善和扩展;
  • 实现(Implementor):确定实现维度的基本操作,提供给Abstraction使用。该类一般为接口或抽象类;
  • 具体实现(ConcreteImplementor):Implementor的具体实现。

二、实例

需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。

如果我们不使用桥接模式,我们会定义操作系统接口、视频接口,然后定义各自的子接口,最后两两互相实现,最终是这样的:
在这里插入图片描述
扩展性问题(类爆炸)很明显,如果再增加操作系统、视频格式,最终的实现类会成倍增加。

解决方案就是桥接模式。

桥接模式优化代码

该播放器包含了两个维度,适合使用桥接模式。

定义视频接口及实现:

//视频文件
public interface VideoFile {
	void decode(String fileName);
}
//avi文件
public class AVIFile implements VideoFile {
	public void decode(String fileName) {
		System.out.println("avi视频文件:"+ fileName);
	}
}
//rmvb文件
public class REVBBFile implements VideoFile {
	public void decode(String fileName) {
		System.out.println("rmvb文件:" + fileName);
	}
}
//操作系统抽象
public abstract class OperatingSystemVersion {
	protected VideoFile videoFile;
	public OperatingSystemVersion(VideoFile videoFile) {
		this.videoFile = videoFile;
	}
	public abstract void play(String fileName);
}
//Windows版本
public class Windows extends OperatingSystem {
	public Windows(VideoFile videoFile) {
		super(videoFile);
	}
	public void play(String fileName) {
		videoFile.decode(fileName);
	}
}
//mac版本
public class Mac extends OperatingSystemVersion {
	public Mac(VideoFile videoFile) {
		super(videoFile);
	}
	public void play(String fileName) {
		videoFile.decode(fileName);
	}
}

测试类:

//测试类
public class Client {
	public static void main(String[] args) {
		OperatingSystem os = new Windows(new AVIFile());
		os.play("战狼3");
	}
}

桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。
实现细节对客户透明。

三、源码中使用的桥接模式

1、桥接模式在JDBC中的应用

以mysql为例,我们使用jdbc一般这样用:

// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test");
// 得到执行sql语句的statement
PreparedStatement pst = conn.prepareStatement("select * from user");
// 返回结果
ResultSet resultSet = pst.executeQuery();

java提供了Driver接口,并没有实现,具体的实现由各大厂商完成。

mysql对Driver的实现类如下:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    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()
    }
}

当执行Class.forName(“com.mysql.jdbc.Driver”)时,会执行其静态代码块,将Driver封装成DriverInfo:

// java.sql.DriverManager#registerDriver(java.sql.Driver, java.sql.DriverAction)
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);

}

后续在调用DriverManager.getConnection时,我们跟踪源码:

// java.sql.DriverManager#getConnection(java.lang.String)
@CallerSensitive
public static Connection getConnection(String url)
    throws SQLException {

    java.util.Properties info = new java.util.Properties();
    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());
                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");
}

在getConnection中又会调用各厂商自己实现的Driver的connect()方法获取连接对象,这样就巧妙地避开继承,为不同的数据库提供了相同的接口。JDBC的DriverManager就是桥:
在这里插入图片描述

我们总结一下,大致是这样一个逻辑:
1.Class.forName(“com.mysql.jdbc.Driver”); 将mysql的Driver初始化,放入DriverManager中;
2.DriverManager.getConnection 实际上是调用的mysql的Driver,获取的connect。

巧妙地使用了桥接模式。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。