Design-Pattern-Bridge
桥接模式(Bridge Pattern)
是一种结构型模式。它的主要作用是将抽象部分与实现部分分离,使它们可以独立变化。
场景
有一个形状Shape
类,从它扩展出两个子类:长方形Rectangle
和
圆形Circle
。假设需要在另一个维度颜色Color
上,对Shape
再进行扩展细分。如果只有红色Red
和蓝色Blue
两种颜色,那么一共需要四个类覆盖所有的组合。
1 |
|
在形状和颜色种类不多时,上述的继承关系并没有太大问题。但如果有一百种形状,那么想新加一种颜色,就需要新增一百个新增颜色和已有形状组合的类。更不用说需要在更多维度进行扩展,例如材质Material
、尺寸Size
等等。具体来说,类需要在\(n\)个维度拓展,第\(i\)个维度的种类为\(m_i\),那么类的组合数量为\(\prod \limits_{i=1}^n
m_i\)。这种类的数量随着维度的扩展剧烈增长,造成系统复杂性过高、难以维护的情况,称为类爆炸。
BlueRectangle
和BlueCircle
关于颜色方面的代码中,是高度重复的。当需要对Blue
的代码内容进行改动时,需要改动所有和Blue
有关的类。从另一个角度说,形状和颜色是两种不相关的维度。如果把两个维度写在同一个类中,违反了单一职责原则。这可能导致改动颜色代码时,会破坏形状中的功能。
桥接模式
解决方法也很直观。对于重复出现的Bule
和Red
代码,抽取到单独的类中。并在形状中,组合对颜色的引用。当形状需要访问颜色相关的特性时,委派给具体颜色对象的方法。形状中对颜色的引用,像一座桥梁,将两者的功能组合到一起,因此称其为桥接模式。此时类关系降维成了下图。
1 |
|
重构完后,类的数量变成了\(\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
对应抽象Abstraction
,Color
对应实现Implementation
,Rectangle
和Cirle
对应精确抽象RefinedAbstraction
,Blue
和Red
对应具体实现ConcreteImplementation
。核心结构是,抽象通过组合,持有了实现的对象。为了获得了实现相关的特性,当抽象中方法调用,需要委派给实际实现对象。客户端只与抽象交互。
实际上,桥接模式在前文中已经出现过。Design-Pattern-Design-Principle中,在讨论组合优于继承的话题时,举例了抽象怪物类中,组合了实现飞行和攻击的能力。这里通过桥接模式,避免了“会攻击不会飞行的怪物类”、“不会攻击会飞行的怪物类”的产生。
抽象与实现
在描述桥接模式时,使用了GoF
书籍中定义的概念,抽象与实现。
由于这两个概念,和面向对象中的抽象类和接口实现概念形式上相似,容易引起歧义。让人觉得抽象是接口定义,而实现是对接口的具体实现。然而两者没有必然联系,虽然语义上存在相似之处。
具体来说,桥接模式中的抽象,不一定是抽象类,可以是具体类。实现也不一定接口实现类,一般是接口本身。抽象和实现也不是Design-Pattern-Relationships-Between-Objects中的继承或实现关系,而是组合关系。所以这里的抽象和实现,和编程语言中说的不是同一个事。
桥接模式中的抽象,一般是高阶控制层,负责定义对象的行为和属性,使得客户端可以通过它与实现部分进行交互,而不必关心实现的细节;而实现定义了抽象部分所描述的操作的具体实现,关注的是如何执行操作。从这个角度看,宏观上抽象与实现之间的关系和编程语言中的接口和接口实现概念相近,都是一个负责定义行为,另一个负责具体实现,强调将接口与其实现分离,以提高灵活性和可扩展性。区别是接口与接口实现通常只涉及一个层次,而抽象与实现涉及多个层次。抽象与每个实现都对应着一组接口与接口实现。例如上例中,抽象对应着Shape
抽象类和Rectangle
和Cirle
具体类,实现对应着Color
接口和Blue
和Red
实现类。并且在抽象中,包含着对实现的引用,和核心业务逻辑和行为;而在接口中,无法定义成员变量,通常也不会包含实际业务代码(虽然java8
接口也可以有默认方法)。
桥接模式适用于类在多个维度上进行拓展的场景。在上例中,我们天然地认为了形状是抽象,颜色是实现,是形状持有了颜色的引用。抽象和实现无法相互替换,意味着多个维度并不是一视同仁,地位均等的。哪个为抽象,哪个为实现,主要是根据这些维度在系统中的角色,和它们对系统的设计影响来决定的。一个有颜色的形状,会比有形状的颜色要来的更符合客户端的需求。
此外桥接模式中,通过抽象和实现进行分离,再通过组合,形成\(AB = A + B\)的效果,需要两者功能相互独立。如果具体抽象会影响到具体实现的行为,则不适用于桥接模式。
与适配器模式的区别
桥接模式中,抽象组合了实现,通过成员变量持有被包装类的对象,也是一种包装。
适配器模式与桥接模式的主要区别如下:
- 适配器模式中,被包装类的代码通常来自第三方库或陈年代码,没有改动权限;而在桥接模式中,实现(被包装的对象)的代码通常需要开发人员自己编写。
- 从动机上来说,适配器模式则是将被适配接口,转换为了客户端需要的目标接口,扩展了代码的兼容性;桥接模式把相互独立的抽象和实现抽取到了两个类中,避免类爆炸。
- 适配器模式的目标接口与被适配接口,通常是功能相近但不同的接口;桥接模式的抽象与实现中的接口,通常是两个不同维度,接口完全不同。
与策略模式的区别
在行为型模式中有一种策略模式。策略模式中通常是一个上下文组合一个策略的引用,也是一种包装。策略通常是一个接口,在运行时能够更换其不同实现类。从这来看,策略模式的类结构图和桥接模式几乎相同。网上很多回答会强调,桥接模式是结构型模式,策略模式是行为型模式,所以他们俩不一样。这是在玩概念游戏,是在用定义去解释特征。对于我们区分两者也没有帮助。
如果它看起来像鸭子、游泳像鸭子、叫声像鸭子,那么它可能就是只鸭子。
我们应更多从本质上进行区分。实际上很多策略模式的例子,在桥接模式的概念中解释也没有太多违和感。上下文通常作为高阶控制层,在实际执行时委派给了具体策略实现。上下文通常独立于具体策略,因此可以相互独立地改动两部分代码。这些正是上文中对桥接模式的描述,在策略模式上同样成立。最显著的区别,是桥接模式中,抽象可以进行拓展,而策略模式中的上下文通常不拓展,除此外没有区别。因此笔者认为策略模式是桥接模式的一种特例。实际编写代码时,通常也不加区分,不会指定要使用其中一种。
总结
如果一个类,需要在多个维度上进行拓展,可能会导致类爆炸问题,可以通过桥接模式进行解耦。
选定其中相对用于控制的维度作为抽象,将其他维度抽取成实现。通过组合的形式在抽象中持有实现的引用。在客户端访问抽象时,把实际执行委派给实现对象。
桥接模式与适配器模式、策略模式均为包装。结构相似,但又有所不同。