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 三种模式的组合使用

实际项目中,这三种模式经常一起出现。我画了一张图,展示它们如何协作:

协议适配设计模式组合架构图 协议适配设计模式组合架构 客户端 桥接:抽象协议层 适配器:接口转换层 桥接:传输实现层 策略:序列化/编码 第三方协议库 各层独立变化 通过组合实现灵活适配

这张图展示了一个典型的组合场景:

  1. 桥接模式把协议分为抽象层和实现层,两者独立变化
  2. 适配器模式在中间层对接第三方协议库
  3. 策略模式在最底层封装序列化/编码算法

这样做的好处是:任何一个维度变化,都不影响其他层。比如换序列化方式,只改策略层;换传输协议,只改桥接实现层。

我的经验:在大型协议适配项目中,这三种模式几乎必用。我建议你在设计阶段就画好分层图,明确每个模式负责的边界。别等到代码写了一半才想起来重构,那代价太大了。

3.5 小结

三种模式各有侧重:

  • 适配器解决「接口不匹配」的问题,适合对接已有系统
  • 桥接解决「多维度变化」的问题,适合设计可扩展的协议框架
  • 策略解决「算法可替换」的问题,适合封装序列化、校验等逻辑

记住,设计模式是工具,不是目的。别为了用模式而用模式。我见过最糟糕的代码,就是把所有模式堆在一起,结果一个简单的协议适配搞出了十几个类,维护起来痛不欲生。