桥接模式 (Bridge Pattern)

9 阅读8分钟

桥接模式 (Bridge Pattern)

一、模式概述

桥接模式的核心理念是:将抽象部分与实现部分分离,使它们可以独立变化

这句话听起来很抽象,用具体场景来理解:假设你在开发一个图形绘制库,有"形状"(圆形、矩形、三角形)和"渲染器"(OpenGL、DirectX、软件渲染)两个维度。如果用继承,你需要创建 CircleOpenGLCircleDirectXCircleSoftwareRectangleOpenGL……类数量是 M×N 的笛卡尔积。每新增一个形状或一个渲染器,都要修改大量类。

桥接模式的解法是:把"形状"和"渲染器"拆成两条独立的继承链,形状持有渲染器的引用,通过组合而非继承连接它们。新增形状不影响渲染器,新增渲染器不影响形状。类数量从 M×N 降为 M+N。

为什么叫"桥接"?

因为两条继承链之间有一座"桥"——抽象类持有一个实现接口的引用。这座桥让两端可以独立演化:

  抽象层                        实现层
  Shape ───────桥─────── Renderer
   / \                       / \
Circle Rectangle        OpenGL DirectX

二、模式结构

角色职责
Abstraction(抽象)定义高层接口,持有 Implementor 引用
RefinedAbstraction(扩展抽象)扩展 Abstraction 的功能
Implementor(实现接口)定义底层操作接口,供 Abstraction 调用
ConcreteImplementor(具体实现)实现 Implementor 接口的具体类

关键设计原则:Abstraction 不直接创建 ConcreteImplementor,而是通过 Implementor 接口调用。这样 Abstraction 完全不依赖具体实现。

三、Go 语言实现

3.1 形状与渲染器

package main

import "fmt"

// Renderer 实现接口 —— 绘制原语
type Renderer interface {
	DrawCircle(x, y, radius int)
	DrawRectangle(x, y, width, height int)
}

// ===== 具体实现:不同渲染器 =====

type OpenGLRenderer struct{}

func (r *OpenGLRenderer) DrawCircle(x, y, radius int) {
	fmt.Printf("  [OpenGL] 绘制圆形: 中心(%d,%d) 半径=%d\n", x, y, radius)
}
func (r *OpenGLRenderer) DrawRectangle(x, y, w, h int) {
	fmt.Printf("  [OpenGL] 绘制矩形: (%d,%d) %dx%d\n", x, y, w, h)
}

type DirectXRenderer struct{}

func (r *DirectXRenderer) DrawCircle(x, y, radius int) {
	fmt.Printf("  [DirectX] 绘制圆形: 中心(%d,%d) 半径=%d\n", x, y, radius)
}
func (r *DirectXRenderer) DrawRectangle(x, y, w, h int) {
	fmt.Printf("  [DirectX] 绘制矩形: (%d,%d) %dx%d\n", x, y, w, h)
}

// ===== 抽象层:形状 =====

type Shape struct {
	renderer Renderer // 桥接的引用
}

func (s *Shape) SetRenderer(r Renderer) {
	s.renderer = r
}

type Circle struct {
	Shape
	x, y, radius int
}

func NewCircle(x, y, radius int) *Circle {
	return &Circle{x: x, y: y, radius: radius}
}

func (c *Circle) Draw() {
	c.renderer.DrawCircle(c.x, c.y, c.radius)
}

type Rectangle struct {
	Shape
	x, y, w, h int
}

func NewRectangle(x, y, w, h int) *Rectangle {
	return &Rectangle{x: x, y: y, w: w, h: h}
}

func (r *Rectangle) Draw() {
	r.renderer.DrawRectangle(r.x, r.y, r.w, r.h)
}

func main() {
	// 两种渲染器
	ogl := &OpenGLRenderer{}
	d3d := &DirectXRenderer{}

	// 形状可以搭配任意渲染器
	circle := NewCircle(10, 20, 5)
	rect := NewRectangle(0, 0, 100, 50)

	fmt.Println("用 OpenGL 渲染:")
	circle.SetRenderer(ogl)
	circle.Draw()
	rect.SetRenderer(ogl)
	rect.Draw()

	fmt.Println("\n用 DirectX 渲染:")
	circle.SetRenderer(d3d)
	circle.Draw()
	rect.SetRenderer(d3d)
	rect.Draw()
}

运行结果:

用 OpenGL 渲染:
  [OpenGL] 绘制圆形: 中心(10,20) 半径=5
  [OpenGL] 绘制矩形: (0,0) 100x50

用 DirectX 渲染:
  [DirectX] 绘制圆形: 中心(10,20) 半径=5
  [DirectX] 绘制矩形: (0,0) 100x50

关键点:新增一种形状(比如三角形)不需要修改任何渲染器代码;新增一种渲染器(比如 VulkanRenderer)也不需要修改任何形状代码。两条维度独立演化。

3.2 消息发送——多渠道多格式

更贴近实际项目的场景:消息系统需要支持多种渠道(短信、邮件、推送)和多种格式(普通文本、HTML、Markdown)。

package main

import "fmt"

// MessageSender 实现接口 —— 发送渠道
type MessageSender interface {
	Send(content string) error
}

// ===== 具体实现:不同渠道 =====

type SMSSender struct {
	phone string
}

func (s *SMSSender) Send(content string) error {
	fmt.Printf("  [短信->%s] %s\n", s.phone, content)
	return nil
}

type EmailSender struct {
	address string
}

func (e *EmailSender) Send(content string) error {
	fmt.Printf("  [邮件->%s] %s\n", e.address, content)
	return nil
}

type PushSender struct {
	deviceID string
}

func (p *PushSender) Send(content string) error {
	fmt.Printf("  [推送->%s] %s\n", p.deviceID, content)
	return nil
}

// ===== 抽象层:消息格式 =====

type Message struct {
	sender MessageSender // 桥接
}

func (m *Message) SetSender(s MessageSender) {
	m.sender = s
}

// PlainTextMessage 纯文本消息
type PlainTextMessage struct {
	Message
	text string
}

func NewPlainTextMessage(text string) *PlainTextMessage {
	return &PlainTextMessage{text: text}
}

func (m *PlainTextMessage) Send() {
	m.sender.Send(m.text)
}

// HTMLMessage HTML 格式消息
type HTMLMessage struct {
	Message
	title string
	body  string
}

func NewHTMLMessage(title, body string) *HTMLMessage {
	return &HTMLMessage{title: title, body: body}
}

func (m *HTMLMessage) Send() {
	content := fmt.Sprintf("<html><head><title>%s</title></head><body>%s</body></html>",
		m.title, m.body)
	m.sender.Send(content)
}

// UrgentMessage 紧急消息(扩展抽象层)
type UrgentMessage struct {
	Message
	text string
}

func NewUrgentMessage(text string) *UrgentMessage {
	return &UrgentMessage{text: text}
}

func (m *UrgentMessage) Send() {
	content := fmt.Sprintf("【紧急】%s", m.text)
	m.sender.Send(content)
}

func main() {
	sms := &SMSSender{phone: "138xxxx0001"}
	email := &EmailSender{address: "user@example.com"}
	push := &PushSender{deviceID: "device-abc"}

	// 纯文本 + 不同渠道
	plain := NewPlainTextMessage("您的验证码是 123456")
	fmt.Println("纯文本消息:")
	plain.SetSender(sms)
	plain.Send()
	plain.SetSender(email)
	plain.Send()
	plain.SetSender(push)
	plain.Send()

	// HTML + 邮件
	fmt.Println("\nHTML 消息:")
	html := NewHTMLMessage("周报", "<h1>本周完成 3 个需求</h1>")
	html.SetSender(email)
	html.Send()

	// 紧急 + 短信
	fmt.Println("\n紧急消息:")
	urgent := NewUrgentMessage("服务器宕机,请立即处理!")
	urgent.SetSender(sms)
	urgent.Send()
	urgent.SetSender(push)
	urgent.Send()
}

运行结果:

纯文本消息:
  [短信->138xxxx0001] 您的验证码是 123456
  [邮件->user@example.com] 您的验证码是 123456
  [推送->device-abc] 您的验证码是 123456

HTML 消息:
  [邮件->user@example.com] <html><head><title>周报</title></head><body><h1>本周完成 3 个需求</h1></body></html>

紧急消息:
  [短信->138xxxx0001] 【紧急】服务器宕机,请立即处理!
  [推送->device-abc] 【紧急】服务器宕机,请立即处理!

这个例子清晰地展示了桥接模式的价值:3 种渠道 × 3 种格式 = 9 种组合,但代码中只有 3+3=6 个类。新增渠道或格式互不影响。

3.3 数据访问层——多数据库多查询模式

package main

import "fmt"

// DBDriver 实现接口
type DBDriver interface {
	Query(sql string) string
	Execute(sql string) error
}

// MySQLDriver 具体实现
type MySQLDriver struct {
	connStr string
}

func (d *MySQLDriver) Query(sql string) string {
	return fmt.Sprintf("[MySQL:%s] 查询: %s", d.connStr, sql)
}
func (d *MySQLDriver) Execute(sql string) error {
	fmt.Printf("[MySQL:%s] 执行: %s\n", d.connStr, sql)
	return nil
}

// PostgresDriver 具体实现
type PostgresDriver struct {
	connStr string
}

func (d *PostgresDriver) Query(sql string) string {
	return fmt.Sprintf("[PG:%s] 查询: %s", d.connStr, sql)
}
func (d *PostgresDriver) Execute(sql string) error {
	fmt.Printf("[PG:%s] 执行: %s\n", d.connStr, sql)
	return nil
}

// Repository 抽象层
type Repository struct {
	driver DBDriver
}

func (r *Repository) SetDriver(d DBDriver) {
	r.driver = d
}

// UserRepository 用户仓储
type UserRepository struct {
	Repository
}

func (ur *UserRepository) FindByID(id int) string {
	return ur.driver.Query(fmt.Sprintf("SELECT * FROM users WHERE id=%d", id))
}

func (ur *UserRepository) Create(name string, age int) error {
	return ur.driver.Execute(fmt.Sprintf("INSERT INTO users(name,age) VALUES('%s',%d)", name, age))
}

// OrderRepository 订单仓储
type OrderRepository struct {
	Repository
}

func (or *OrderRepository) FindByID(id int) string {
	return or.driver.Query(fmt.Sprintf("SELECT * FROM orders WHERE id=%d", id))
}

func (or *OrderRepository) Create(userID, amount int) error {
	return or.driver.Execute(fmt.Sprintf("INSERT INTO orders(user_id,amount) VALUES(%d,%d)", userID, amount))
}

func main() {
	mysql := &MySQLDriver{connStr: "localhost:3306"}
	pg := &PostgresDriver{connStr: "localhost:5432"}

	userRepo := &UserRepository{}
	orderRepo := &OrderRepository{}

	fmt.Println("=== MySQL 驱动 ===")
	userRepo.SetDriver(mysql)
	fmt.Println(userRepo.FindByID(1))
	userRepo.Create("张三", 25)
	orderRepo.SetDriver(mysql)
	fmt.Println(orderRepo.FindByID(100))
	orderRepo.Create(1, 999)

	fmt.Println("\n=== PostgreSQL 驱动 ===")
	userRepo.SetDriver(pg)
	fmt.Println(userRepo.FindByID(2))
	userRepo.Create("李四", 30)
	orderRepo.SetDriver(pg)
	fmt.Println(orderRepo.FindByID(200))
	orderRepo.Create(2, 888)
}

运行结果:

=== MySQL 驱动 ===
[MySQL:localhost:3306] 查询: SELECT * FROM users WHERE id=1
[MySQL:localhost:3306] 执行: INSERT INTO users(name,age) VALUES('张三',25)
[MySQL:localhost:3306] 查询: SELECT * FROM orders WHERE id=100
[MySQL:localhost:3306] 执行: INSERT INTO orders(user_id,amount) VALUES(1,999)

=== PostgreSQL 驱动 ===
[PG:localhost:5432] 查询: SELECT * FROM users WHERE id=2
[PG:localhost:5432] 执行: INSERT INTO users(name,age) VALUES('李四',30)
[PG:localhost:5432] 查询: SELECT * FROM orders WHERE id=200
[PG:localhost:5432] 执行: INSERT INTO orders(user_id,amount) VALUES(2,888)

这就是 Go 标准库 database/sql 的设计思路——sql.DB 是抽象层,driver.Driver 是实现接口,不同数据库驱动实现同一个接口,上层代码完全不变。

四、桥接模式 vs 策略模式

两者结构相似——都是"持有接口引用,运行时注入",但意图不同:

维度桥接模式策略模式
解决问题两个维度独立变化同一维度的算法可替换
生命周期实现对象通常长期持有策略对象频繁切换
设计意图结构型——分离抽象与实现行为型——封装可互换的算法
典型场景形状×渲染器、渠道×格式排序算法、支付方式

桥接关注的是结构层面的解耦——两个维度本来就是独立的概念;策略关注的是行为层面的可替换——同一个操作有多种实现方式。

五、Go 中桥接模式的天然优势

Go 的接口是隐式实现的,这使得桥接模式在 Go 中几乎是"免费"的:

  1. 不需要抽象基类:Go 的结构体嵌入天然支持"持有接口引用"
  2. 接口小而精Renderer 接口只定义 DrawCircleDrawRectangle,符合接口隔离
  3. 依赖注入自然SetRenderer 方法就是 Go 社区常见的依赖注入方式

Go 标准库中 database/sql/driverio.Reader/Writerhash.Hash 等接口体系都体现了桥接思想——定义小接口,让具体实现可独立替换。

六、小结

桥接模式的核心价值是将多维度变化拆解为独立变化链

  • M×N 的类爆炸问题 → M+N 的组合方案
  • 通过"持有接口引用"连接抽象层和实现层
  • 新增维度不影响已有代码(开放-封闭原则)
  • Go 的隐式接口让桥接模式的实现格外简洁

判断是否需要桥接模式的标准:当你发现继承体系出现两个正交的变化维度时(比如形状×渲染器、渠道×格式、数据库×仓储),就应该考虑把其中一个维度抽成接口,用组合替代继承。