模式说明

桥接模式的解释,就是“抽象与实现解耦,使两者可以独立变化”,这种模式是严格遵守了组合优于继承原则。那么,讲解桥接模式之前,我们先来看看继承会有哪些缺点。如果一个类不是专门为了继承而设计则不应该被继承,这是为什么呢?因为该类的方法有可能会随时发生改变,有可能是添加 final 或改名等,都会使得子类无法使用。又或者父类增加了一个与子类方法名相同但返回类型不同的方法,会致使编译错误。

并不是说继承一无是处,继承非常强大,但很多时候会违反封装性,这是因为父类可能会不断改变直接影响子类的功能实现。如果子类和父类存在 “is-a” 关系,这样才值得使用继承,就如模板设计模式。

而桥接模式则是为了解耦继承中的抽象和实现,从而避免类继承所带来的缺点,且不需受到继承的约束。而具体的实现也非常简单就是通过组合及依赖注入的方式完成,难点就在于识别出抽象和实现。

应用场景

通过桥接模式将继承关系拆解,可以参考如下图 1 所示。
image.png
图 1

桥接模式能够使程序更容易被扩展,而且不受继承的约束。以 Java 中的 InputStreamReader 为例,这个是典型的桥接模式设计,现在参考它的实现方式进行扩展,将字节流转为 Base64 及 Hex 文本,相应的增加桥接类 Base64Reader / HexReader。桥接模式能够使得 InputStream 不需要继承类或实现接口的方式提供额外的字节流转字符流的功能,极大限度的增加其可扩展性,类之间的关系如下图 2 所示。
image.png
图 2

代码实现

字符流扩展

现在我们通过代码实现 Base64Reader / HexRerader,将字节流转为 Base64 / Hex 字符串,代码如下。

  1. public class Base64Reader extends Reader {
  2. private InputStream in;
  3. public Base64Reader(InputStream in) {
  4. super(in);
  5. this.in = in;
  6. }
  7. @Override
  8. public int read(char[] cbuf, int off, int len) throws IOException {
  9. // 3 bytes -> 4 chars
  10. return ...;
  11. }
  12. @Override
  13. public void close() throws IOException {
  14. in.close();
  15. }
  16. }
  1. public class HexReader extends Reader {
  2. private InputStream in;
  3. public HexReader(InputStream in) {
  4. super(in);
  5. this.in = in;
  6. }
  7. @Override
  8. public int read(char[] cbuf, int off, int len) throws IOException {
  9. // 1 byte -> 2 chars
  10. return ...;
  11. }
  12. @Override
  13. public void close() throws IOException {
  14. in.close();
  15. }
  16. }

项目实战

再来看一个实际的项目,要实现的功能是将 DSL 风格的代码转换为 SQL,主要是为了解决重复编写 JDBC 代码,以 Java 代码风格代替直接拼写 SQL 语句,代码片段如下所示。

  1. select().from("t_table").where().eq("name", "jsql").execQuery();
  2. // SQL: select * from t_table where name=?
  3. // values: ["jsql"]

刚开始设计了一个 Builder 接口,其中接口就有非常多的方法,如 select() / update() / insert() / delete() / where() / and() / or() / eq()… 还有很多其他方法。为了区分 select() / update() / insert() / delete() 四种类型,于是抽象出模板类 AbstractBuilder,分别创建 SelectBuilder / UpdateBuilder / InsertBuilder / DeleteBuilder 继承 AbstractBuilder,以保证每个操作的合法性,如 update / insert / delete 不能使用 forUpdate() 操作,至今类的关系会是如下图 3 所示。
image.png
图 3

上面的需求中并没有提及使用哪种数据库,要知道 MySQL / Oracle 的 SQL 实现就不一样,虽然大部分相同,但都会带有自个的个性 SQL 特性,现在我们需要支持不同的数据库,现在将 SelectBuilder / UpdateBuilder / InsertBuilder / DeleteBuilder 继承具体的数据库 Builder,如下
图 4 所示。
image.png
图 4_

上图 4 的问题是继承的类实在太多了,简直是继承爆炸,而且只是部分的数据库,还有其他的数据库待实现。现在面临的问题就是如何减少这种继承的类数量及降低继承的层次关系。为了解耦继承关系,抽离实现和抽象,这不就是桥接模式要做的事情吗,于是将上图 4 以桥接模式重新设计后,将得到下图 5 的关系所示。
image.png
图 5

现在抽象出 Builder 负责构造 SQL,而 Dialect(方言) 则是根据不同的数据库实现,从而解决因不同数据库之间的差异引起的类继承爆炸问题。Builder 属于抽象(相当于所谓的桥),Dialect 对应的是实现,经过桥接方式的设计,将变化部分提炼到 Dialect 中处理。以下是针对上述实现的代码片段。

  1. public abstract class AbstractBuilder implements Builder {
  2. public AbstractBuilder(Dialect dialect) {
  3. this.dialect = dialect;
  4. // ...
  5. }
  6. @Override
  7. public Builder joinUsing(String tableName, String... columns) {
  8. // 并不是所有数据库都支持 join <TABLE_NAME> using 语法
  9. if (!dialect.supportJoinUsing()) {
  10. throw new UnsupportedOperationException();
  11. }
  12. // join <tableName> using (ref_id)
  13. joinUsing("join", tableName, columns);
  14. return this;
  15. }
  16. // ... ...
  17. }
  1. public interface Dialect {
  2. boolean supportJoinUsing();
  3. // ...
  4. }
  5. public class MySQLDialect implements Dialect {
  6. public boolean supportJoinUsing() {
  7. return true;
  8. }
  9. // ...
  10. }
  11. public class OrableDialect implements Dialect {
  12. public boolean supportJoinUsing() {
  13. return true;
  14. }
  15. // ...
  16. }
  1. Dialect dialect = new MySQLDialect();
  2. Builder builder = new SelectBuilder(dialect);
  3. builder.select().from("t_table").where().eq("name", "jsql").execQuery();