3. 协议适配设计模式:适配器、桥接、策略
协议适配这件事,说白了就是让两个「语言不通」的系统能正常对话。我做了这么多年协议开发,最深的体会就是:没有万能的设计模式,只有最合适的组合拳。
今天咱们就聊聊三种最常用的模式——适配器、桥接、策略。它们各自解决什么问题?在协议适配中怎么用?我踩过哪些坑?
3.1 适配器模式:让不兼容的接口「握手」
适配器模式,我习惯叫它「转接头」。你想想看,Type-C 转 HDMI,不就是把两种不同规格的接口对接起来吗?
在协议适配里,场景更具体:客户端期望调用 A 接口,但服务端只提供 B 接口。适配器就在中间做一层转换。
核心思想:通过一个中间层,把目标接口转换成客户端期望的接口。
3.1.1 经典结构
适配器模式分两种:类适配器(继承)和对象适配器(组合)。我个人更推荐对象适配器,因为组合比继承灵活得多。
// 目标接口:客户端期望的协议
public interface IProtocolClient {
void send(String data);
String receive();
}
// 适配者:已有的第三方协议库
public class ThirdPartyProtocol {
public void transmit(byte[] rawData) { /* ... */ }
public byte[] fetch() { /* ... */ }
}
// 适配器:把第三方协议包装成目标接口
public class ProtocolAdapter implements IProtocolClient {
private ThirdPartyProtocol adaptee;
public ProtocolAdapter(ThirdPartyProtocol adaptee) {
this.adaptee = adaptee;
}
@Override
public void send(String data) {
byte[] raw = data.getBytes(StandardCharsets.UTF_8);
adaptee.transmit(raw); // 转换调用
}
@Override
public String receive() {
byte[] raw = adaptee.fetch();
return new String(raw, StandardCharsets.UTF_8);
}
}
你看,客户端完全不知道底层是 ThirdPartyProtocol,它只跟 IProtocolClient 打交道。这就是适配器的价值——解耦。
3.1.2 我在项目中遇到的坑
我曾经接手过一个遗留系统,对接的是某厂商的私有协议。那协议报文格式奇葩得很,字段顺序跟标准完全不搭边。我一开始想直接改厂商的库,结果发现改了会影响其他模块。
后来我用适配器模式,在中间做了一层报文重组。嗯,这里要注意:适配器不是万能的。如果两个协议的语义差异太大(比如一个用 JSON,一个用二进制流),适配器会变得臃肿不堪。这时候就该考虑桥接模式了。
我的建议:适配器适合「接口形态不同但语义相近」的场景。如果语义都不同,别硬套适配器。
3.2 桥接模式:把抽象和实现分离
桥接模式,说白了就是把「做什么」和「怎么做」分开。在协议适配里,抽象层定义协议的行为(比如「发送」「接收」「解析」),实现层负责具体的编码解码逻辑。
为什么要这么做?因为协议族经常变。你今天对接的是 HTTP,明天可能就要支持 MQTT,后天又来一个自定义 TCP 协议。如果抽象和实现绑死,每次新增协议都得改一大片代码。
3.2.1 桥接模式的结构
桥接模式的核心是两个独立变化的维度:
- 抽象化角色:定义协议的高层操作(如 connect、send、close)
- 实现化角色:定义底层的传输细节(如 TCP 连接、序列化方式)
// 实现化接口:底层传输方式
public interface ITransport {
void open(String endpoint);
void write(byte[] data);
byte[] read();
void close();
}
// 具体实现:TCP 传输
public class TcpTransport implements ITransport {
private Socket socket;
@Override
public void open(String endpoint) { /* 建立 TCP 连接 */ }
@Override
public void write(byte[] data) { /* 发送数据 */ }
@Override
public byte[] read() { /* 接收数据 */ }
@Override
public void close() { /* 关闭连接 */ }
}
// 抽象化:协议客户端
public abstract class ProtocolClient {
protected ITransport transport;
public ProtocolClient(ITransport transport) {
this.transport = transport;
}
public abstract void sendMessage(String msg);
public abstract String receiveMessage();
}
// 具体抽象:JSON 协议客户端
public class JsonProtocolClient extends ProtocolClient {
public JsonProtocolClient(ITransport transport) {
super(transport);
}
@Override
public void sendMessage(String msg) {
String json = "{\"data\":\"" + msg + "\"}";
transport.write(json.getBytes());
}
@Override
public String receiveMessage() {
byte[] raw = transport.read();
return new String(raw);
}
}
你看,现在我可以自由组合:new JsonProtocolClient(new TcpTransport()),或者换成 UDP 传输、Protobuf 序列化,都不需要改抽象层。
3.2.2 桥接 vs 适配器
很多初学者搞混这两个模式。我画个简单的对比:
| 维度 | 适配器模式 | 桥接模式 |
|---|---|---|
| 目的 | 让不兼容的接口能协作 | 让抽象和实现独立变化 |
| 变化点 | 接口形态不同 | 抽象层和实现层各自变化 |
| 使用时机 | 已有系统,接口不匹配 | 设计阶段,预见到多维度变化 |
| 典型场景 | 对接第三方 SDK | 多协议、多传输方式组合 |
注意:桥接模式会增加类的数量。如果你的协议只有一种实现,别为了用模式而用模式。我见过有人硬套桥接,结果抽象层和实现层一一对应,纯属浪费。
3.3 策略模式:把算法「拎」出来
策略模式,我理解就是把「怎么做」的细节封装成独立的策略类。在协议适配里,最常见的场景就是序列化/反序列化策略。
你想想看,同一个协议,可能今天用 JSON,明天用 XML,后天换成 Protobuf。如果序列化逻辑散落在各个地方,改起来得疯掉。
3.3.1 策略模式的结构
策略模式的核心就三样:策略接口、具体策略、上下文。
// 策略接口:序列化策略
public interface ISerializationStrategy {
byte[] serialize(Object obj);
<T> T deserialize(byte[] data, Class<T> clazz);
}
// 具体策略:JSON 序列化
public class JsonSerialization implements ISerializationStrategy {
private ObjectMapper mapper = new ObjectMapper();
@Override
public byte[] serialize(Object obj) {
return mapper.writeValueAsBytes(obj);
}
@Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
return mapper.readValue(data, clazz);
}
}
// 具体策略:Protobuf 序列化
public class ProtobufSerialization implements ISerializationStrategy {
// 假设使用 Protobuf 3
@Override
public byte[] serialize(Object obj) {
// 实际需要 MessageLite 类型
return ((MessageLite) obj).toByteArray();
}
@Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
// 通过反射调用 parseFrom
// ...
}
}
// 上下文:协议处理器
public class ProtocolProcessor {
private ISerializationStrategy strategy;
public ProtocolProcessor(ISerializationStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(ISerializationStrategy strategy) {
this.strategy = strategy;
}
public void process(byte[] rawData) {
// 使用当前策略反序列化
MyMessage msg = strategy.deserialize(rawData, MyMessage.class);
// 处理业务逻辑...
}
}
你看,策略模式让算法可以随时切换。我可以在运行时根据配置或协商结果动态选择策略。
3.3.2 策略模式 + 工厂模式 = 绝配
实际项目中,我不会让客户端直接 new 策略对象。我会配合工厂模式,根据协议类型自动选择策略。
public class SerializationFactory {
private static final Map<String, ISerializationStrategy> strategies = new HashMap<>();
static {
strategies.put("json", new JsonSerialization());
strategies.put("protobuf", new ProtobufSerialization());
strategies.put("xml", new XmlSerialization());
}
public static ISerializationStrategy getStrategy(String type) {
ISerializationStrategy s = strategies.get(type.toLowerCase());
if (s == null) {
throw new IllegalArgumentException("不支持的序列化类型: " + type);
}
return s;
}
}
这样,客户端只需要传一个字符串(比如从协议头里解析出来的 Content-Type),就能拿到对应的策略。
避坑指南:我曾经在策略里放了太多状态,导致多线程环境下出问题。记住:策略对象最好是无状态的,或者用 ThreadLocal 隔离。
3.4 三种模式的组合使用
实际项目中,这三种模式经常一起出现。我画了一张图,展示它们如何协作:
这张图展示了一个典型的组合场景:
- 桥接模式把协议分为抽象层和实现层,两者独立变化
- 适配器模式在中间层对接第三方协议库
- 策略模式在最底层封装序列化/编码算法
这样做的好处是:任何一个维度变化,都不影响其他层。比如换序列化方式,只改策略层;换传输协议,只改桥接实现层。
我的经验:在大型协议适配项目中,这三种模式几乎必用。我建议你在设计阶段就画好分层图,明确每个模式负责的边界。别等到代码写了一半才想起来重构,那代价太大了。
3.5 小结
三种模式各有侧重:
- 适配器解决「接口不匹配」的问题,适合对接已有系统
- 桥接解决「多维度变化」的问题,适合设计可扩展的协议框架
- 策略解决「算法可替换」的问题,适合封装序列化、校验等逻辑
记住,设计模式是工具,不是目的。别为了用模式而用模式。我见过最糟糕的代码,就是把所有模式堆在一起,结果一个简单的协议适配搞出了十几个类,维护起来痛不欲生。