设计模式(八):桥接模式

Published on 2024-03-07 16:01 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、桥接模式定义

桥接模式(Bridge Pattern)也叫做桥梁模式,是一个比较简单的模式,其定义如下:Decouple an abstraction from its implementation so that the two can vary independently.(将抽象和实现解耦,使得两者可以独立地变化。)

这个模式说简单也简单,但是在维基百科上却说它是最复杂的设计模式之一:桥接模式软件设计模式中最复杂的模式之一,它把事物对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。

具体怎么看待它,这就得仁者见仁了。

桥接模式的类图结构如下所示:

桥接模式.drawio

这里面有四个重要的角色:

1、Abstraction

抽象类,它维护一个对实现化角色的引用,并将具体实现的细节委托给实现化角色。也就是说它维护了 Implementor / 即它的实现类 ConcreteImplementorA..,二者是聚合关系,Abstraction 充当桥接类

2、RefinedAbstraction

是Abstraction的子类,Refined是“改进的”、“精炼的”或“完善的”的意思。在软件工程和设计模式中,Refined通常用来表示对某个概念、角色或者类进行更进一步的改进、扩展或者细化,在桥接模式中,"Refined Abstraction"表示对抽象化角色进行进一步的扩展和细化,提供更具体和特定的功能。或许有些人会疑惑为什么不叫”ConcreteAbstractoin“,只是看名字已经矛盾了,具体的抽象化?哈哈哈,而且这里使用Refined进行描述更加贴合”抽象化角色的扩展和细化“这一定位。

3、Implementor

行为实现类的接口

4、ConcreteImplementor

行为的具体实现类

桥接模式把抽象与行为实现两个层分开了,来保持各部分的独立性以及他们的功能扩展。

说起来还是比较抽象,使用一个案例来解释说明下。

二、桥接模式案例

1、案例演示:电脑分类

我们将电脑分为台式机电脑、笔记本电脑、IPad三种类型,但是从品牌上来讲,它可能是Dell、华为、三星、苹果、联想电脑,每个品牌都有台式机、笔记本和IPad,通常来说,我们类图建模可能如下所示:

桥接模式-电脑分类建模.drawio

在这个模型里,扩展会变得很困难:如果我们再增加一个电脑类型,比如一体机,就需要增加各个品牌的一体机;同样如果再增加一个品牌小米,也要在各个类型下增加,这样的话,扩展性很差,即(类爆炸)

2、使用桥接模式重构

桥接模式是基于 合成复用原则 设计的一种设计模式。

合成复用原则强调通过组合(合成)而不是继承来实现代码复用。合成复用原则鼓励使用对象组合,将对象的功能组合起来,而不是通过继承来获得功能。这样可以使系统更加灵活、可扩展,并避免继承层次结构的臃肿和耦合。

桥接模式正是基于合成复用原则的一种实现方式。它通过将抽象化和实现化分离,使用桥接(Bridge)连接它们,实现了抽象和实现的解耦。通过桥接模式,可以在两个独立变化的维度上进行扩展,而不会相互影响。

具体来说,在桥接模式中,抽象化角色(Abstraction)和实现化角色(Implementor)之间的关系是通过聚合、组合关系建立的,而不是通过继承关系。

现在重新看看上面的案例,使用桥接模式的方式如何解决上面的问题,先看下重构后的类图

桥接模式-桥接模式电脑分类重构.drawio

具体代码如下

Implementor

Brand是Implementor的角色,它的代码如下

/**
 * Implementor
 */
public interface Brand {
    
    void open();
    
    void close();
    
    void work();
}

ConcreteImplementor

各种具体的品牌是ConcreteImplementor的角色,代码如下

/**
 * ConcreteImplementor Huawei
 */
public class Huawei implements Brand{
    @Override
    public void open() {
        System.out.println("华为电脑开机中");
    }

    @Override
    public void close() {
        System.out.println("华为电脑关机中");
    }

    @Override
    public void work() {
        System.out.println("华为电脑工作中");
    }
}

/**
 * ConcreteImplementor Apple
 */
public class Apple implements Brand {
    
    @Override
    public void open() {
        System.out.println("苹果电脑开机中");
    }

    @Override
    public void close() {
        System.out.println("苹果电脑关机中");
    }

    @Override
    public void work() {
        System.out.println("苹果电脑工作中");
    }
}

Abstraction

Computer是Abstraction的角色,它的代码如下

/**
 * Abstraction
 */
public class Computer {

    private Brand brand;

    public Computer(Brand brand) {
        this.brand = brand;
    }

    public void open() {
        brand.open();
    }

    public void work() {
        brand.work();
    }

    public void close() {
        brand.close();
    }
}

RefinedAbstractoin

DeskPC和Laptop是RefinedAbstraction角色,代码如下所示

/**
 * RefinedAbstraction
 */
public class DeskPC extends Computer{
    
    public DeskPC(Brand brand) {
        super(brand);
    }

    @Override
    public void open() {
        super.open();
        System.out.println("台式机");
    }

    @Override
    public void work() {
        super.work();
        System.out.println("台式机");
    }

    @Override
    public void close() {
        super.close();
        System.out.println("台式机");
    }
}



/**
 * RefinedAbstraction
 */
public class Laptop extends Computer{
    public Laptop(Brand brand) {
        super(brand);
    }

    @Override
    public void open() {
        super.open();
        System.out.println("笔记本");
    }

    @Override
    public void work() {
        super.work();
        System.out.println("笔记本");
    }

    @Override
    public void close() {
        super.close();
        System.out.println("笔记本");
    }
}

启动类

Client类是启动类

public class Client {

    public static void main(String[] args) {
        DeskPC pc = new DeskPC(new Huawei());
        pc.open();
        pc.work();
        pc.close();
    }
}

案例分析

看下这行代码的调用

DeskPC pc = new DeskPC(new Huawei());
pc.open();

pc.open方法实际上调用了computer的open方法,computer的open方法实际上调用了Brand的open方法,那实际上就是调用的Brand的open方法,类Computer实际上充当了一个桥梁的作用,“桥接模式”正是这个意思。

PS. 家庭上网电信给的光猫有两种上网方式,一种是路由模式,另外一种是桥接模式,在这里的桥接模式指的是光猫什么也不做,就像一根网线一样,想要上网需要有专门的路由器拨号上网,这时候的光猫就像是桥梁一样连接路由器和运营商网络,所以叫做“桥接模式”,和设计模式中的桥接模式的意思有异曲同工之妙。

这时候无论是想新增加一种电脑类型:比如一体机,或者新增一个电脑品牌,比如小米,都只需要新增一个类即可,两个维度都可随意独立扩展。

三、总结

优势

  • 分离抽象接口及其实现部分,提高了比继承更好的解决方案,可以减少子类的个数,降低系统的管理和维护成本
  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统

劣势

  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性

使用场景

  • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
  • 一个系统存在两个独立变化的维度,且这两个维度都需要进行扩展,桥接模式最合适不过

四、源码案例

这里以JDK的日志类为例,先看如下这段代码

Logger logger = Logger.getLogger("demo.logger.Main");
// 设置为记录 INFO 级别及以上的日志消息
logger.setLevel(Level.INFO); 
logger.setUseParentHandlers(false);
// 将日志消息输出到控制台
Handler handler = new ConsoleHandler(); 
logger.addHandler(handler);
handler.setLevel(Level.INFO);
// 设置简单的日志消息格式化方式
Formatter formatter = new SimpleFormatter();
handler.setFormatter(formatter);
logger.info("这是一条info级别的消息");

这是一段将日志打印到控制台的代码,其中,Handler和Formatter类的设计应用了桥接模式的思想。

Handler类:

image-20240307152421637

Formatter类:

image-20240307152452191

可以看到Handler类持有Formatter对象,并有个方法可以动态的设置Formatter对象

image-20240307152613626

而且Handler和Formatter都有自己不同的实现,具体类图如下

桥接模式-日志框架中的桥接模式.drawio

需要注意的是,Handler 和 Formatter 并没有直接应用桥接模式,但是,Handler 和 Formatter 的设计和用途与桥接模式中的抽象和实现相似,可以说具有一定的桥接模式的概念。

在日志记录器中,Handler 负责处理日志消息的输出,而 Formatter 则负责对日志消息进行格式化。Handler 和 Formatter 之间的关系可以看作是桥接模式中的抽象和实现的关系。

Handler 充当了桥接模式中的抽象角色,它定义了处理日志消息的通用接口,并将具体的处理实现委托给具体的子类。

Formatter 充当了桥接模式中的实现角色,它定义了对日志消息进行格式化的接口,并将具体的格式化实现委托给具体的子类。

通过使用Handler和Formatter的组合,可以实现不同的处理方式和格式化方式,而它们之间的关系是松耦合的,可以独立进行扩展和修改。这种设计类似于桥接模式中将抽象和实现分离的思想,使得处理和格式化的实现可以独立变化,提高了代码的灵活性和可扩展性。


#设计模式
目录