如果实现系统可能有多维度分类,而每种分类都可能有变化,那么就把这种多维度分离出来,让他们独立变化,减少他们之间的强耦合。

桥接模式是一种结构型设计模式,可以将业务逻辑拆分成不同的部分,从而能独立地进行开发。

背景

假如你有一个几何形状Shape类,从它能扩展出两个子类:圆形Circle和方形Square 。

  • 你希望对这样的类层次结构进行扩展以使其包含颜色,所以你打算创建名为红色Red和蓝色Blue的形状子类。
  • 由于你已有两个子类,所以总共需要创建四个类才能覆盖所有组合,例如蓝色圆形BlueCircle和红色方形RedSquare。

在层次结构中新增形状和颜色将导致代码复杂程度指数增长,使情况越来越糟。

解决方案

问题的根本原因是我们试图在两个独立的维度——形状与颜色——上扩展形状类,这在处理类继承时是很常见的问题。

桥接模式通过将继承改为组合的方式来解决这个问题。

具体来说,就是抽取其中一个维度并使之成为独立的类层次,这样就可以在初始类中引用这个新层次的对象,从而使得一个类不必拥有所有的状态和行为。

根据该方法,可以

  • 将颜色相关的代码抽取到拥有红色和蓝色两个子类的颜色类中
  • 在形状类中添加一个指向某一颜色对象的引用成员变量。

现在,形状类可以将所有与颜色相关工作委派给连入的颜色对象。这样的引用就成为了形状和颜色之间的桥梁。 此后,新增颜色将不再需要修改形状的类层次,反之亦然。

在桥接模式中,层次结构中的第一层(通常称为抽象部分)将包含对第二层(实现部分)对象的引用。

  • 抽象部分将能将一些(有时是绝大部分)对自己的调用委派给实现部分的对象。
  • 所有的实现部分都有一个通用接口, 因此它们能在抽象部分内部相互替换,以实现不同的执行效果。

桥接模式

适用场景

  • 如果你想要拆分或重组一个具有多重功能的庞杂类(例如能与多个数据库服务器进行交互的类),可以使用桥接模式。
    • 桥接模式可以将庞杂类拆分为几个类层次结构。
    • 此后你可以修改任意一个类层次结构而不会影响到其他类层次结构。
    • 这种方法可以简化代码的维护工作,并将修改已有代码的风险降到最低。
  • 如果你希望在几个独立维度上扩展一个类,可使用该模式。
    • 桥接建议将每个维度抽取为独立的类层次。初始类将相关工作委派给属于对应类层次的对象,无需自己完成所有工作。
  • 如果你需要在运行时切换不同实现方法,可使用桥接模式。
    • 桥接模式可替换抽象部分中的实现对象,具体操作就和给成员变量赋新值一样简单。
    • 这点是很多人混淆桥接模式和策略模式的主要原因。记住,设计模式并不仅是一种对类进行组织的方式,它还能用于沟通意图和解决问题。

实现步骤

  1. 明确类中独立的维度。独立的概念可能是:抽象/平台,域/基础设施,前端/后端或接口/实现。
  2. 了解客户端的业务需求,并在抽象基类中定义它们。
  3. 确定在所有平台上都可执行的业务,并在通用实现接口中声明抽象部分所需的业务。
  4. 为你域内的所有平台创建实现类,但需确保它们遵循实现部分的接口。
  5. 在抽象类中添加指向实现类型的引用成员变量,抽象部分会将大部分工作委派给该成员变量所指向的实现对象。
  6. 如果你的高层逻辑有多个变体,则可通过扩展抽象基类为每个变体创建一个精确抽象。
  7. 客户端代码必须将实现对象传递给抽象部分的构造函数才能使其能够相互关联。 此后,客户端只需与抽象对象进行交互,无需和实现对象打交道。

优缺点

优点

  • 可以创建与平台无关的类和程序。
  • 客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。
  • 开闭原则。你可以新增抽象部分和实现部分,且它们之间不会相互影响。
  • 单一职责原则。抽象部分专注于处理高层逻辑,实现部分处理平台细节。

缺点

  • 对高内聚的类使用该模式可能会让代码更加复杂。

和其它模式的关系

  • 桥接模式通常会于开发前期进行设计,能够将程序的各个部分独立开来以便开发。
    • 适配器模式通常在已有程序中使用,让相互不兼容的类能很好地合作。
  • 桥接、状态模式策略模式 (在某种程度上包括适配器)的接口非常相似。
    • 实际上它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。
    • 模式并不只是以特定方式组织代码的配方,还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 可以将抽象工厂模式和桥接搭配使用。
    • 如果由桥接定义的抽象只能与特定实现合作,这一搭配就非常有用。
    • 在这种情况下,抽象工厂可以对这些关系进行封装,并且对客户端代码隐藏其复杂性。
  • 可以结合使用建造者模式和桥接模式:主管类负责抽象工作,各种不同的生成器负责实现工作。

示例

假设有两台电脑:一台Mac和一台Windows。 还有两台打印机:爱普生和惠普。这两台电脑和打印机可能会任意组合使用。

客户端不应去担心如何将打印机连接至计算机的细节问题。

如果引入新的打印机,不会希望代码量成倍增长。所以创建了两个层次结构,而不是2x2组合的四个结构体:

  • 抽象层: 代表计算机
  • 实施层: 代表打印机

这两个层次可通过桥接进行沟通,其中抽象层(计算机)包含对于实施层(打印机)的引用。 抽象层和实施层均可独立开发,不会相互影响,减少了代码冗余。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Computer 抽象
type Computer interface {
	Print()
	SetPrinter(Printer)
}

// Printer 实施
type Printer interface {
    PrintFile()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

// Mac 精确抽象
type Mac struct {
	printer Printer
}

func (m *Mac) Print() {
	fmt.Println("Print request for mac")
	m.printer.PrintFile()
}

func (m *Mac) SetPrinter(printer Printer) {
	m.printer = printer
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

// Windows 精确抽象
type Windows struct {
	printer Printer
}

func (w *Windows) Print() {
	fmt.Println("Print request for windows")
	w.printer.PrintFile()
}

func (w *Windows) SetPrinter(printer Printer) {
	w.printer = printer
}
1
2
3
4
5
6
7
// Hp 具体实施
type Hp struct {
}

func (h *Hp) PrintFile() {
	fmt.Println("Printing by a HP Printer")
}
1
2
3
4
5
6
7
// Epson 具体实施
type Epson struct {
}

func (e *Epson) PrintFile() {
	fmt.Println("Printing by a EPSON Printer")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

func main() {
  // 具体实施对象
  hpPrinter := &Hp{}
  epsonPrinter := &Epson{}

  var c Computer

  // 精确抽象对象
  c = &Mac{}
  // 设置具体实施对象
  c.SetPrinter(hpPrinter)
  c.Print()
  // 替换具体实施对象
  c.SetPrinter(epsonPrinter)
  c.Print()

  // 精确抽象对象
  c = &Windows{}
  // 设置具体实施对象
  c.SetPrinter(hpPrinter)
  c.Print()
  // 替换具体实施对象
  c.SetPrinter(epsonPrinter)
  c.Print()
}

References

guru