适配器模式
1.适配器模式介绍
适配器模式的主要作用就是把原本不兼容的接口,通过适配修改做到统一。使得用户方便使用,就像我们提到的万能充、数据线、MAC笔记本的转换头、出国旅游买个插座等等,他们都是为了适配各种不同的口
,做的兼容。
除了我们生活中出现的各种适配的场景,那么在业务开发中呢?
在业务开发中我们会经常的需要做不同接口的兼容,尤其是中台服务,中台需要把各个业务线的各种类型服务做统一包装,再对外提供接口进行使用。而这在我们平常的开发中也是非常常见的。
2.案例场景模拟
随着公司的业务的不断发展,当基础的系统逐步成型以后。业务运营就需要开始做用户的拉新和促活,从而保障DAU
的增速以及最终ROI
转换。
而这时候就会需要做一些营销系统,大部分常见的都是裂变、拉客,例如;你邀请一个用户开户、或者邀请一个用户下单,那么平台就会给你返利,多邀多得。同时随着拉新的量越来越多开始设置每月下单都会给首单奖励,等等,各种营销场景。
那么这个时候做这样一个系统就会接收各种各样的MQ消息或者接口,如果一个个的去开发,就会耗费很大的成本,同时对于后期的拓展也有一定的难度。此时就会希望有一个系统可以配置一下就把外部的MQ接入进行,这些MQ就像上面提到的可能是一些注册开户消息、商品下单消息等等。
而适配器的思想方式也恰恰可以运用到这里,并且我想强调一下,适配器不只是可以适配接口往往还可以适配一些属性信息。
2.1场景简述
注册开户MQ
public class create_account {
private String number; // 开户编号
private String address; // 开户地
private Date accountDate; // 开户时间
private String desc; // 开户描述
// ... get/set
}
内部订单MQ
public class OrderMq {
private String uid; // 用户ID
private String sku; // 商品
private String orderId; // 订单ID
private Date createOrderTime; // 下单时间
// ... get/set
}
第三方订单MQ
public class POPOrderDelivered {
private String uId; // 用户ID
private String orderId; // 订单号
private Date orderTime; // 下单时间
private Date sku; // 商品
private Date skuName; // 商品名称
private BigDecimal decimal; // 金额
// ... get/set
}
查询用户内部下单数量接口
public class OrderService {
private Logger logger = LoggerFactory.getLogger(POPOrderService.class);
public long queryUserOrderCount(String userId){
logger.info("自营商家,查询用户的订单是否为首单:{}", userId);
return 10L;
}
}
查询用户第三方下单首单接口
public class POPOrderService {
private Logger logger = LoggerFactory.getLogger(POPOrderService.class);
public boolean isFirstOrder(String uId) {
logger.info("POP商家,查询用户的订单是否为首单:{}", uId);
return true;
}
}
- 以上这几项就是不同的MQ以及不同的接口的一个体现,后面我们将使用这样的MQ消息和接口,给它们做相应的适配。
3.用一坨坨代码实现
先体现一下这种方式的实现模拟,但是这样的实现有一个很大的问题就是,当MQ消息越来越多后,甚至几十几百以后,你作为中台要怎么优化呢?
Mq接收消息实现
//处理注册开户
public class create_accountMqService {
public void onMessage(String message) {
create_account mq = JSON.parseObject(message, create_account.class);
mq.getNumber();
mq.getAccountDate();
// ... 处理自己的业务
}
}
//处理内部订单
public class OrderMqService {
public void onMessage(String message) {
OrderMq mq = JSON.parseObject(message, OrderMq.class);
mq.getUid();
mq.getOrderId();
mq.getCreateOrderTime();
// ... 处理自己的业务
}
}
//处理用户第三方下单首单
public class POPOrderDeliveredService {
public void onMessage(String message) {
POPOrderDelivered mq = JSON.parseObject(message, POPOrderDelivered.class);
mq.getuId();
mq.getOrderId();
mq.getOrderTime();
// ... 处理自己的业务
}
}
- 三组MQ的消息都是一样模拟使用,对于三个消费者。
4.适配器模式重构代码
适配器模式要解决的主要问题就是多种差异化类型的接口做统一输出,这在我们学习工厂方法模式中也有所提到不同种类的奖品处理,其实那也是适配器的应用。
在本文中我们还会再另外体现出一个多种MQ接收,使用MQ的场景。来把不同类型的消息做统一的处理,便于减少后续对MQ接收。
这里展示的MQ兼容的核心部分,也就是处理适配不同的类型字段。而如果我们接收MQ后,在配置不同的消费类时,如果不希望一个个开发类,那么可以使用代理类的方式进行处理。
适配器模型结构
- 这里包括了两个类型的适配;接口适配、MQ适配。之所以不只是模拟接口适配,因为很多时候大家都很常见了,所以把适配的思想换一下到MQ消息体上,增加大家多设计模式的认知。
- 先是做MQ适配,接收各种各样的MQ消息。当业务发展的很快,需要对下单用户首单才给奖励,在这样的场景下再增加对接口的适配操作。
代码实现(MQ消息适配)
统一的MQ消息体
public class RebateInfo {
private String userId; // 用户ID
private String bizId; // 业务ID
private Date bizTime; // 业务时间
private String desc; // 业务描述
// ... get/set
}
- MQ消息中会有多种多样的类型属性,虽然他们都有同样的值提供给使用方,但是如果都这样接入那么当MQ消息特别多时候就会很麻烦。
- 所以在这个案例中我们定义了通用的MQ消息体,后续把所有接入进来的消息进行统一的处理。
MQ消息体适配类
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;
}
}
- 这个类里的方法非常重要,主要用于把不同类型MQ种的各种属性,映射成我们需要的属性并返回。就像一个属性中有
用户ID;uId
,映射到我们需要的;userId
,做统一处理。 - 而在这个处理过程中需要把映射管理传递给
Map<String, String> link
,也就是准确的描述了,当前MQ中某个属性名称,映射为我们的某个属性名称。 - 最终因为我们接收到的
mq
消息基本都是json
格式,可以转换为MAP结构。最后使用反射调用的方式给我们的类型赋值。
测试适配类
编写单元测试类
@Test
public void test_MQAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
create_account create_account = new create_account();
create_account.setNumber("100001");
create_account.setAddress("河北省.廊坊市.广阳区.大学里职业技术学院");
create_account.setAccountDate(new Date());
create_account.setDesc("在校开户");
HashMap<String, String> link01 = new HashMap<String, String>();
link01.put("userId", "number");
link01.put("bizId", "number");
link01.put("bizTime", "accountDate");
link01.put("desc", "desc");
RebateInfo rebateInfo01 = MQAdapter.filter(create_account.toString(), link01);
System.out.println("mq.create_account(适配前)" + create_account.toString());
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(new Date());
HashMap<String, String> link02 = new HashMap<String, String>();
link02.put("userId", "uid");
link02.put("bizId", "orderId");
link02.put("bizTime", "createOrderTime");
RebateInfo rebateInfo02 = MQAdapter.filter(orderMq.toString(), link02);
System.out.println("mq.orderMq(适配前)" + orderMq.toString());
System.out.println("mq.orderMq(适配后)" + JSON.toJSONString(rebateInfo02));
}
- 在这里我们分别模拟传入了两个不同的MQ消息,并设置字段的映射关系。
- 等真的业务场景开发中,就可以配这种映射配置关系交给配置文件或者数据库后台配置,减少编码。
测试结果
mq.create_account(适配前){"accountDate":1591024816000,"address":"河北省.廊坊市.广阳区.大学里职业技术学院","desc":"在校开户","number":"100001"}
mq.create_account(适配后){"bizId":"100001","bizTime":1591077840669,"desc":"在校开户","userId":"100001"}
mq.orderMq(适配前){"createOrderTime":1591024816000,"orderId":"100000890193847111","sku":"10928092093111123","uid":"100001"}
mq.orderMq(适配后){"bizId":"100000890193847111","bizTime":1591077840669,"userId":"100001"}
Process finished with exit code 0
- 从上面可以看到,同样的字段值在做了适配前后分别有统一的字段属性,进行处理。这样业务开发中也就非常简单了。
- 另外有一个非常重要的地方,在实际业务开发中,除了反射的使用外,还可以加入代理类把映射的配置交给它。这样就可以不需要每一个mq都手动创建类了。
代码实现(接口使用适配)
就像我们前面提到随着业务的发展,营销活动本身要修改,不能只是接了MQ就发奖励。因为此时已经拉新的越来越多了,需要做一些限制。
因为增加了只有首单用户才给奖励,也就是你一年或者新人或者一个月的第一单才给你奖励,而不是你之前每一次下单都给奖励。
那么就需要对此种方式进行限制,而此时MQ中并没有判断首单的属性。只能通过接口进行查询,而拿到的接口如下;
接口 | 描述 |
---|---|
org.itstack.demo.design.service.OrderService.queryUserOrderCount(String userId) | 出参long,查询订单数量 |
org.itstack.demo.design.service.OrderService.POPOrderService.isFirstOrder(String uId) | 出参boolean,判断是否首单 |
- 两个接口的判断逻辑和使用方式都不同,不同的接口提供方,也有不同的出参。一个是直接判断是否首单,另外一个需要根据订单数量判断。
- 因此这里需要使用到适配器的模式来实现,当然if语句也是可以实现的。
定义统一适配接口
public interface OrderAdapterService {
boolean isFirst(String uId);
}
- 后面的实现类都需要完成此接口,并把具体的逻辑包装到指定的类中,满足单一职责。
分别实现两个不同的接口
内部商品接口
public class InsideOrderService implements OrderAdapterService {
private OrderService orderService = new OrderService();
public boolean isFirst(String uId) {
return orderService.queryUserOrderCount(uId) <= 1;
}
}
第三方商品接口
public class POPOrderAdapterServiceImpl implements OrderAdapterService {
private POPOrderService popOrderService = new POPOrderService();
public boolean isFirst(String uId) {
return popOrderService.isFirstOrder(uId);
}
}
- 在这两个接口中都实现了各自的判断方式,尤其像是提供订单数量的接口,需要自己判断当前接到mq时订单数量是否
<= 1
,以此判断是否为首单。
测试适配类
编写单元测试类
@Test
public void test_itfAdapter() {
OrderAdapterService popOrderAdapterService = new POPOrderAdapterServiceImpl();
System.out.println("判断首单,接口适配(POP):" + popOrderAdapterService.isFirst("100001"));
OrderAdapterService insideOrderService = new InsideOrderService();
System.out.println("判断首单,接口适配(自营):" + insideOrderService.isFirst("100001"));
}
测试结果
23:25:47.076 [main] INFO o.i.d.design.service.POPOrderService - POP商家,查询用户的订单是否为首单:100001
判断首单,接口适配(POP):true
23:25:47.079 [main] INFO o.i.d.design.service.POPOrderService - 自营商家,查询用户的订单是否为首单:100001
判断首单,接口适配(自营):false
Process finished with exit code 0
- 从测试结果上来看,此时已经的接口已经做了统一的包装,外部使用时候就不需要关心内部的具体逻辑了。而且在调用的时候只需要传入统一的参数即可,这样就满足了适配的作用。
5.总结
- 从上文可以看到不使用适配器模式这些功能同样可以实现,但是使用了适配器模式就可以让代码:干净整洁易于维护、减少大量重复的判断和使用、让代码更加易于维护和拓展。
- 尤其是我们对MQ这样的多种消息体中不同属性同类的值,进行适配再加上代理类,就可以使用简单的配置方式接入对方提供的MQ消息,而不需要大量重复的开发。非常利于拓展。
- 设计模式的学习学习过程可能会在一些章节中涉及到其他设计模式的体现,只不过不会重点讲解,避免喧宾夺主。但在实际的使用中,往往很多设计模式是综合使用的,并不会单一出现。