4、渠道适配器模式:适配器设计模式在分发中的应用

说到报价分发,我第一个想到的就是适配器模式。

为什么?因为真实世界里的渠道,简直五花八门。邮件、API、消息队列、短信、Webhook……每个渠道都有自己的脾气。你想想看,如果每次新增一个渠道,就要改一遍核心分发逻辑,那代码迟早变成一团乱麻。

适配器模式,说白了就是给每个渠道配一个「翻译官」。核心引擎只跟翻译官说话,翻译官再去跟具体渠道打交道。这样,核心逻辑稳如泰山,渠道扩展随心所欲。

4.1 适配器模式的核心思想

适配器模式不是什么新鲜概念。它解决的核心问题就一个:让不兼容的接口能协同工作

在报价分发场景里,我们的核心引擎期望一个统一的调用方式:

  • 传入报价数据
  • 指定目标渠道
  • 等待发送结果

但每个渠道的SDK、协议、参数格式都不一样。这时候,适配器就派上用场了。

核心原则:面向接口编程,而不是面向实现编程。引擎只依赖抽象接口,不依赖具体渠道。

我个人习惯把适配器分成三层:

  1. 接口层:定义统一的发送契约
  2. 适配器层:实现接口,封装渠道差异
  3. 渠道层:实际的第三方SDK或协议

嗯,这里要注意:适配器不是简单的「包装」。它要处理协议转换、参数映射、异常翻译、重试策略……这些脏活累活,全得扛下来。

4.2 统一接口规范

先看接口定义。我一般这样设计:

public interface ChannelAdapter {
    // 发送报价
    SendResult send(QuoteRequest request);
    
    // 查询发送状态
    SendStatus queryStatus(String messageId);
    
    // 获取渠道类型
    ChannelType getChannelType();
}

这个接口很薄,但够用。为什么?因为报价分发不需要太复杂的交互。核心就是「发出去」和「查状态」。

QuoteRequest 里包含什么?我列一下:

字段 说明 示例
quoteId 报价单号 Q20241001-001
recipient 接收方标识 邮箱/手机号/队列名
content 报价内容体 JSON/HTML/纯文本
priority 优先级 HIGH/MEDIUM/LOW
metadata 扩展属性 Map<String, String>

SendResult 也很简单:

public class SendResult {
    private boolean success;
    private String messageId;
    private String errorCode;
    private String errorMessage;
    private long costMs;
}

统一接口的好处,我在项目里体会太深了。有一次要紧急接入一个新的即时通讯渠道,团队里一个小伙子花了半天就搞定了——写个适配器,实现三个方法,完事。核心引擎一行代码没改。

4.3 邮件渠道适配器实现

邮件渠道,看起来简单,坑其实不少。

先看代码骨架:

public class EmailAdapter implements ChannelAdapter {
    
    private final JavaMailSender mailSender;
    private final EmailConfig config;
    
    @Override
    public SendResult send(QuoteRequest request) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            
            helper.setTo(request.getRecipient());
            helper.setSubject("报价单:" + request.getQuoteId());
            helper.setText(request.getContent(), true); // 支持HTML
            
            mailSender.send(message);
            
            return SendResult.success(generateMessageId());
        } catch (Exception e) {
            return SendResult.fail("EMAIL_SEND_ERROR", e.getMessage());
        }
    }
    
    @Override
    public SendStatus queryStatus(String messageId) {
        // 邮件协议本身不支持状态查询
        // 这里只能返回已发送状态
        return SendStatus.SENT;
    }
    
    @Override
    public ChannelType getChannelType() {
        return ChannelType.EMAIL;
    }
}

我曾经踩过一个坑:邮件发送是同步阻塞的,如果SMTP服务器响应慢,整个线程就卡住了。后来我加了一个异步发送的包装,用线程池隔离,才算解决。

避坑指南:邮件适配器要注意附件大小、收件人数量限制、以及邮件内容编码。我曾经因为附件超过25MB,被邮件服务器直接拒收,导致报价单没发出去,客户投诉了一整天。

4.4 API渠道适配器实现

API渠道,通常是调用第三方REST接口。比如企业微信、钉钉、Slack的Webhook。

public class ApiAdapter implements ChannelAdapter {
    
    private final RestTemplate restTemplate;
    private final ApiConfig config;
    
    @Override
    public SendResult send(QuoteRequest request) {
        try {
            // 构建请求体
            ApiRequest apiRequest = new ApiRequest();
            apiRequest.setMsgType("quote");
            apiRequest.setContent(request.getContent());
            apiRequest.setTimestamp(System.currentTimeMillis());
            
            // 签名
            String sign = signRequest(apiRequest, config.getSecret());
            apiRequest.setSign(sign);
            
            // 发送
            ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
                config.getWebhookUrl(),
                apiRequest,
                ApiResponse.class
            );
            
            if (response.getStatusCode().is2xxSuccessful()) {
                return SendResult.success(response.getBody().getMessageId());
            } else {
                return SendResult.fail("API_HTTP_ERROR", 
                    "HTTP " + response.getStatusCodeValue());
            }
        } catch (Exception e) {
            return SendResult.fail("API_NETWORK_ERROR", e.getMessage());
        }
    }
    
    @Override
    public SendStatus queryStatus(String messageId) {
        // 调用API查询消息状态
        String url = config.getQueryUrl() + "/" + messageId;
        ApiStatusResponse resp = restTemplate.getForObject(url, ApiStatusResponse.class);
        return mapStatus(resp.getStatus());
    }
    
    @Override
    public ChannelType getChannelType() {
        return ChannelType.API;
    }
    
    private String signRequest(ApiRequest request, String secret) {
        // 签名逻辑,略
        return "signature";
    }
}

API渠道的适配器,我建议重点关注三点:

  • 超时设置:连接超时、读取超时都要单独配置
  • 重试策略:网络抖动是常态,指数退避重试是标配
  • 限流保护:第三方API通常有调用频率限制

我记得有一次,某个渠道的API突然变慢,从50ms飙到5秒。幸好适配器里配了超时和熔断,不然整个分发引擎都被拖垮了。

4.5 消息队列渠道适配器实现

消息队列渠道,适合高吞吐、异步的场景。比如把报价单扔到RabbitMQ或Kafka里,让下游慢慢消费。

public class MqAdapter implements ChannelAdapter {
    
    private final RabbitTemplate rabbitTemplate;
    private final MqConfig config;
    
    @Override
    public SendResult send(QuoteRequest request) {
        try {
            // 构建消息
            Message message = MessageBuilder
                .withBody(request.getContent().getBytes(StandardCharsets.UTF_8))
                .setHeader("quoteId", request.getQuoteId())
                .setHeader("priority", request.getPriority().name())
                .setHeader("timestamp", System.currentTimeMillis())
                .build();
            
            // 发送到指定队列
            CorrelationData correlationData = new CorrelationData(request.getQuoteId());
            rabbitTemplate.send(
                config.getExchange(),
                config.getRoutingKey(),
                message,
                correlationData
            );
            
            return SendResult.success(correlationData.getId());
        } catch (Exception e) {
            return SendResult.fail("MQ_SEND_ERROR", e.getMessage());
        }
    }
    
    @Override
    public SendStatus queryStatus(String messageId) {
        // MQ本身不提供消息状态查询
        // 需要结合业务数据库判断
        return SendStatus.UNKNOWN;
    }
    
    @Override
    public ChannelType getChannelType() {
        return ChannelType.MESSAGE_QUEUE;
    }
}

消息队列适配器有个特点:发送成功不代表消费成功。所以我在实际项目中,会额外加一个「消费确认」的回调机制。消息被下游成功处理后,会回调一个确认接口,更新状态。

小技巧:消息队列适配器里,建议把消息ID和报价单号做关联。这样排查问题时,能快速定位到具体是哪条消息出了问题。

4.6 适配器注册与动态路由

有了适配器,怎么用?我一般会搞一个适配器注册中心:

public class ChannelAdapterRegistry {
    
    private final Map<ChannelType, ChannelAdapter> adapterMap = new ConcurrentHashMap<>();
    
    public void register(ChannelAdapter adapter) {
        adapterMap.put(adapter.getChannelType(), adapter);
    }
    
    public ChannelAdapter getAdapter(ChannelType type) {
        ChannelAdapter adapter = adapterMap.get(type);
        if (adapter == null) {
            throw new IllegalArgumentException("不支持的渠道类型:" + type);
        }
        return adapter;
    }
}

分发引擎拿到报价单后,根据配置的渠道类型,从注册中心取出对应的适配器,直接调用send方法。整个过程干净利落。

我画了一张图,帮你理解整个适配器模式在分发中的结构:

报价分发引擎 - 适配器模式架构 报价分发核心引擎 ChannelAdapter 接口 邮件适配器 EmailAdapter API适配器 ApiAdapter 消息队列适配器 MqAdapter SMTP / IMAP 邮件服务器 REST / Webhook 第三方API AMQP / Kafka 消息中间件 核心引擎 统一接口 适配器实现 具体渠道

从图上你能看到,核心引擎只跟统一接口打交道。具体怎么发、发给谁,全由适配器搞定。这就是适配器模式的精髓——解耦

4.7 适配器模式的设计要点

最后,我总结几个设计要点,都是实战中摸爬滚打出来的:

  • 接口要稳定:一旦发布,尽量不改。新增字段用optional,不要破坏现有实现。
  • 异常要统一:所有适配器返回的异常,都要转换成统一的错误码和错误信息。前端和监控只看这一层。
  • 配置要外置:每个适配器的配置(超时、重试、限流)都放到配置文件里,不要硬编码。
  • 监控要独立:每个适配器单独上报指标(发送量、成功率、耗时),方便定位问题。

一句话总结:适配器模式让报价分发引擎变得「开放扩展、封闭修改」。新增渠道?写个适配器,注册进去,完事。核心逻辑,动都不动。

嗯,这就是适配器模式在报价分发中的应用。说白了,就是给每个渠道配一个「翻译官」,让核心引擎只跟翻译官说话。简单,但极其有效。


公众号:蓝海资料掘金营,微信deep3321