上一篇讲了外观模式。
它解决的问题是:
复杂子系统不应该直接裸露给调用方,而应该通过一个统一、高层、易用的入口来提供服务。
比如诊断子系统、OTA 子系统、仿真平台、数据采集系统,都可以通过一个 Facade 把复杂流程收起来,让业务层不必直接面对一堆底层模块。
但真实工程里,还有另一类结构问题。
这类问题不是"子系统太复杂",也不是"接口不兼容",而是:
一个对象有两个变化方向,而且这两个方向会分别变化。
比如车企软件里很常见的场景:
- 诊断业务会变化,底层通信方式也会变化
- 控制逻辑会变化,硬件平台也会变化
- OTA 升级流程会变化,数据传输通道也会变化
- 数据采集任务会变化,存储和上报方式也会变化
- HMI 业务组件会变化,渲染平台也会变化
- 仿真场景类型会变化,仿真引擎实现也会变化
这时如果还靠继承一层层扩展,很容易出现类数量膨胀。
比如:
text
CanDiagnosticService
DoipDiagnosticService
SerialDiagnosticService
SimulationDiagnosticService
CanSecureDiagnosticService
DoipSecureDiagnosticService
SerialSecureDiagnosticService
SimulationSecureDiagnosticService
CanFlashDiagnosticService
DoipFlashDiagnosticService
SerialFlashDiagnosticService
SimulationFlashDiagnosticService
你会发现,类名越来越长,继承树越来越乱。
真正的问题不是"类不够多"。
而是:
抽象和实现被绑在了一棵继承树里,导致两个变化方向互相拖累。
桥接模式,就是为了解决这个问题。
一、先从一个车企场景说起
假设我们现在要做一个诊断服务。
业务层希望使用这样的能力:
cpp
ReadDid()
WriteDid()
ReadDtc()
ClearDtc()
也就是说,上层关心的是诊断业务动作。
但底层通信方式可能有很多种:
- CAN
- DoIP
- 串口
- 仿真通道
- 远程诊断通道
如果一开始系统很简单,只有 CAN,你可能会写:
cpp
class CanDiagnosticService {
public:
std::vector<uint8_t> ReadDid(uint16_t did);
bool WriteDid(uint16_t did, const std::vector<uint8_t>& data);
};
后来支持 DoIP,于是又写:
cpp
class DoipDiagnosticService {
public:
std::vector<uint8_t> ReadDid(uint16_t did);
bool WriteDid(uint16_t did, const std::vector<uint8_t>& data);
};
再后来支持串口,又写一个:
cpp
class SerialDiagnosticService {
public:
std::vector<uint8_t> ReadDid(uint16_t did);
bool WriteDid(uint16_t did, const std::vector<uint8_t>& data);
};
这时你会发现,很多诊断业务逻辑其实是重复的。
比如读 DID 的请求格式可能都是:
text
0x22 + DID
写 DID 的请求格式可能都是:
text
0x2E + DID + Data
区别只是底层怎么发出去。
CAN 有 CAN 的发送方式。
DoIP 有 DoIP 的发送方式。
串口有串口的发送方式。
仿真通道有仿真通道的发送方式。
如果你把"诊断业务"和"通信实现"写死在一个类里,就会出现一个问题:
每新增一种诊断业务,要在每种通信实现里改一遍。
每新增一种通信实现,又要把所有诊断业务复制一遍。
这就是两个变化方向被绑死之后的典型后果。
二、为什么直接继承会出问题?
很多人一开始会觉得:
那就继承啊。
基类写公共逻辑,子类实现不同通信方式,不就行了吗?
简单场景下确实可以。
但当变化方向不止一个时,继承很快会变得笨重。
1. 类数量会按组合膨胀
假设诊断服务有三种业务类型:
- 普通诊断
- 安全诊断
- 刷写诊断
底层通信有四种实现:
- CAN
- DoIP
- 串口
- 仿真
如果用继承硬组合,可能会变成:
text
NormalCanDiagnosticService
NormalDoipDiagnosticService
NormalSerialDiagnosticService
NormalSimulationDiagnosticService
SecureCanDiagnosticService
SecureDoipDiagnosticService
SecureSerialDiagnosticService
SecureSimulationDiagnosticService
FlashCanDiagnosticService
FlashDoipDiagnosticService
FlashSerialDiagnosticService
FlashSimulationDiagnosticService
这还只是 3 × 4。
如果再加一个"远程诊断通道",或者再加一个"售后诊断服务",类数量还会继续增长。
这不是设计变复杂了。
这是变化维度被错误地揉在了一起。
2. 抽象层被实现细节污染
诊断服务本来应该关心的是:
- 读 DID
- 写 DID
- 读 DTC
- 清 DTC
- 切会话
- 安全访问
但如果它和 CAN、DoIP、串口这些实现细节绑在一起,业务类里就会不断出现底层细节:
cpp
if (useCan) {
// 按 CAN 帧发送
} else if (useDoip) {
// 按 DoIP 报文发送
} else if (useSerial) {
// 按串口协议发送
}
这段代码的问题不在于 if-else 本身。
真正的问题是:
诊断业务层开始关心通信实现细节了。
这会让抽象层变脏。
3. 新增实现时,抽象层也被迫变化
假设系统已经支持 CAN 和 DoIP。
后来项目要求支持"仿真通道"。
如果诊断业务和通信实现没有分离,你可能要改很多类:
- 普通诊断类
- 安全诊断类
- 刷写诊断类
- 测试诊断类
- 售后诊断类
但从业务语义上看,新增仿真通道并不应该影响诊断业务本身。
这说明原来的结构把不该耦合的东西耦合在一起了。
4. 两个变化方向的生命周期不同
诊断业务的变化,通常来自需求。
比如:
- 新增一个 DID 读取流程
- 新增安全访问策略
- 新增刷写前检查
- 新增响应校验逻辑
通信实现的变化,通常来自平台和环境。
比如:
- 从 CAN 切到 DoIP
- 从实车切到仿真
- 从本地通道切到远程通道
- 从供应商 A SDK 切到供应商 B SDK
这两个变化方向不一定同时发生。
如果它们被绑在同一棵继承树里,那么任何一边变化,都可能牵动另一边。
这就是桥接模式要解决的根本问题。
三、桥接模式到底是什么?
桥接模式可以这样理解:
把抽象部分和实现部分分离,使它们可以独立变化。
再说得直白一点:
不要把两个变化方向硬塞进同一棵继承树里,而是把其中一个变化方向抽成接口,让另一个方向通过组合去使用它。
比如诊断服务这个例子里,有两个变化方向:
text
变化方向一:诊断业务抽象
变化方向二:通信实现方式
桥接之后,结构就会变成:
text
DiagnosticService
↓ 持有
ITransport
↑
CanTransport / DoipTransport / SerialTransport
诊断服务不再关心底层到底是 CAN 还是 DoIP。
它只关心:
我有一个能 Send 和 Receive 的通信实现。
底层通信实现也不关心上面到底是普通诊断、安全诊断还是刷写诊断。
它只负责:
把数据按自己的方式发出去,再把响应收回来。
这就是桥接。
这里的"桥"不是一个具体的桥类。
它更像一种结构关系:
抽象层通过组合,桥接到实现层。
四、桥接模式解决的核心问题
桥接模式最核心的价值有三个。
1. 让抽象和实现分别变化
抽象层可以扩展自己的业务能力。
比如:
text
DiagnosticService
SecureDiagnosticService
FlashDiagnosticService
实现层也可以扩展自己的底层实现。
比如:
text
CanTransport
DoipTransport
SerialTransport
SimulationTransport
它们之间通过一个稳定接口连接。
这样新增一种业务抽象,不需要复制所有通信实现。
新增一种通信实现,也不需要改所有业务抽象。
2. 避免继承层次爆炸
桥接模式不是不用继承。
而是不把多个变化维度都压进继承里。
它更常见的做法是:
一边用继承表达抽象层扩展;
另一边用接口和组合表达实现层变化。
这样结构会更平。
类数量也更可控。
3. 让依赖关系更清楚
没有桥接时,代码可能是:
text
诊断业务类 直接依赖 CAN / DoIP / Serial 细节
有了桥接之后,代码变成:
text
诊断业务类 依赖 ITransport 抽象
ITransport 由不同通信实现提供
这就让边界更清楚:
- 诊断业务负责业务流程
- 通信实现负责数据传输
- 两者通过接口协作
工程里很多复杂性,都是因为边界不清楚。
桥接模式就是帮你把边界重新切开。
五、桥接模式的核心角色
桥接模式通常有几个角色。
1. Abstraction:抽象
也就是高层业务抽象。
比如:
text
DiagnosticService
ControlService
OtaService
DataCollectionTask
HmiWidget
它定义的是调用方真正关心的业务能力。
2. RefinedAbstraction:扩展抽象
也就是抽象层的进一步扩展。
比如:
text
SecureDiagnosticService
FlashDiagnosticService
RemoteDiagnosticService
它们可以在高层业务上继续扩展,而不需要关心底层实现细节。
3. Implementor:实现接口
也就是底层实现的统一接口。
比如:
text
ITransport
IHardwareDriver
IStorageBackend
IRenderBackend
IUpgradeChannel
它不是业务接口,而是抽象层需要依赖的实现能力。
4. ConcreteImplementor:具体实现
也就是真正完成底层工作的类。
比如:
text
CanTransport
DoipTransport
SerialTransport
SimulationTransport
或者:
text
BoschBrakeDriver
ContinentalBrakeDriver
MockBrakeDriver
它们实现同一个 Implementor 接口。
5. Client:调用方
调用方使用 Abstraction。
它通常不需要直接关心 ConcreteImplementor。
当然,在对象创建阶段,系统可能会通过工厂或配置来决定具体使用哪个实现。
六、桥接模式的结构
可以用一个简化结构理解:
text
Client
↓
Abstraction
↓ 持有
Implementor
↑
ConcreteImplementorA
ConcreteImplementorB
ConcreteImplementorC
用诊断例子表示就是:
text
业务层
↓
DiagnosticService
↓ 持有 ITransport
CanTransport / DoipTransport / SimulationTransport
用 UML 简化表示:
#mermaid-svg-ScEAWOEESQNPD2fL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ScEAWOEESQNPD2fL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ScEAWOEESQNPD2fL .error-icon{fill:#552222;}#mermaid-svg-ScEAWOEESQNPD2fL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ScEAWOEESQNPD2fL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ScEAWOEESQNPD2fL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ScEAWOEESQNPD2fL .marker.cross{stroke:#333333;}#mermaid-svg-ScEAWOEESQNPD2fL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ScEAWOEESQNPD2fL p{margin:0;}#mermaid-svg-ScEAWOEESQNPD2fL g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-ScEAWOEESQNPD2fL g.classGroup text .title{font-weight:bolder;}#mermaid-svg-ScEAWOEESQNPD2fL .cluster-label text{fill:#333;}#mermaid-svg-ScEAWOEESQNPD2fL .cluster-label span{color:#333;}#mermaid-svg-ScEAWOEESQNPD2fL .cluster-label span p{background-color:transparent;}#mermaid-svg-ScEAWOEESQNPD2fL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ScEAWOEESQNPD2fL .cluster text{fill:#333;}#mermaid-svg-ScEAWOEESQNPD2fL .cluster span{color:#333;}#mermaid-svg-ScEAWOEESQNPD2fL .nodeLabel,#mermaid-svg-ScEAWOEESQNPD2fL .edgeLabel{color:#131300;}#mermaid-svg-ScEAWOEESQNPD2fL .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-ScEAWOEESQNPD2fL .label text{fill:#131300;}#mermaid-svg-ScEAWOEESQNPD2fL .labelBkg{background:#ECECFF;}#mermaid-svg-ScEAWOEESQNPD2fL .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-ScEAWOEESQNPD2fL .classTitle{font-weight:bolder;}#mermaid-svg-ScEAWOEESQNPD2fL .node rect,#mermaid-svg-ScEAWOEESQNPD2fL .node circle,#mermaid-svg-ScEAWOEESQNPD2fL .node ellipse,#mermaid-svg-ScEAWOEESQNPD2fL .node polygon,#mermaid-svg-ScEAWOEESQNPD2fL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ScEAWOEESQNPD2fL .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL g.clickable{cursor:pointer;}#mermaid-svg-ScEAWOEESQNPD2fL g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ScEAWOEESQNPD2fL g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-ScEAWOEESQNPD2fL .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-ScEAWOEESQNPD2fL .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ScEAWOEESQNPD2fL .dashed-line{stroke-dasharray:3;}#mermaid-svg-ScEAWOEESQNPD2fL .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-ScEAWOEESQNPD2fL #compositionStart,#mermaid-svg-ScEAWOEESQNPD2fL .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #compositionEnd,#mermaid-svg-ScEAWOEESQNPD2fL .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #dependencyStart,#mermaid-svg-ScEAWOEESQNPD2fL .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #dependencyStart,#mermaid-svg-ScEAWOEESQNPD2fL .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #extensionStart,#mermaid-svg-ScEAWOEESQNPD2fL .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #extensionEnd,#mermaid-svg-ScEAWOEESQNPD2fL .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #aggregationStart,#mermaid-svg-ScEAWOEESQNPD2fL .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #aggregationEnd,#mermaid-svg-ScEAWOEESQNPD2fL .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #lollipopStart,#mermaid-svg-ScEAWOEESQNPD2fL .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL #lollipopEnd,#mermaid-svg-ScEAWOEESQNPD2fL .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ScEAWOEESQNPD2fL .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-ScEAWOEESQNPD2fL .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ScEAWOEESQNPD2fL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ScEAWOEESQNPD2fL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ScEAWOEESQNPD2fL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} bridge
DiagnosticService
-transport: ITransport
+ReadDid(did)
+WriteDid(did, data)
SecureDiagnosticService
+ReadSecureDid(did)
ITransport
+Connect()
+Send(data)
+Receive()
CanTransport
+Connect()
+Send(data)
+Receive()
DoipTransport
+Connect()
+Send(data)
+Receive()
这张图里最关键的是:
DiagnosticService不继承CanTransport,也不直接写死 DoIP。它只是持有一个
ITransport。
这就是桥接模式的核心。
七、一个 C++ 示例:诊断业务和通信实现解耦
先定义底层通信实现接口。
cpp
#include <cstdint>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class ITransport {
public:
virtual ~ITransport() = default;
virtual bool Connect() = 0;
virtual bool Send(const std::vector<uint8_t>& data) = 0;
virtual std::vector<uint8_t> Receive() = 0;
};
然后定义几个具体通信实现。
cpp
class CanTransport : public ITransport {
public:
bool Connect() override {
std::cout << "connect by CAN\n";
return true;
}
bool Send(const std::vector<uint8_t>& data) override {
std::cout << "send CAN frame, size=" << data.size() << "\n";
return true;
}
std::vector<uint8_t> Receive() override {
std::cout << "receive CAN response\n";
return {0x62, 0xF1, 0x90, 'V', 'I', 'N'};
}
};
class DoipTransport : public ITransport {
public:
bool Connect() override {
std::cout << "connect by DoIP\n";
return true;
}
bool Send(const std::vector<uint8_t>& data) override {
std::cout << "send DoIP payload, size=" << data.size() << "\n";
return true;
}
std::vector<uint8_t> Receive() override {
std::cout << "receive DoIP response\n";
return {0x62, 0xF1, 0x90, 'V', 'I', 'N'};
}
};
接下来定义高层诊断抽象。
cpp
class DiagnosticService {
public:
explicit DiagnosticService(std::unique_ptr<ITransport> transport)
: m_transport(std::move(transport)) {}
virtual ~DiagnosticService() = default;
bool Connect() {
return m_transport->Connect();
}
virtual std::vector<uint8_t> ReadDid(uint16_t did) {
std::vector<uint8_t> request = {
0x22,
static_cast<uint8_t>((did >> 8) & 0xFF),
static_cast<uint8_t>(did & 0xFF)
};
if (!m_transport->Send(request)) {
return {};
}
return m_transport->Receive();
}
virtual bool WriteDid(uint16_t did, const std::vector<uint8_t>& data) {
std::vector<uint8_t> request = {
0x2E,
static_cast<uint8_t>((did >> 8) & 0xFF),
static_cast<uint8_t>(did & 0xFF)
};
request.insert(request.end(), data.begin(), data.end());
return m_transport->Send(request);
}
protected:
ITransport& Transport() {
return *m_transport;
}
private:
std::unique_ptr<ITransport> m_transport;
};
这里 DiagnosticService 只关心诊断请求怎么组。
它不关心请求是通过 CAN 发出去,还是通过 DoIP 发出去。
底层实现通过 ITransport 被桥接进来。
如果要扩展一种安全诊断服务,可以这样写:
cpp
class SecureDiagnosticService : public DiagnosticService {
public:
explicit SecureDiagnosticService(std::unique_ptr<ITransport> transport)
: DiagnosticService(std::move(transport)) {}
bool UnlockSecurity(uint8_t level) {
std::vector<uint8_t> request = {0x27, level};
if (!Transport().Send(request)) {
return false;
}
auto response = Transport().Receive();
return !response.empty() && response[0] == 0x67;
}
std::vector<uint8_t> ReadSecureDid(uint16_t did) {
if (!UnlockSecurity(0x01)) {
std::cout << "security unlock failed\n";
return {};
}
return ReadDid(did);
}
};
调用方可以这样使用:
cpp
int main() {
{
std::unique_ptr<ITransport> transport =
std::make_unique<CanTransport>();
DiagnosticService service(std::move(transport));
service.Connect();
auto vin = service.ReadDid(0xF190);
}
{
std::unique_ptr<ITransport> transport =
std::make_unique<DoipTransport>();
SecureDiagnosticService service(std::move(transport));
service.Connect();
auto vin = service.ReadSecureDid(0xF190);
}
return 0;
}
你会发现:
- 普通诊断服务可以搭配 CAN
- 普通诊断服务也可以搭配 DoIP
- 安全诊断服务可以搭配 CAN
- 安全诊断服务也可以搭配 DoIP
但我们不需要写:
text
CanDiagnosticService
DoipDiagnosticService
CanSecureDiagnosticService
DoipSecureDiagnosticService
这就是桥接模式带来的结构收益。
八、再看一个更贴近工程的例子:控制逻辑和硬件平台解耦
桥接模式在车企软件里很常见。
比如制动控制。
上层可能有不同控制逻辑:
text
NormalBrakeController
SportBrakeController
EnergyRecoveryBrakeController
底层可能有不同硬件平台:
text
BoschBrakeHardware
ContinentalBrakeHardware
MockBrakeHardware
如果直接继承组合,很容易变成:
text
NormalBoschBrakeController
NormalContinentalBrakeController
SportBoschBrakeController
SportContinentalBrakeController
EnergyRecoveryBoschBrakeController
EnergyRecoveryContinentalBrakeController
类数量一下就膨胀。
更合理的做法是:
text
BrakeController
↓ 持有
IBrakeHardware
↑
BoschBrakeHardware / ContinentalBrakeHardware / MockBrakeHardware
这样控制逻辑和硬件实现就可以分别变化。
新增一个控制策略,不需要复制所有硬件适配代码。
新增一个硬件平台,也不需要重写所有控制逻辑。
这就是桥接模式在工程里的实际意义:
把两个独立变化的维度拆开,让它们通过接口组合在一起。
九、这个例子到底好在哪里?
1. 诊断业务不再绑定通信实现
DiagnosticService 只负责诊断业务。
它知道怎么组请求、怎么解释响应、什么时候需要安全访问。
但它不知道底层是 CAN 还是 DoIP。
这让诊断业务逻辑更干净。
2. 通信实现可以独立扩展
如果未来新增一个仿真通道,只需要实现:
cpp
class SimulationTransport : public ITransport {
// ...
};
原来的 DiagnosticService 不需要改。
如果未来新增一个远程诊断通道,也一样。
3. 两个方向可以自由组合
你可以用:
text
普通诊断 + CAN
普通诊断 + DoIP
安全诊断 + CAN
安全诊断 + DoIP
刷写诊断 + DoIP
刷写诊断 + 仿真通道
组合关系可以在创建阶段决定。
这比提前写死一堆子类更灵活。
4. 更适合测试
桥接之后,你可以写一个测试用的实现:
cpp
class MockTransport : public ITransport {
// ...
};
然后用它测试诊断业务逻辑。
这样测试 DiagnosticService 时,不需要真的连 CAN 卡,也不需要真的连 ECU。
这对单元测试非常有价值。
十、车企项目里,哪些地方适合桥接模式?
1. 诊断业务与通信通道解耦
比如:
text
DiagnosticService
↓
ITransport
↑
CanTransport / DoipTransport / SerialTransport / SimulationTransport
诊断业务关注 UDS 流程。
通信实现关注底层收发。
两者适合桥接。
2. 控制逻辑与硬件平台解耦
比如:
text
BrakeController
↓
IBrakeHardware
↑
BoschHardware / ContinentalHardware / MockHardware
控制逻辑可能随着业务策略变化。
硬件平台可能随着车型、供应商、项目变化。
这两个维度不应该互相污染。
3. OTA 流程与传输通道解耦
OTA 业务可能包括:
- 检查版本
- 校验包
- 进入升级模式
- 分片传输
- 校验刷写结果
- 失败回滚
但传输通道可能是:
- 车端本地文件
- TBOX 下载
- 以太网传输
- 诊断刷写通道
- 仿真测试通道
OTA 流程和传输实现也适合桥接。
4. 数据采集任务与存储后端解耦
数据采集任务关心:
- 采哪些信号
- 采样频率是多少
- 什么时候开始
- 什么时候停止
- 是否过滤异常值
存储后端可能是:
- 本地文件
- SQLite
- 云端服务
- Kafka
- 测试桩
这也是典型的两个变化维度。
5. HMI 业务组件与渲染平台解耦
HMI 业务组件可能是:
- 车速表
- 电量显示
- 告警弹窗
- 导航卡片
渲染平台可能是:
- Qt
- Android
- Web
- 自研渲染引擎
如果业务组件和渲染实现写死在一起,跨平台会非常痛苦。
桥接模式可以让业务组件依赖渲染接口,而不是直接依赖某个平台。
十一、桥接模式和适配器模式有什么区别?
这两个模式都像是在"接一层"。
但它们的意图完全不同。
适配器模式
适配器解决的是:
已有类的接口和当前系统需要的接口不兼容,怎么接进来。
它通常是事后补救。
比如老系统已经有一个 LegacyCanChannel,但新系统需要 ITransport。
于是写一个 LegacyCanAdapter。
重点是:
把旧接口转换成新接口。
桥接模式
桥接解决的是:
抽象和实现有两个变化方向,怎么让它们分别变化。
它通常是设计阶段就主动拆分。
比如诊断业务和通信实现都可能变化。
于是让 DiagnosticService 持有 ITransport。
重点是:
把两个变化维度拆开。
一句话区分
适配器:接口不兼容,所以接一下。
桥接:两个维度都会变,所以拆开来。
如果你面对的是一个已经存在的老接口,那更像适配器。
如果你面对的是两个会长期独立变化的维度,那更像桥接。
十二、桥接模式和装饰器模式有什么区别?
装饰器模式也常用组合。
桥接模式也常用组合。
但它们解决的问题不同。
装饰器模式
装饰器关注的是:
不改原对象,动态增加额外能力。
比如:
text
LoggingTransportDecorator
RetryTransportDecorator
MetricsTransportDecorator
它的重点是能力增强。
桥接模式
桥接关注的是:
抽象层和实现层不要绑死,让它们分别变化。
比如:
text
DiagnosticService
↓
ITransport
↑
CanTransport / DoipTransport
它的重点是维度拆分。
一句话区分
装饰器:在原能力外面叠加能力。
桥接:把两个变化方向拆成两套结构。
如果你是在给对象加日志、重试、统计,通常是装饰器。
如果你是在拆"业务抽象"和"底层实现",通常是桥接。
十三、桥接模式和策略模式有什么区别?
桥接模式和策略模式也很像。
因为它们都经常表现为:
text
一个类持有一个接口
运行时传入不同实现
但关注点不一样。
策略模式
策略模式解决的是:
一个行为有多种算法或策略,运行时可以替换。
比如:
- 能量回收策略
- 路径规划策略
- 告警处理策略
- 排序策略
- 校验策略
它关注的是某个行为怎么做。
桥接模式
桥接模式解决的是:
抽象和实现是两个独立变化的层次,需要解耦。
比如:
- 诊断业务 vs 通信实现
- 控制逻辑 vs 硬件平台
- 数据采集任务 vs 存储后端
- HMI 组件 vs 渲染引擎
它关注的是结构层次怎么拆。
一句话区分
策略模式更像"换算法"。
桥接模式更像"拆维度"。
如果只是一个方法有多种算法,优先考虑策略模式。
如果是两个对象层级都可能独立扩展,桥接模式更合适。
十四、桥接模式最容易被滥用在哪里?
桥接模式很好,但也容易被写复杂。
1. 只有一个变化方向,也强行桥接
如果你的系统里只有一种实现,而且短期内也不会有第二种实现,就没必要提前抽一层。
比如只有一个固定通信通道,还强行写:
text
ITransport
AbstractTransport
ConcreteTransport
TransportBridge
这就不是设计,是加戏。
设计模式不是越早用越好。
它应该用在真实复杂性出现的地方。
2. Implementor 接口设计得太贴某个实现
桥接的关键是实现接口要稳定。
如果你设计的 ITransport 长这样:
cpp
virtual void SendCanFrame(int canId, const uint8_t* data, int len) = 0;
那它其实已经偏向 CAN 了。
未来 DoIP、串口、仿真通道接进来都会很别扭。
好的 Implementor 接口应该表达抽象层真正需要的底层能力,而不是某个具体实现的细节。
3. 把桥接层写成万能中转层
有些代码会把桥接接口越写越大。
比如一个 ITransport 里塞满:
text
SendCanFrame()
SendDoipPacket()
OpenSerialPort()
StartSimulation()
UploadToCloud()
这就失去了桥接的意义。
接口应该稳定、清晰、聚焦。
如果一个接口什么都想表达,最后它就什么都表达不好。
4. 抽象层和实现层边界没切对
桥接最难的不是写代码。
而是判断:
哪些属于抽象层?
哪些属于实现层?
比如诊断服务里:
- 组 UDS 请求,属于诊断业务
- 通过 CAN 还是 DoIP 发送,属于通信实现
- 安全访问流程,通常属于诊断业务
- TCP 建链细节,属于 DoIP 实现
边界切错之后,桥接结构就会变得很别扭。
5. 对象创建阶段混乱
桥接之后,对象通常需要组合起来。
比如:
text
DiagnosticService + CanTransport
SecureDiagnosticService + DoipTransport
如果这些组合关系到处手写,也会乱。
所以桥接模式经常会和工厂、配置中心、依赖注入一起使用。
桥接负责结构解耦。
工厂负责对象组装。
这两个不是冲突关系。
十五、工程中更推荐的用法
1. 先识别两个变化维度
使用桥接前,先问:
这里是不是真的有两个独立变化方向?
比如:
text
业务类型会变
底层实现也会变
如果答案是肯定的,再考虑桥接。
不要看到组合和接口就觉得是桥接。
桥接的核心是"两个维度分别变化"。
2. 抽象层按业务语义设计
抽象层要面向调用方。
比如诊断服务应该暴露:
text
ReadDid()
WriteDid()
ReadDtc()
ClearDtc()
而不是暴露一堆底层发送细节。
抽象层越贴近业务,调用方越舒服。
3. 实现层按稳定能力设计
实现层接口要表达底层公共能力。
比如通信实现可以抽象成:
text
Connect()
Send()
Receive()
不要把某个具体协议的细节直接写进公共接口。
否则桥接接口会变得很难复用。
4. 优先组合,而不是多层继承
桥接模式的核心就是组合。
cpp
class DiagnosticService {
private:
std::unique_ptr<ITransport> m_transport;
};
它不是通过继承 CanTransport 获得能力。
而是通过持有 ITransport 来使用能力。
这也是"组合优于继承"的一个典型落地。
5. 创建逻辑可以交给工厂
桥接本身不负责决定用哪个实现。
比如到底用 CAN 还是 DoIP,可能来自:
- 配置文件
- 车型平台
- 运行环境
- 测试参数
- 工厂方法
- 依赖注入容器
这部分不要硬塞进业务类。
可以交给工厂或配置层。
6. 测试时分别测试两个维度
桥接之后,测试也应该更清楚。
你可以分别测试:
- 诊断业务是否正确组包
- CAN 通信实现是否正确发送
- DoIP 通信实现是否正确发送
- 诊断业务能否配合 MockTransport 工作
这比一上来测试一堆组合类更容易定位问题。
十六、桥接模式的优缺点
优点
第一,抽象和实现解耦。
业务层不再直接绑定底层实现。
第二,两个方向可以独立扩展。
新增业务抽象和新增底层实现,不需要互相复制。
第三,避免继承层次爆炸。
多个变化维度不再被硬压进一棵继承树。
第四,更容易测试。
抽象层可以配合 Mock 实现测试。
实现层也可以单独测试。
第五,更适合平台化代码。
车企软件里经常有多车型、多平台、多供应商、多环境,桥接模式很适合处理这类变化。
缺点
第一,结构会比直接写类更复杂。
多了一层接口和组合关系,新人一开始可能不容易理解。
第二,接口边界设计要求更高。
如果抽象层和实现层切分不合理,代码会更别扭。
第三,对象创建会更复杂。
需要在创建阶段决定抽象和实现如何组合。
第四,不适合简单场景。
如果只有一个实现、一个业务类型,强行桥接只会增加负担。
十七、使用桥接模式前,先问这 6 个问题
1. 这里是否真的有两个变化方向?
比如业务会变,底层实现也会变。
如果只有一个方向会变,桥接可能不是最合适的模式。
2. 这两个变化方向是否应该独立演进?
如果新增一个底层实现,不应该影响业务抽象。
如果新增一个业务抽象,也不应该复制底层实现。
这时桥接才有价值。
3. 现在的继承结构是否已经开始膨胀?
如果你已经看到大量组合类名,比如:
text
A1B1
A1B2
A2B1
A2B2
那就要警惕了。
这很可能说明你把两个维度塞进了一棵继承树。
4. 抽象层是否能稳定表达业务语义?
桥接不是为了抽接口而抽接口。
抽象层应该让调用方觉得自然。
比如:
cpp
diagService.ReadDid(0xF190);
就比:
cpp
diagService.SendRawBytes(...);
更符合诊断业务语义。
5. 实现层接口是否足够通用?
实现层接口应该能覆盖不同实现。
如果接口明显偏向某个具体实现,后面接其他实现时就会很痛苦。
6. 创建和组合关系是否有明确归属?
桥接后,对象需要被组装。
谁来决定:
text
SecureDiagnosticService + DoipTransport
这个问题要想清楚。
不要让组合逻辑散落在业务代码各处。
十八、总结
桥接模式解决的是:
抽象和实现都有可能变化时,如何让它们分别变化,而不是互相绑死。
它不是为了让代码多一层接口。
它真正想解决的是:
- 两个变化维度被揉在一起
- 继承层次随着组合数量膨胀
- 抽象层被底层实现细节污染
- 新增实现时需要修改大量业务类
- 新增业务时需要复制大量底层实现代码
- 平台化、多车型、多供应商场景下扩展困难
一句话概括:
桥接模式的重点不是"多写一个接口",而是"把两个独立变化的维度拆开"。
在车企软件里,它很适合这些场景:
- 诊断业务与通信通道解耦
- 控制逻辑与硬件平台解耦
- OTA 流程与传输通道解耦
- 数据采集任务与存储后端解耦
- HMI 业务组件与渲染平台解耦
- 仿真业务流程与仿真引擎解耦
但它也不适合所有场景。
如果系统只有一个实现维度,或者变化点还没有出现,就不要为了"像设计模式"而提前桥接。
设计模式真正有价值的地方,不是名字背得熟,而是:
你能不能看见变化的方向,并让代码结构顺着变化方向生长。
如果上一篇外观模式提醒我们:
不要把"明明应该由系统边界统一承担的复杂流程",丢给每一个业务调用方自己去拼。
那么这一篇桥接模式提醒我们:
不要把"明明会分别变化的抽象和实现",硬绑在同一棵继承树里。
如果这篇对你有帮助,欢迎点赞、转发、关注。
我们下一篇继续拆设计模式。