第 14 章 · 桥接(Bridge)
一句话:两个各自会变的维度不要用继承乘在一起,拆成两套类,一边手里拿着另一边的对象,用的时候再组合。 解决的痛:消息类型 × 发送渠道写成子类,3 × 3 是 9 个类,各加一种变 16 个;同一段渠道代码复制在好几个类里,改规则时漏一个副本就出事。
14.1 一个场景
Section titled “14.1 一个场景”你在多练网的消息组写后端。系统要给用户和值班同事发三种消息:
- 普通通知:标题原样,发一次就算。
- 紧急告警:发给值班的人,标题前加
[紧急],发送失败最多试 3 次。 - 营销活动:标题前加
[活动],正文末尾要带退订说明。
每种消息都可能走三个渠道:App 推送、短信、邮件。渠道各有各的发送细节和签名:App 推送在标题前写“多练 ·”;短信没有标题栏,标题并进正文,开头是【多练】;邮件在落款写”—— 多练”。
每格是一种组合。直觉写法就是给每格写一个类。
14.2 先按直觉写
Section titled “14.2 先按直觉写”UrgentSmsMessage 就是“用短信发的紧急告警”,类型规则和短信细节写在一起。按类型分三个文件,每个文件三个类:
# 3 种消息 × 3 个渠道 = 9 个类,这里列出 4 个,其余 5 个照同样的样子写:# NoticeSmsMessage、NoticeEmailMessage、UrgentAppMessage、UrgentEmailMessage、MarketingAppMessage
# notice.py:普通通知class NoticeAppMessage: # 普通通知 × App 推送 def send(self, to: str, title: str, body: str) -> None: self._deliver(to, title, body) # 普通通知:标题原样,不重试
def _deliver(self, to: str, title: str, body: str) -> bool: # App 推送的发送细节 print(f"[App 推送] {to}:多练 · {title} | {body}") return True # 模拟:总是成功
# urgent.py:紧急告警class UrgentSmsMessage: # 紧急告警 × 短信 def send(self, to: str, title: str, body: str) -> None: title = f"[紧急] {title}" # 紧急告警:标题加前缀,失败最多试 3 次 for _ in range(3): if self._deliver(to, title, body): return
def _deliver(self, to: str, title: str, body: str) -> bool: # 短信的发送细节:没有标题栏,签名放最前 print(f"[短信] {to}:【多练】{title}:{body}") return True
# marketing.py:营销活动(增长组维护)class MarketingSmsMessage: # 营销活动 × 短信 def send(self, to: str, title: str, body: str) -> None: title = f"[活动] {title}" # 营销活动:标题加前缀,正文带退订说明,不重试 self._deliver(to, title, f"{body}(可在设置中退订)")
def _deliver(self, to: str, title: str, body: str) -> bool: # 和 UrgentSmsMessage 里的一模一样 print(f"[短信] {to}:【多练】{title}:{body}") return True
class MarketingEmailMessage: # 营销活动 × 邮件 def send(self, to: str, title: str, body: str) -> None: title = f"[活动] {title}" # 和 MarketingSmsMessage 里的一模一样 self._deliver(to, title, f"{body}(可在设置中退订)")
def _deliver(self, to: str, title: str, body: str) -> bool: # 邮件的发送细节:签名放落款 print(f"[邮件] {to}:主题「{title}」 {body} —— 多练") return True
if __name__ == "__main__": NoticeAppMessage().send("user_42", "课程更新", "第 14 章已上线") UrgentSmsMessage().send("138****0000", "支付服务不可用", "支付失败率 35%") MarketingSmsMessage().send("139****1111", "国庆五折", "全部课程五折")
# 输出:# [App 推送] user_42:多练 · 课程更新 | 第 14 章已上线# [短信] 138****0000:【多练】[紧急] 支付服务不可用:支付失败率 35%# [短信] 139****1111:【多练】[活动] 国庆五折:全部课程五折(可在设置中退订)# [邮件] [email protected]:主题「[活动] 国庆五折」 全部课程五折(可在设置中退订) —— 多练TS 版一样:
// 3 种消息 × 3 个渠道 = 9 个类,这里列出 4 个,其余 5 个照同样的样子写:// NoticeSmsMessage、NoticeEmailMessage、UrgentAppMessage、UrgentEmailMessage、MarketingAppMessage
// notice.ts:普通通知class NoticeAppMessage { // 普通通知 × App 推送 send(to: string, title: string, body: string): void { this.deliver(to, title, body); // 普通通知:标题原样,不重试 }
private deliver(to: string, title: string, body: string): boolean { // App 推送的发送细节 console.log(`[App 推送] ${to}:多练 · ${title} | ${body}`); return true; // 模拟:总是成功 }}
// urgent.ts:紧急告警class UrgentSmsMessage { // 紧急告警 × 短信 send(to: string, title: string, body: string): void { title = `[紧急] ${title}`; // 紧急告警:标题加前缀,失败最多试 3 次 for (let i = 0; i < 3; i++) { if (this.deliver(to, title, body)) return; } }
private deliver(to: string, title: string, body: string): boolean { // 短信的发送细节:没有标题栏,签名放最前 console.log(`[短信] ${to}:【多练】${title}:${body}`); return true; }}
// marketing.ts:营销活动(增长组维护)class MarketingSmsMessage { // 营销活动 × 短信 send(to: string, title: string, body: string): void { title = `[活动] ${title}`; // 营销活动:标题加前缀,正文带退订说明,不重试 this.deliver(to, title, `${body}(可在设置中退订)`); }
private deliver(to: string, title: string, body: string): boolean { // 和 UrgentSmsMessage 里的一模一样 console.log(`[短信] ${to}:【多练】${title}:${body}`); return true; }}
class MarketingEmailMessage { // 营销活动 × 邮件 send(to: string, title: string, body: string): void { title = `[活动] ${title}`; // 和 MarketingSmsMessage 里的一模一样 this.deliver(to, title, `${body}(可在设置中退订)`); }
private deliver(to: string, title: string, body: string): boolean { // 邮件的发送细节:签名放落款 console.log(`[邮件] ${to}:主题「${title}」 ${body} —— 多练`); return true; }}
new NoticeAppMessage().send("user_42", "课程更新", "第 14 章已上线");new UrgentSmsMessage().send("138****0000", "支付服务不可用", "支付失败率 35%");new MarketingSmsMessage().send("139****1111", "国庆五折", "全部课程五折");
// 输出:// [App 推送] user_42:多练 · 课程更新 | 第 14 章已上线// [短信] 138****0000:【多练】[紧急] 支付服务不可用:支付失败率 35%// [短信] 139****1111:【多练】[活动] 国庆五折:全部课程五折(可在设置中退订)// [邮件] [email protected]:主题「[活动] 国庆五折」 全部课程五折(可在设置中退订) —— 多练每个类一目了然。大多数人第一版都这么写。
14.3 需求变了
Section titled “14.3 需求变了”新需求是:加一种“验证码”消息(标题是“登录验证码”这样,正文写明有效期,失败重试一次);加一个“企业微信”渠道,给内部同事发。
要改的地方:
- 验证码:新建 verify_code.py,写
VerifyCodeAppMessage、VerifyCodeSmsMessage、VerifyCodeEmailMessage。_deliver从别的类里抄渠道代码,验证码的规则也抄三遍。 - 企业微信:在四个文件里各加一个类,
NoticeWeComMessage、UrgentWeComMessage、MarketingWeComMessage、VerifyCodeWeComMessage。企业微信的发送代码一模一样,写四遍。 - 类从 9 个变成 16 个,每条渠道规则的副本也跟着变多,短信代码从 3 份变 4 份。再加一个渠道是 20 个类。
第 3 处的代价上个月就见识过。短信服务商通知:签名必须和备案名一致,【多练】要改成【多练网】,下月起不合规的短信直接拦截。产品顺带要求 App 推送和邮件的签名也统一成“多练网”。签名在每个类的 _deliver 里,9 处要改。同事改了 notice.py 和 urgent.py 里的 6 个类:
print(f"[短信] {to}:【多练网】{title}:{body}") # 原来是【多练】marketing.py 归增长组,放在另一个目录,他没搜到。把 before.py 前两个类的签名照此改成“多练网”、两个营销类不动,运行结果:
[App 推送] user_42:多练网 · 课程更新 | 第 14 章已上线[短信] 138****0000:【多练网】[紧急] 支付服务不可用:支付失败率 35%[短信] 139****1111:【多练】[活动] 国庆五折:全部课程五折(可在设置中退订)[邮件] [email protected]:主题「[活动] 国庆五折」 全部课程五折(可在设置中退订) —— 多练测试只发了通知和告警,都对。这个月 1 日起,营销短信全被拦下,国庆活动的短信一条也没发出去。
问题的形状是:类型和渠道用继承乘在一起,类的数量是两边的乘积;同一条渠道规则复制在每种类型的类里,改一次要找遍所有副本。
黄色的 ① 是验证码一行,② 是企业微信一列。红色的 ③ 是签名事故:通知、告警两行的 6 个类改了,边框加粗的营销三个类漏了。
14.4 用桥接模式重写
Section titled “14.4 用桥接模式重写”把两个维度拆成两套类。渠道只管“怎么送到”,包括自己的签名,接口是 deliver(to, title, body);消息只管“发什么、发几次”,手里拿着一个渠道对象。组合在创建时决定:UrgentMessage(SmsChannel())。
from typing import Protocol
# 实现部分:渠道只管"怎么送到",包括各自的签名格式class Channel(Protocol): def deliver(self, to: str, title: str, body: str) -> bool: ...
class AppChannel: def deliver(self, to: str, title: str, body: str) -> bool: print(f"[App 推送] {to}:多练网 · {title} | {body}") return True # 模拟:总是成功
class SmsChannel: def __init__(self, fail_times: int = 0) -> None: self.fail_times = fail_times # 模拟:前几次网关超时
def deliver(self, to: str, title: str, body: str) -> bool: if self.fail_times > 0: self.fail_times -= 1 print(f"[短信] {to}:网关超时") return False print(f"[短信] {to}:【多练网】{title}:{body}") return True
class EmailChannel: def deliver(self, to: str, title: str, body: str) -> bool: print(f"[邮件] {to}:主题「{title}」 {body} —— 多练网") return True
class WeComChannel: # 新渠道:企业微信 def deliver(self, to: str, title: str, body: str) -> bool: print(f"[企业微信] {to}:{title} | {body}(多练网)") return True
# 抽象部分:消息管"发什么、发几次",手里拿着一个渠道;基类给出默认规则,子类覆盖class Message: max_attempts = 1
def __init__(self, channel: Channel) -> None: self.channel = channel # 这就是"桥"
def send(self, to: str, title: str, body: str) -> None: for _ in range(self.max_attempts): if self.channel.deliver(to, self.format_title(title), self.format_body(body)): return
def format_title(self, title: str) -> str: return title
def format_body(self, body: str) -> str: return body
class NoticeMessage(Message): pass
class UrgentMessage(Message): max_attempts = 3
def format_title(self, title: str) -> str: return f"[紧急] {title}"
class MarketingMessage(Message): def format_title(self, title: str) -> str: return f"[活动] {title}"
def format_body(self, body: str) -> str: return f"{body}(可在设置中退订)"
class VerifyCodeMessage(Message): # 新类型:验证码 max_attempts = 2
def format_title(self, title: str) -> str: return f"{title}验证码"
def format_body(self, body: str) -> str: return f"{body},5 分钟内有效,请勿告诉他人"
if __name__ == "__main__": NoticeMessage(AppChannel()).send("user_42", "课程更新", "第 14 章已上线") UrgentMessage(SmsChannel(fail_times=1)).send("138****0000", "支付服务不可用", "支付失败率 35%") MarketingMessage(SmsChannel()).send("139****1111", "国庆五折", "全部课程五折") VerifyCodeMessage(SmsChannel()).send("139****1111", "登录", "382916") UrgentMessage(WeComChannel()).send("zhangsan", "支付服务不可用", "支付失败率 35%")
# 输出:# [App 推送] user_42:多练网 · 课程更新 | 第 14 章已上线# [短信] 138****0000:网关超时# [短信] 138****0000:【多练网】[紧急] 支付服务不可用:支付失败率 35%# [短信] 139****1111:【多练网】[活动] 国庆五折:全部课程五折(可在设置中退订)# [邮件] [email protected]:主题「[活动] 国庆五折」 全部课程五折(可在设置中退订) —— 多练网# [短信] 139****1111:【多练网】登录验证码:382916,5 分钟内有效,请勿告诉他人# [企业微信] zhangsan:[紧急] 支付服务不可用 | 支付失败率 35%(多练网)TS 版一样:
// 实现部分:渠道只管"怎么送到",包括各自的签名格式interface Channel { deliver(to: string, title: string, body: string): boolean;}
class AppChannel implements Channel { deliver(to: string, title: string, body: string): boolean { console.log(`[App 推送] ${to}:多练网 · ${title} | ${body}`); return true; // 模拟:总是成功 }}
class SmsChannel implements Channel { constructor(private failTimes = 0) {} // 模拟:前几次网关超时
deliver(to: string, title: string, body: string): boolean { if (this.failTimes > 0) { this.failTimes--; console.log(`[短信] ${to}:网关超时`); return false; } console.log(`[短信] ${to}:【多练网】${title}:${body}`); return true; }}
class EmailChannel implements Channel { deliver(to: string, title: string, body: string): boolean { console.log(`[邮件] ${to}:主题「${title}」 ${body} —— 多练网`); return true; }}
class WeComChannel implements Channel { // 新渠道:企业微信 deliver(to: string, title: string, body: string): boolean { console.log(`[企业微信] ${to}:${title} | ${body}(多练网)`); return true; }}
// 抽象部分:消息管"发什么、发几次",手里拿着一个渠道;基类给出默认规则,子类覆盖class Message { protected maxAttempts = 1;
constructor(protected readonly channel: Channel) {} // 这就是"桥"
send(to: string, title: string, body: string): void { for (let i = 0; i < this.maxAttempts; i++) { if (this.channel.deliver(to, this.formatTitle(title), this.formatBody(body))) return; } }
protected formatTitle(title: string): string { return title; }
protected formatBody(body: string): string { return body; }}
class NoticeMessage extends Message {}
class UrgentMessage extends Message { protected maxAttempts = 3;
protected formatTitle(title: string): string { return `[紧急] ${title}`; }}
class MarketingMessage extends Message { protected formatTitle(title: string): string { return `[活动] ${title}`; }
protected formatBody(body: string): string { return `${body}(可在设置中退订)`; }}
class VerifyCodeMessage extends Message { // 新类型:验证码 protected maxAttempts = 2;
protected formatTitle(title: string): string { return `${title}验证码`; }
protected formatBody(body: string): string { return `${body},5 分钟内有效,请勿告诉他人`; }}
new NoticeMessage(new AppChannel()).send("user_42", "课程更新", "第 14 章已上线");new UrgentMessage(new SmsChannel(1)).send("138****0000", "支付服务不可用", "支付失败率 35%");new MarketingMessage(new SmsChannel()).send("139****1111", "国庆五折", "全部课程五折");new VerifyCodeMessage(new SmsChannel()).send("139****1111", "登录", "382916");new UrgentMessage(new WeComChannel()).send("zhangsan", "支付服务不可用", "支付失败率 35%");
// 输出:// [App 推送] user_42:多练网 · 课程更新 | 第 14 章已上线// [短信] 138****0000:网关超时// [短信] 138****0000:【多练网】[紧急] 支付服务不可用:支付失败率 35%// [短信] 139****1111:【多练网】[活动] 国庆五折:全部课程五折(可在设置中退订)// [邮件] [email protected]:主题「[活动] 国庆五折」 全部课程五折(可在设置中退订) —— 多练网// [短信] 139****1111:【多练网】登录验证码:382916,5 分钟内有效,请勿告诉他人// [企业微信] zhangsan:[紧急] 支付服务不可用 | 支付失败率 35%(多练网)两套类之间只有一条线。标红的两个新类各只写自己那一边。
标题和重试次数由 UrgentMessage 定,SmsChannel 只管加签名、发出去;第一次超时,消息再调它一次。
对照 14.3:
- 第 1 处,验证码:加一个
VerifyCodeMessage,配哪个渠道都能用。 - 第 2 处,企业微信:加一个
WeComChannel,四种消息都能用。 - 第 3 处,类的数量:从乘积变成相加,4 种类型加 4 个渠道是 8 个类。签名只在 4 个渠道类里各写一次,营销消息的类里根本没有签名,也就漏不掉。
也有不省事的地方。第一,按用户设置挑渠道的那段代码,仍得登记 WeComChannel。第二,拆之前要定好每个决定归哪一边:重试看消息重不重要,归消息;签名是渠道的规矩,归渠道。放错边就出事:嫌短信网关老超时,把“试 3 次”写进 SmsChannel,走 App 的紧急告警就不再重试,营销短信反倒试 3 次,网关只是回包慢时,用户会收到三条一样的广告。第三,有的规则同时取决于两边,比如营销短信要写“回复 TD 退订”,邮件要放退订链接,这时 Channel 接口得加一个方法,四个渠道类都要补上。
14.5 模式定义
Section titled “14.5 模式定义”GoF 对桥接的定义是:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
这里的“抽象”和“实现”不是抽象类和实现类:抽象部分是调用方直接用的那层(消息),实现部分是底下干活的那层(渠道)。两层各有继承体系,中间的对象引用就是“桥”。
对照案例图:Message 是 Abstraction,Channel 是 Implementor。
| 角色 | 在本章案例里是谁 | 职责 |
|---|---|---|
| 抽象(Abstraction) | Message |
定义调用方用的接口(send),持有一个实现部分的对象 |
| 扩充抽象(RefinedAbstraction) | UrgentMessage、MarketingMessage 等 |
在抽象上加各自的规则:标题、重试、退订 |
| 实现者(Implementor) | Channel |
实现部分的接口,通常比抽象部分的接口更底层(deliver) |
| 具体实现者(ConcreteImplementor) | SmsChannel、WeComChannel 等 |
对接一个具体的平台 |
常见场景:
- 跨平台:UI 控件(按钮、窗口)× 操作系统,控件逻辑只写一份,画图交给各平台。
- 驱动:业务接口 × 数据库,代码面对同一套接口,底下换 MySQL 或 PostgreSQL 的驱动。
- 渲染:图形(圆、矩形)× 渲染后端(SVG、Canvas),14.9 的练习就是这一类。
桥接就是第 1 章“组合优于继承”用在两个维度上,1.5 的 Report(period=Daily(), exporter=PdfExporter()) 就是一座小桥。
14.6 真实系统里的例子
Section titled “14.6 真实系统里的例子”例一:logging 的 Logger 和 Handler(Python)
Section titled “例一:logging 的 Logger 和 Handler(Python)”Python 标准库的 logging 分两层。Logger 是代码里打日志的那一层:按文档的说法,它判断级别,组装出一条 LogRecord,过一遍过滤器,再交给挂在它上面的每个 Handler(还会顺着父 Logger 往上传)。Handler 有自己的级别、过滤器和 Formatter,决定写成什么样、写到哪里:StreamHandler 写控制台,FileHandler 写文件,logging.handlers 里还有发网络、发邮件的。简化版:
import sysfrom collections.abc import Callablefrom dataclasses import dataclassfrom typing import Protocol, TextIO
DEBUG, INFO = 10, 20 # 和 logging.DEBUG、logging.INFO 的数值相同
@dataclassclass LogRecord: # 真实的 logging.LogRecord 还带时间、文件名、行号、线程等 name: str levelname: str msg: str
class Handler(Protocol): # 实现部分,对应 logging.Handler def emit(self, record: LogRecord) -> None: ...
class StreamHandler: # 对应 logging.StreamHandler:写到一个流(控制台);FileHandler 是它的子类 def __init__(self, stream: TextIO) -> None: self.stream = stream
def emit(self, record: LogRecord) -> None: self.stream.write(f"{record.levelname}:{record.name}:{record.msg}\n")
class BufferingHandler: # 对应 logging.handlers.BufferingHandler:先攒在内存里 def __init__(self) -> None: self.buffer: list[LogRecord] = []
def emit(self, record: LogRecord) -> None: self.buffer.append(record)
class Logger: # 抽象部分,对应 logging.Logger:管级别、过滤、组装记录,不管写到哪 def __init__(self, name: str, level: int) -> None: self.name, self.level = name, level self.filters: list[Callable[[LogRecord], bool]] = [] self.handlers: list[Handler] = []
def log(self, level: int, msg: str, *args: object) -> None: if level < self.level: return # 级别不够,连记录都不组装 # 简化:这里当场拼好消息;真实的 LogRecord 把 msg 和 args 分开存,到 getMessage() 才拼 record = LogRecord(self.name, "DEBUG" if level == DEBUG else "INFO", msg % args) if all(f(record) for f in self.filters): for h in self.handlers: h.emit(record) # 真实的 Logger 调 handler.handle():先过 Handler 自己的过滤,再加锁调 emit()
def info(self, msg: str, *args: object) -> None: self.log(INFO, msg, *args)
if __name__ == "__main__": log = Logger("pay", INFO) log.filters.append(lambda r: "password" not in r.msg) memory = BufferingHandler() log.handlers += [StreamHandler(sys.stdout), memory] # 换、加 Handler,Logger 一行不改 log.log(DEBUG, "连接池大小 %d", 8) log.info("订单 %s 支付成功", "A1001") log.info("password=%s", "123456") print(f"内存里攒了 {len(memory.buffer)} 条:{memory.buffer[0].msg}")
# 输出:# INFO:pay:订单 A1001 支付成功# 内存里攒了 1 条:订单 A1001 支付成功Logger 对应 Message,Handler 对应 Channel。debug 那条和带密码那条都在 Logger 这层被拦下,Handler 根本没见过。
例二:React 的协调器和渲染器(TypeScript)
Section titled “例二:React 的协调器和渲染器(TypeScript)”React 大致也是这么分的。协调器(Reconciler)调组件拿到元素树,对比新旧两棵树,决定建哪些节点、挂到哪里;真正建节点的是渲染器:react-dom 建 DOM 节点,react-native 建原生视图。两者之间约定的一组方法,react-reconciler 包叫它 host config,里面有 createInstance、appendChild 等。简化版只留建节点和挂孩子,同一棵元素树交给两个渲染器:
// 元素树:真实 React 里是 JSX 编译出来的对象,带 props、key 等type El = string | { type: string; children: El[] };
// 实现部分:对应 react-reconciler 的 host config,每个渲染器(react-dom、react-native)各写一份// 真实的方法更多、参数更多:createInstance、createTextInstance、appendInitialChild、commitUpdate……interface Renderer<I> { createInstance(type: string): I; createTextInstance(text: string): I; appendChild(parent: I, child: I): void;}
// 抽象部分:对应协调器(Reconciler),只决定"建什么、挂到哪",不知道最后画在哪里// 真实的协调器还要对比新旧两棵树,只把变了的部分交给渲染器function render<I>(el: El, r: Renderer<I>): I { if (typeof el === "string") return r.createTextInstance(el); const instance = r.createInstance(el.type); for (const child of el.children) r.appendChild(instance, render(child, r)); return instance;}
class HtmlNode { // 假装是浏览器的 DOM 节点 readonly children: HtmlNode[] = []; constructor(readonly tag: string | null, readonly text = "") {} toString(): string { return this.tag === null ? this.text : `<${this.tag}>${this.children.join("")}</${this.tag}>`; }}
const domRenderer: Renderer<HtmlNode> = { // 像 react-dom:变成 DOM 节点 createInstance: (type) => new HtmlNode(type), createTextInstance: (text) => new HtmlNode(null, text), appendChild: (parent, child) => { parent.children.push(child); },};
const textRenderer: Renderer<string[]> = { // 像终端里的渲染器:变成缩进的文本行 createInstance: (type) => [type], createTextInstance: (text) => [text], appendChild: (parent, child) => { parent.push(...child.map((line) => ` ${line}`)); },};
const toc: El = { type: "ul", children: [ { type: "li", children: ["第 14 章 桥接"] }, { type: "li", children: ["第 15 章 享元"] },] };console.log(render(toc, domRenderer).toString());console.log(render(toc, textRenderer).join("\n"));
// 输出:// <ul><li>第 14 章 桥接</li><li>第 15 章 享元</li></ul>// ul// li// 第 14 章 桥接// li// 第 15 章 享元render 是抽象部分,Renderer 是实现者接口,两个渲染器是具体实现者。render 里没有一行和 HTML 有关。
14.7 什么时候别用
Section titled “14.7 什么时候别用”- 只有一个维度在变。消息只走短信,类型一直在加,普通继承或第 17 章的策略就够了。硬拆出只有一个实现的
Channel接口,读代码的人要多跳一层才看到短信怎么发。 - 两个维度不是“用”而是“是”。“VIP 用户”是用户的一种,不是用户手里的零件。一个等级枚举字段加一张折扣表就够;拆成
User持有Tier对象,每加一条按等级区分的规则,都要在Tier接口和每个等级类里各加一个方法。 - 项目早期,第二个维度还没出现。只有短信时提前拆桥,接口按短信设计;企业微信真来了,多半发现
deliver(to, title, body)不合用(它要卡片、按钮),接口照样得改,消息类连带要改。等第二个渠道来了再拆,接口按两个真实的样子设计。
14.8 容易混淆的模式
Section titled “14.8 容易混淆的模式”| 模式 | 意图 | 区别 |
|---|---|---|
| 适配器(Adapter) | 让一个接口对不上的类按调用方要的接口工作 | 适配器是事后补救:两边都写好了,接不上,中间加一层转换。桥接不为对接而转换:抽象和实现两侧的接口都是事先设计的,见第 9 章 |
| 策略(Strategy) | 把一族可互换的算法各自封装 | 结构几乎一样,都持有一个接口对象;但策略换的是一个算法(怎么算运费),桥接的实现部分是一整套平台能力,而且抽象部分自己也有一棵继承树。见第 17 章 |
14.9 练一练
Section titled “14.9 练一练”图表 Chart 有折线图 LineChart 和柱状图 BarChart,渲染后端 Renderer 有 SvgRenderer(输出 SVG 字符串)和 AsciiRenderer(输出字符画)。用桥接拆开,同一组数据 [3, 5, 2, 4],两种图各用两种后端画一次。Python 和 TS 各写一版。
提示:先想 Renderer 要提供哪几个最基本的动作(开始画布、画一根柱子、画一串点),图表只用这些动作;字符画里折线只画点也行。SVG 的纵轴朝下,要翻过来。
14.10 小结
Section titled “14.10 小结”- 桥接把两个各自会变的维度拆成两套类,一边持有另一边的对象,组合在创建时决定。
- 类的数量从乘积变成相加;一条渠道规则只写在一个渠道类里,不会散在每种消息里等着漏改。
- 拆之前要定好每个决定归哪一边;同时取决于两边的规则要扩展实现者接口,所有实现都得跟着改。
- 和适配器的分别:桥接是事先设计的拆分,适配器是事后的补救。和策略的分别:策略换一个算法,桥接换一整套底层能力。