设计模式-桥接模式学习之旅

595 阅读9分钟

“这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

一、什么是桥接模式?

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

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

二、桥接模式的应用场景

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。桥接模式适用于以下几种业务场景:

  1. 在抽象和具体实现之间需要增加更多的灵活性的场景。
  2. 一个类存在两个(或多个)独立变化的维度,而这两个(或多个)维度都需要独立进行扩展。
  3. 不希望使用继承,或因为多层继承导致系统类的个数剧增。

桥接模式的一个常用使用场景就是为了替代继承。我们知道,继承拥有很多优点,比如抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的帮助我们实现代码复用(封装)的功能,但是同时,这也是继承的一大缺点。因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明了继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿。。。因此,在设计模式中,有一个原则为:优先使用组合/聚合的方式,而不是继承。

在生活场景中的桥接模式也随处可见,比如连接两个空间维度的桥,比如链接虚拟网络与真实网络的连接。

三、桥接模式在业务场景中的应用

举个例子,我们平时办公的时候经常通过发邮件消息、短信消息或者系统内消息和同事进行沟通。尤其是在走一些审批流程的时候,我们需要记录这些过程以备查。我们根据消息的类别来划分的话,可以分为邮件消息、短信消息和系统内消息。但是,根据消息的紧急程度来划分的话,可以分为普通消息、紧急消息和特急消息。显然,整个消息系统可以划分为两个维度,如下图:

image.png

如果,我们用继承的话情况就复杂了,而且不利于扩展。邮件信息可以是普通的,也可以是紧急的,短信消息可以是普通的,也可以是紧急的。下面我们用桥接模式来解决这个问题:

首先创建一个IMessage接口担任桥接的角色:

/**
 * 实现消息发送的统一接口
 */
public interface IMessage {

    //要发送的消息的内容和接收人
    void send(String message, String toUser);
}

创建邮件消息实现EmailMessage类:

/**
 * 邮件短消息的实现类
 */
public class EmailMessage implements IMessage {
    @Override
    public void send(String message, String toUser) {
        System.out.println(String.format("使用邮件短消息的方法,发送消息 %s 给 %s", message, toUser));
    }
}

创建手机短信实现SmsMessage类:

/**
 * 系统内短消息的实现类
 * SMS(Short IMessage Service)短信息服务
 */
public class SmsMessage implements IMessage {
    @Override
    public void send(String message, String toUser) {
        System.out.println(String.format("使用系统内部短消息的方法,发送消息 %s 给 %s", message, toUser));
    }
}

然后,再创建桥接抽象角色AbstractMessage类:

/**
 * 抽象类消息
 */
public abstract class AbstractMessage {

    //持有一个实现部分的抽象
    private IMessage iMessage;

    //构造方法,传入实现部分的对象
    public AbstractMessage(IMessage iMessage) {
        this.iMessage = iMessage;
    }

    //发送消息,委派给实现部分的方法
    public void sendMessage(String message, String toUser) {
        iMessage.send(message, toUser);
    }
}

创建具体实现普通消息NormalMessage类:

/**
 * 普通消息类
 */
public class NormalMessage extends AbstractMessage {

    //构造方法,传入实现部分的对象
    public NormalMessage(IMessage iMessage) {
        super(iMessage);
    }

    @Override
    public void sendMessage(String message, String toUser) {
        //对于普通消息,直接调用父类方法,发送消息即可
        super.sendMessage(message, toUser);
    }
}

创建具体实现紧急消息UrgencyMessage类:

/**
 * 加急消息
 */
public class UrgencyMessage extends AbstractMessage {

    //构造方法
    public UrgencyMessage(IMessage iMessage) {
        super(iMessage);
    }

    @Override
    public void sendMessage(String message, String toUser) {
        message = "加急:" + message;
        super.sendMessage(message, toUser);
    }
}

编写客户端测试代码:

public class Test {

    public static void main(String[] args) {
        IMessage message = new SmsMessage();
        AbstractMessage abstractMessage = new NormalMessage(message);
        abstractMessage.sendMessage("加班申请特批", "马总");

        message = new EmailMessage();
        abstractMessage = new UrgencyMessage(message);
        abstractMessage.sendMessage("加班申请特批", "马总");
    }
}

运行结果如下:

image.png

上面的案例中,我们采用桥接模式解耦了 “消息类型” 和 “消息紧急程度” 这两个独立变化的维度,后续如果有更多的消息类型,比如微信、钉钉等,那么直接新建一个类继承IMessage即可,如果是紧急程度需求增加,那么同样只需新建一个类实现AbstractMessage类即可。

四、桥接模式在源码中的应用

大家都熟悉的JDBC API,其中有一个Driver类就是桥接对象。我们都知道,我们在使用的时候通过Class.forName()方法可以动态加载各个数据库厂商实现的Driver类。具体客户端应用代码如下,以MySQL的实现为例:

public class Test {
    
    public static void main(String[] args) throws Exception {
        //1.加载驱动
        Class.forName("com.mysql.jdbc.Driver");//反射机制加载驱动类
        //2.获取连接Connection
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
        //3.得到执行sql语句的对象Statement
        Statement stmt = conn.createStatement();
        //4.执行sql语句,并返回结果
        ResultSet rs = stmt.executeQuery("select * from xxx");
    }
}

首先,我们来看一下Driver接口的定义:

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;
}

Driver在JDBC中并没有做任何实现,具体的功能实现由各厂商完成,我们以MySQL的实例为例。

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!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     *
     * @throws SQLException
     * if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

当我们执行Class.forName("com.mysql.jdbc.Driver")方法的时候,就会执行com.mysql.jdbc.Driver这个类的静态代码块中的代码。而静态代码中的代码只是调用了一下DriverManager的registerDriver()方法,然后将Driver对象注册到DriverManager中。我们可以继续跟进到DriverManager这个类中,来看相关的代码:

public class DriverManager {


    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    private static volatile int loginTimeout = 0;
    private static volatile java.io.PrintWriter logWriter = null;
    private static volatile java.io.PrintStream logStream = null;
    // Used in println() to synchronize logWriter
    private final static  Object logSync = new Object();

    /* Prevent the DriverManager class from being instantiated. */
    private DriverManager(){}

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    final static SQLPermission SET_LOG_PERMISSION =
        new SQLPermission("setLog");

    final static SQLPermission DEREGISTER_DRIVER_PERMISSION =
        new SQLPermission("deregisterDriver");

    public static java.io.PrintWriter getLogWriter() {
            return logWriter;
    }

    public static void setLogWriter(java.io.PrintWriter out) {

        SecurityManager sec = System.getSecurityManager();
        if (sec != null) {
            sec.checkPermission(SET_LOG_PERMISSION);
        }
            logStream = null;
            logWriter = out;
    }

    @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()));
    }

    @CallerSensitive
    public static Driver getDriver(String url)
        throws SQLException {

        println("DriverManager.getDriver(\"" + url + "\")");

        Class<?> callerClass = Reflection.getCallerClass();

        // Walk through the loaded registeredDrivers attempting to locate someone
        // who understands the given URL.
        for (DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerClass)) {
                try {
                    if(aDriver.driver.acceptsURL(url)) {
                        // Success!
                        println("getDriver returning " + aDriver.driver.getClass().getName());
                    return (aDriver.driver);
                    }

                } catch(SQLException sqe) {
                    // Drop through and try the next driver.
                }
            } else {
                println(" skipping: " + aDriver.driver.getClass().getName());
            }

        }

        println("getDriver: no suitable driver");
        throw new SQLException("No suitable driver", "08001");
    }

    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }

    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);

    }

    @CallerSensitive
    public static synchronized void deregisterDriver(Driver driver)
        throws SQLException {
        if (driver == null) {
            return;
        }

        SecurityManager sec = System.getSecurityManager();
        if (sec != null) {
            sec.checkPermission(DEREGISTER_DRIVER_PERMISSION);
        }

        println("DriverManager.deregisterDriver: " + driver);

        DriverInfo aDriver = new DriverInfo(driver, null);
        if(registeredDrivers.contains(aDriver)) {
            if (isDriverAllowed(driver, Reflection.getCallerClass())) {
                DriverInfo di = registeredDrivers.get(registeredDrivers.indexOf(aDriver));
                 // If a DriverAction was specified, Call it to notify the
                 // driver that it has been deregistered
                 if(di.action() != null) {
                     di.action().deregister();
                 }
                 registeredDrivers.remove(aDriver);
            } else {
                // If the caller does not have permission to load the driver then
                // throw a SecurityException.
                throw new SecurityException();
            }
        } else {
            println(" couldn't find driver to unload");
        }
    }

    @CallerSensitive
    public static java.util.Enumeration<Driver> getDrivers() {
        java.util.Vector<Driver> result = new java.util.Vector<>();

        Class<?> callerClass = Reflection.getCallerClass();

        // Walk through the loaded registeredDrivers.
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerClass)) {
                result.addElement(aDriver.driver);
            } else {
                println(" skipping: " + aDriver.getClass().getName());
            }
        }
        return (result.elements());
    }

    public static void setLoginTimeout(int seconds) {
        loginTimeout = seconds;
    }

    public static int getLoginTimeout() {
        return (loginTimeout);
    }

    public static void println(String message) {
        synchronized (logSync) {
            if (logWriter != null) {
                logWriter.println(message);

                // automatic flushing is never enabled, so we must do it ourselves
                logWriter.flush();
            }
        }
    }

    private static boolean isDriverAllowed(Driver driver, Class<?> caller) {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        return isDriverAllowed(driver, callerCL);
    }

    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 void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

               
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }


    // 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");
    }


}

class DriverInfo {

    final Driver driver;
    DriverAction da;
    DriverInfo(Driver driver, DriverAction action) {
        this.driver = driver;
        da = action;
    }

    @Override
    public boolean equals(Object other) {
        return (other instanceof DriverInfo)
                && this.driver == ((DriverInfo) other).driver;
    }

    @Override
    public int hashCode() {
        return driver.hashCode();
    }

    @Override
    public String toString() {
        return ("driver[className="  + driver + "]");
    }

    DriverAction action() {
        return da;
    }
}

在注册之前,将传过来的Driver对象,封装成了一个DriverInfo对象。接下来继续执行客户端代码的第二步,调用DriverManager的getConnection()方法获取连接对象。在getConnection()中就又会调用各自厂商实现的Driver的connect()方法获得连接对象。这样的话,就巧妙的避开了使用继承,为不同的数据库提供了相同的接口。JDBC API中DriverManager就是桥,如下图所示:

image.png

五、桥接模式的优缺点

优点:

  1. 分离抽象部分及其具体实现部分。
  2. 提供了系统的扩展性。
  3. 符合开闭原则。
  4. 符合合成复用原则。

缺点:

  1. 增加了系统的理解与设计难度。
  2. 需要正确的识别系统中两个独立变化的维度。

六、友情链接

设计模式-工厂模式学习之旅

设计模式-单例模式学习之旅

设计模式-原型模式学习之旅

设计模式-建造者模式学习之旅

设计模式-代理模式学习之旅

设计模式-门面模式学习之旅

设计模式-装饰器模式学习之旅

设计模式-享元模式学习之旅

设计模式-组合模式学习之旅

设计模式-适配器模式学习之旅

欢迎大家关注微信公众号(MarkZoe)互相学习、互相交流。