定义
适配器模式的主要作用是把原本不兼容的接口通过适配修改做到统一,方便调用方使用。
就像日常生活中用到的万能充电器、数据线和笔记本的转换接头,它们都为适配各种不同的接口进行了兼容。
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。(用于项目重构?)
实践
在业务开发中,经常需要做不同接口的兼容,尤其是中台服务。中台需要把各个业务线的类型服务统一包装,再对外提供接口。
MQ消息体兼容场景
随着公司业务的不断扩展,基础架构系统逐步成型,业务运营就需要开始做新用户的拉新和老用户的促活,从而保障DAU的增速,以及最终实现ROI转换。
这时就需要做一些营销系统,常见的有裂变、拉客,如邀请一位用户开户,或者邀请一位用户下单,平台就会返利,并且多邀多得。同时,随着拉新量的增多,平台开始设置每月首单返现等奖励。
开发这样一个营销系统就会遇到各种各样的MQ消息或接口,如果逐个开发,会耗费很高的成本,同时后期的扩展也有一定的难度。此时会希望有一个系统,配置后就能把外部的MQ接入,这些MQ就像上面提到的注册开户消息、商品下单消息等。而适配器的思想也恰恰可以运用在这里。需要强调的是,适配器不只可以适配接口,还可以适配一些属性信息。
这里列出三种MQ信息
代码语言:javascript复制@Data
public class CreateAccount {
private String number;
private String address;
private Date accountDate;
private String desc;
}
@Data
public class OrderMq {
private String uid;
private String sku;
private String orderId;
private Date createOrderTime;
}
@Data
public class POPOrderDelivered {
private String uId;
private String orderId;
private Date orderTime;
private String sku;
private String skuName;
private BigDecimal decimal;
}
查询用户内部下单数量接口
代码语言:javascript复制@Slf4j
public class OrderService {
public long queryUserOrderCount(String userId){
log.info("自营商家,查询用户的订单是否为首单:{}", userId);
return 10L;
}
}
查询用户第三方下单首单接口
代码语言:javascript复制@Slf4j
public class POPOrderService {
public boolean isFirstOrder(String uId) {
log.info("POP商家,查询用户的订单是否为首单:{}", uId);
return true;
}
}
接下来使用适配器进行实现
为了满足产品功能的需求,提取此项功能中必须的字段信息,单独创建一个类RebateInfo。
代码语言:javascript复制@Data
public class RebateInfo {
private String userId;
private String bizId;
private String bizTime;
private String desc;
}
MQ消息统一适配类。
代码语言:javascript复制public class MQAdapter {
public static RebateInfo filter(String strJson, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return filter(JSON.parseObject(strJson, Map.class), link);
}
public static RebateInfo filter(Map obj, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
RebateInfo rebateInfo = new RebateInfo();
for (String key : link.keySet()) {
Object val = obj.get(link.get(key));
RebateInfo.class.getMethod("set" key.substring(0, 1).toUpperCase() key.substring(1), String.class).invoke(rebateInfo, val.toString());
}
return rebateInfo;
}
}
单元测试
代码语言:javascript复制public class ApiTest {
@Test
public void test_MQAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, ParseException {
SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parse = s.parse("2020-06-01 23:20:16");
CreateAccount create_account = new CreateAccount();
create_account.setNumber("100001");
create_account.setAddress("河北省.廊坊市.广阳区.大学里职业技术学院");
create_account.setAccountDate(parse);
create_account.setDesc("在校开户");
HashMap<String, String> link01 = new HashMap<>();
link01.put("userId", "number");
link01.put("bizId", "number");
link01.put("bizTime", "accountDate");
link01.put("desc", "desc");
RebateInfo rebateInfo01 = MQAdapter.filter(JSONObject.toJSONString(create_account), link01);
System.out.println("mq.create_account(适配前)" JSONObject.toJSONString(create_account));
System.out.println("mq.create_account(适配后)" JSON.toJSONString(rebateInfo01));
System.out.println(" ");
OrderMq orderMq = new OrderMq();
orderMq.setUid("100001");
orderMq.setSku("10928092093111123");
orderMq.setOrderId("100000890193847111");
orderMq.setCreateOrderTime(parse);
HashMap<String, String> link02 = new HashMap<>();
link02.put("userId", "uid");
link02.put("bizId", "orderId");
link02.put("bizTime", "createOrderTime");
RebateInfo rebateInfo02 = MQAdapter.filter(JSONObject.toJSONString(orderMq), link02);
System.out.println("mq.orderMq(适配前)" JSONObject.toJSONString(orderMq));
System.out.println("mq.orderMq(适配后)" JSON.toJSONString(rebateInfo02));
}
}
输出结果:
代码语言:javascript复制mq.create_account(适配前){"accountDate":1591024816000,"address":"河北省.廊坊市.广阳区.大学里职业技术学院","desc":"在校开户","number":"100001"}
mq.create_account(适配后){"bizId":"100001","bizTime":"1591024816000","desc":"在校开户","userId":"100001"}
mq.orderMq(适配前){"createOrderTime":1591024816000,"orderId":"100000890193847111","sku":"10928092093111123","uid":"100001"}
mq.orderMq(适配后){"bizId":"100000890193847111","bizTime":"1591024816000","userId":"100001"}
定义统一适配接口(OrderAdapterService)
代码语言:javascript复制public interface OrderAdapterService {
boolean isFirst(String uId);
}
分别实现两个不同的接口
代码语言:javascript复制
public class InsideOrderServiceImpl implements OrderAdapterService {
private final OrderService orderService = new OrderService();
@Override
public boolean isFirst(String uId) {
return orderService.queryUserOrderCount(uId) <= 1;
}
}
public class POPOrderAdapterServiceImpl implements OrderAdapterService {
private final POPOrderService popOrderService = new POPOrderService();
@Override
public boolean isFirst(String uId) {
return popOrderService.isFirstOrder(uId);
}
}
单元测试
代码语言:javascript复制public class ApiTest {
@Test
public void test_itfAdapter() {
OrderAdapterService popOrderAdapterService = new POPOrderAdapterServiceImpl();
System.out.println("判断首单,接口适配(POP):" popOrderAdapterService.isFirst("100001"));
OrderAdapterService insideOrderService = new InsideOrderServiceImpl();
System.out.println("判断首单,接口适配(自营):" insideOrderService.isFirst("100001"));
}
}
代码语言:javascript复制14:53:31.455 [main] INFO fun.lixj.design.service.POPOrderService - POP商家,查询用户的订单是否为首单:100001
判断首单,接口适配(POP):true
14:53:31.459 [main] INFO fun.lixj.design.service.OrderService - 自营商家,查询用户的订单是否为首单:100001
判断首单,接口适配(自营):false
总结
从本文可以看出,即使不使用适配器模式,也可以实现这些功能。但是使用了适配器模式可以让代码更干净、整洁,减少大量重复的判断和使用,同时也让代码更易于维护和扩展。尤其对于MQ等多种消息体中有不同属性的同类值(abc="123"、def="123"),进行适配再加上代理类,就可以使用简单的配置方式接入对方提供的MQ消息,而不需要重复地开发,非常利于扩展。
Copyright: 采用 知识共享署名4.0 国际许可协议进行许可 Links: https://lixj.fun/archives/设计模式-适配器模式