桥接设计模式

144 阅读5分钟

Gof 原文

Decouple an abstraction from its implementation so that the two can varyindependently.

在这里插入图片描述

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。 桥接模式适用于以下几种业务场景。 (1)在抽象和具体实现之间需要增加更多灵活性的场景。 (2)一个类存在两个(或多个)独立变化的维度,而这两个(或多个)维度都需要独立进行扩展。 (3)不希望使用继承,或因为多层继承导致系统类的个数剧增。

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

The implementation of bridge design pattern follows the notion to prefer Composition over inheritance. 桥接设计模式的实现遵循组合优于继承的概念。

在这里插入图片描述类内部具备两种或多种变化维度时,比如图形的形状和颜色。

public interface Shape {
    void draw();
}
class Round implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}
public interface A1_Color {
	public void getColor();
}
public class Red implements Color {
    @Override
    public String getColor() {
        return "红";
    }
}

如果想要红色的圆

这时我们很容易想到两种设计方案:

  • 为了复用形状类,将每种形状定义为父类,每种不同颜色的图形继承自其形状父类。
  • 为了复用颜色类,将每种颜色定义为父类,每种不同颜色的图形继承自其颜色父类。
class RedRound extends Round {
    @Override
    public void draw() {
        System.out.println("绘制红色的圆形");
    }
}

或者

public class RedRound extends Red {
    @Override
    public String getColor() {
        return "绘制红色的圆形";
    }
}

一旦有5个颜色5个形状,就要写25个类!!!

这时使用继承很容易造成子类越来越多。

形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合,这就是桥接模式的思想。

下面是理解过程

颜色的接口

public interface A1_Color {
	public void applyColor();
}

颜色的实现类

public class A5_RedColor implements A1_Color{

	public void applyColor(){
		System.out.println("red.");
	}
}
public class A6_GreenColor implements A1_Color{

	public void applyColor(){
		System.out.println("green.");
	}
}

将形状和颜色分离,根据需要对形状和颜色进行组合,代码里体现就是把原来两个类(RedRound extends Red)继承关系,现在变成了组合就是 Has-a 的关系。

public abstract class A2_Shape {
	protected A6_GreenColor color;
	...
	}

此时就不是继承关系了而是组合关系了,一个颜色可以和各种形状组合,是一对多的关系。

中文一些解释里,将抽象与其实现解耦,将抽象部分与它的实现部分分离,Decouple an abstraction from its implementation翻译过来也是把抽象从实现里解耦。

这些名词说的太难理解了,不知道具体啥操作,后来慢慢有点体会,思路如下:

上述代码里protected A6_GreenColor color;就是抽象和实现绑定了,因为A6_GreenColorA1_Color的实现

如果在每个形状类中桥接 Color 接口:

public abstract class A2_Shape {
	//Composition - implementor
	protected A1_Color color;
	//constructor with implementor as input argument
	public A2_Shape(A1_Color c){
		this.color=c;
	}
	abstract public void applyColor();
}

这样就是 将抽象与其实现解耦,A2_Shape 里不关心 A1_Color 具体实现,只是使用A1_Color 接口的方法,A1_Color 因为是接口所以具体实现有很灵活

此时各种颜色可以和各种形状组合,就是 多对多 的关系了。

protected A1_Color color;此时的 color 就是颜色和形状组合的桥梁。

public class A3_Triangle extends A2_Shape{
	public A3_Triangle(A1_Color c) {
		super(c);
	}
	@Override
	public void applyColor() {
		System.out.println("Triangle filled with color!! ");
		color.applyColor();
	} 
}
public class A4_Pentagon extends A2_Shape{
	public A4_Pentagon(A1_Color c) {
		super(c);
	}
	@Override
	public void applyColor() {
		System.out.println("Pentagon filled with color!! ");
		color.applyColor();
	} 
}

测试

		A2_Shape tri = new A3_Triangle(new A5_RedColor());
		tri.applyColor();
		
		A2_Shape pent = new A4_Pentagon(new A6_GreenColor());
		pent.applyColor();
Triangle filled with color!! 
red.
Pentagon filled with color!! 
green.

在这里插入图片描述 上文是读完下文修改的版本,下文才是思维源头(图解设计模式里的文章)

先介绍两个概念

希望增加新功能时

想给一个类增加新功能时,会创建这个类的子类增加新的方法。

  • 父类具有基本功能
  • 在子类中增加新的功能
  • 以上这种层次结构被称为类的功能层次结构

希望增加新的实现时

  • 父类通过声明抽象方法来定义接口(API)
  • 子类通过实现具体方法来实现接口(API)
  • 这种层次结构称为类的实现层次结构

当类的层次结构只有一层时,功能层次结构与实现层次结构是混杂在一个层次结构中的,这样容易使类的层次结构变得复杂,因为要把他们分开,但是分开后会缺少联系,多以要在他们之间搭一座桥,这就是Bridge模式的作用

Bridge模式的作用就是连接类的功能层次结构类的实现层次结构

public class B1_Display {
    private DisplayImpl impl;
    public B1_Display(DisplayImpl impl) {
        this.impl = impl;
    }
    public void open() {
        impl.rawOpen();
    }
    public void print() {
        impl.rawPrint();
    }
    public void close() {
        impl.rawClose();
    }
    public final void display() {
        open();
        print();                    
        close();
    }
}

注意:private DisplayImpl impl; 其中impl就是两个层次结构的桥梁

子类增加新功能,此为类的功能层次结构的体现

public class B2_CountDisplay extends B1_Display {
    public B2_CountDisplay(DisplayImpl impl) {
        super(impl);
    }
    public void multiDisplay(int times) {       // 循环显示times次
        open();
        for (int i = 0; i < times; i++) {
            print();
        }
        close();
    }
}

以下为类的实现层次结构

public abstract class B3_DisplayImpl {
    public abstract void rawOpen();
    public abstract void rawPrint();
    public abstract void rawClose();
}
public class B4_StringDisplayImpl extends B3_DisplayImpl {
    private String string;                              // 要显示的字符串
    private int width;                                  // 以字节单位计算出的字符串的宽度
    public B4_StringDisplayImpl(String string) {           // 构造函数接收要显示的字符串string
        this.string = string;                           // 将它保存在字段中
        this.width = string.getBytes().length;          // 把字符串的宽度也保存在字段中,以供使用。
    }
    public void rawOpen() {
        printLine();
    }
    public void rawPrint() {
        System.out.println("|" + string + "|");         // 前后加上"|"并显示
    }
    public void rawClose() {
        printLine();
    }
    private void printLine() {
        System.out.print("+");                          // 显示用来表示方框的角的"+"
        for (int i = 0; i < width; i++) {               // 显示width个"-"
            System.out.print("-");                      // 将其用作方框的边框
        }
        System.out.println("+");                        // 显示用来表示方框的角的"+"
    }
}

private DisplayImpl impl; 其中impl把两个两种层次结构给连接起来了

使用

    public static void main(String[] args) {
        B1_Display d1 = new B1_Display(new B4_StringDisplayImpl("Hello, China."));
        B1_Display d2 = new B2_CountDisplay(new B4_StringDisplayImpl("Hello, World."));
        B2_CountDisplay d3 = new B2_CountDisplay(new B4_StringDisplayImpl("Hello, Universe."));
        d1.display();
        d2.display();
        d3.display();
        d3.multiDisplay(5);
    }

结果

+-------------+
|Hello, China.|
+-------------+
+-------------+
|Hello, World.|
+-------------+
+----------------+
|Hello, Universe.|
+----------------+
+----------------+
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
+----------------+

在这里插入图片描述

桥接模式在JDK源码中的应用

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

Class.forName("com.mysql.jdbc.Driver");
conn = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/ssm", "root", "123456");
ps = (PreparedStatement) conn.prepareStatement("select * from flower");
rs = ps.executeQuery();

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

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

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

public class DriverManager {
...
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
...
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
...
...
    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 {

        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            throw new NullPointerException();
        }
        println("registerDriver: " + driver);
    }
...

接下来继续执行客户端代码的第二步,调用DriverManager的getConnection()方法获取连接对象

public class DriverManager {
...
    @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()));
    }
...
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
        println("DriverManager.getConnection(\"" + url + "\")");
        SQLException reason = null;
        for(DriverInfo aDriver : registeredDrivers) {
            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 (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 API中的DriverManager就是桥,如下图所示。 在这里插入图片描述