Design-Pattern-Bridge

桥接模式(Bridge Pattern)是一种结构型模式。它的主要作用是将抽象部分与实现部分分离,使它们可以独立变化。

场景

有一个形状Shape类,从它扩展出两个子类:长方形Rectangle和 圆形Circle。假设需要在另一个维度颜色Color上,对Shape再进行扩展细分。如果只有红色Red和蓝色Blue两种颜色,那么一共需要四个类覆盖所有的组合。

1
2
3
4
5
                   ----Shape---
/ \
Rectangle Circle
/ \ / \
BlueRectangle RedRectangle BlueCircle RedCircle

在形状和颜色种类不多时,上述的继承关系并没有太大问题。但如果有一百种形状,那么想新加一种颜色,就需要新增一百个新增颜色和已有形状组合的类。更不用说需要在更多维度进行扩展,例如材质Material、尺寸Size等等。具体来说,类需要在\(n\)个维度拓展,第\(i\)个维度的种类为\(m_i\),那么类的组合数量为\(\prod \limits_{i=1}^n m_i\)。这种类的数量随着维度的扩展剧烈增长,造成系统复杂性过高、难以维护的情况,称为类爆炸

BlueRectangleBlueCircle关于颜色方面的代码中,是高度重复的。当需要对Blue的代码内容进行改动时,需要改动所有和Blue有关的类。从另一个角度说,形状和颜色是两种不相关的维度。如果把两个维度写在同一个类中,违反了单一职责原则。这可能导致改动颜色代码时,会破坏形状中的功能。

桥接模式

解决方法也很直观。对于重复出现的BuleRed代码,抽取到单独的类中。并在形状中,组合对颜色的引用。当形状需要访问颜色相关的特性时,委派给具体颜色对象的方法。形状中对颜色的引用,像一座桥梁,将两者的功能组合到一起,因此称其为桥接模式。此时类关系降维成了下图。

1
2
3
          ----Shape---                        Color
/ \ / \
Rectangle(Color) Circle(Color) Blue Red

重构完后,类的数量变成了\(\sum \limits_{i=1}^n m_i\),代码重复率下降,形状和颜色的代码也不再耦合。

classDiagram
    direction TB
    class Client
    
    class Abstraction {
    	- Implementation i
    	+featureA()
    }

	Client --> Abstraction
	
    class RefinedAbstraction {
    	+featureB()
    }
    
	RefinedAbstraction --|> Abstraction
	
	class Implementation {
		<< interface >>
		+method()
	}
	
	Abstraction --o Implementation
	
	class ConcreteImplementation {
		+method()
	}

	ConcreteImplementation ..|> Implementation

在类图中,上例Shape对应抽象AbstractionColor对应实现ImplementationRectangleCirle对应精确抽象RefinedAbstractionBlueRed对应具体实现ConcreteImplementation。核心结构是,抽象通过组合,持有了实现的对象。为了获得了实现相关的特性,当抽象中方法调用,需要委派给实际实现对象。客户端只与抽象交互。

实际上,桥接模式在前文中已经出现过。Design-Pattern-Design-Principle中,在讨论组合优于继承的话题时,举例了抽象怪物类中,组合了实现飞行和攻击的能力。这里通过桥接模式,避免了“会攻击不会飞行的怪物类”、“不会攻击会飞行的怪物类”的产生。

抽象与实现

在描述桥接模式时,使用了GoF书籍中定义的概念,抽象实现

由于这两个概念,和面向对象中的抽象类接口实现概念形式上相似,容易引起歧义。让人觉得抽象是接口定义,而实现是对接口的具体实现。然而两者没有必然联系,虽然语义上存在相似之处。

具体来说,桥接模式中的抽象,不一定是抽象类,可以是具体类实现也不一定接口实现类,一般是接口本身。抽象实现也不是Design-Pattern-Relationships-Between-Objects中的继承或实现关系,而是组合关系。所以这里的抽象和实现,和编程语言中说的不是同一个事。

桥接模式中的抽象,一般是高阶控制层,负责定义对象的行为和属性,使得客户端可以通过它与实现部分进行交互,而不必关心实现的细节;而实现定义了抽象部分所描述的操作的具体实现,关注的是如何执行操作。从这个角度看,宏观上抽象实现之间的关系和编程语言中的接口接口实现概念相近,都是一个负责定义行为,另一个负责具体实现,强调将接口与其实现分离,以提高灵活性和可扩展性。区别是接口接口实现通常只涉及一个层次,而抽象实现涉及多个层次。抽象每个实现都对应着一组接口接口实现。例如上例中,抽象对应着Shape抽象类和RectangleCirle具体类,实现对应着Color接口和BlueRed实现类。并且在抽象中,包含着对实现的引用,和核心业务逻辑和行为;而在接口中,无法定义成员变量,通常也不会包含实际业务代码(虽然java8接口也可以有默认方法)。

桥接模式适用于类在多个维度上进行拓展的场景。在上例中,我们天然地认为了形状抽象颜色实现,是形状持有了颜色的引用。抽象实现无法相互替换,意味着多个维度并不是一视同仁,地位均等的。哪个为抽象,哪个为实现,主要是根据这些维度在系统中的角色,和它们对系统的设计影响来决定的。一个有颜色的形状,会比有形状的颜色要来的更符合客户端的需求。

此外桥接模式中,通过抽象和实现进行分离,再通过组合,形成\(AB = A + B\)的效果,需要两者功能相互独立。如果具体抽象会影响到具体实现的行为,则不适用于桥接模式。

与适配器模式的区别

桥接模式中,抽象组合了实现,通过成员变量持有被包装类的对象,也是一种包装

适配器模式与桥接模式的主要区别如下:

  1. 适配器模式中,被包装类的代码通常来自第三方库或陈年代码,没有改动权限;而在桥接模式中,实现(被包装的对象)的代码通常需要开发人员自己编写。
  2. 从动机上来说,适配器模式则是将被适配接口,转换为了客户端需要的目标接口,扩展了代码的兼容性;桥接模式把相互独立的抽象和实现抽取到了两个类中,避免类爆炸。
  3. 适配器模式的目标接口与被适配接口,通常是功能相近但不同的接口;桥接模式的抽象与实现中的接口,通常是两个不同维度,接口完全不同。

与策略模式的区别

在行为型模式中有一种策略模式。策略模式中通常是一个上下文组合一个策略的引用,也是一种包装策略通常是一个接口,在运行时能够更换其不同实现类。从这来看,策略模式的类结构图和桥接模式几乎相同。网上很多回答会强调,桥接模式是结构型模式,策略模式是行为型模式,所以他们俩不一样。这是在玩概念游戏,是在用定义去解释特征。对于我们区分两者也没有帮助。

如果它看起来像鸭子、游泳像鸭子、叫声像鸭子,那么它可能就是只鸭子。

我们应更多从本质上进行区分。实际上很多策略模式的例子,在桥接模式的概念中解释也没有太多违和感。上下文通常作为高阶控制层,在实际执行时委派给了具体策略实现。上下文通常独立于具体策略,因此可以相互独立地改动两部分代码。这些正是上文中对桥接模式的描述,在策略模式上同样成立。最显著的区别,是桥接模式中,抽象可以进行拓展,而策略模式中的上下文通常不拓展,除此外没有区别。因此笔者认为策略模式是桥接模式的一种特例。实际编写代码时,通常也不加区分,不会指定要使用其中一种。

总结

如果一个类,需要在多个维度上进行拓展,可能会导致类爆炸问题,可以通过桥接模式进行解耦。

选定其中相对用于控制的维度作为抽象,将其他维度抽取成实现。通过组合的形式在抽象中持有实现的引用。在客户端访问抽象时,把实际执行委派给实现对象。

桥接模式与适配器模式、策略模式均为包装。结构相似,但又有所不同。


Design-Pattern-Bridge
http://dracoyus.github.io/2024/07/26/Design-Pattern-Bridge/
作者
DracoYu
发布于
2024年7月26日
许可协议