4、渠道适配器模式:适配器设计模式在分发中的应用
说到报价分发,我第一个想到的就是适配器模式。
为什么?因为真实世界里的渠道,简直五花八门。邮件、API、消息队列、短信、Webhook……每个渠道都有自己的脾气。你想想看,如果每次新增一个渠道,就要改一遍核心分发逻辑,那代码迟早变成一团乱麻。
适配器模式,说白了就是给每个渠道配一个「翻译官」。核心引擎只跟翻译官说话,翻译官再去跟具体渠道打交道。这样,核心逻辑稳如泰山,渠道扩展随心所欲。
4.1 适配器模式的核心思想
适配器模式不是什么新鲜概念。它解决的核心问题就一个:让不兼容的接口能协同工作。
在报价分发场景里,我们的核心引擎期望一个统一的调用方式:
- 传入报价数据
- 指定目标渠道
- 等待发送结果
但每个渠道的SDK、协议、参数格式都不一样。这时候,适配器就派上用场了。
核心原则:面向接口编程,而不是面向实现编程。引擎只依赖抽象接口,不依赖具体渠道。
我个人习惯把适配器分成三层:
- 接口层:定义统一的发送契约
- 适配器层:实现接口,封装渠道差异
- 渠道层:实际的第三方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方法。整个过程干净利落。
我画了一张图,帮你理解整个适配器模式在分发中的结构:
从图上你能看到,核心引擎只跟统一接口打交道。具体怎么发、发给谁,全由适配器搞定。这就是适配器模式的精髓——解耦。
4.7 适配器模式的设计要点
最后,我总结几个设计要点,都是实战中摸爬滚打出来的:
- 接口要稳定:一旦发布,尽量不改。新增字段用optional,不要破坏现有实现。
- 异常要统一:所有适配器返回的异常,都要转换成统一的错误码和错误信息。前端和监控只看这一层。
- 配置要外置:每个适配器的配置(超时、重试、限流)都放到配置文件里,不要硬编码。
- 监控要独立:每个适配器单独上报指标(发送量、成功率、耗时),方便定位问题。
一句话总结:适配器模式让报价分发引擎变得「开放扩展、封闭修改」。新增渠道?写个适配器,注册进去,完事。核心逻辑,动都不动。
嗯,这就是适配器模式在报价分发中的应用。说白了,就是给每个渠道配一个「翻译官」,让核心引擎只跟翻译官说话。简单,但极其有效。
公众号:蓝海资料掘金营,微信deep3321