适配器模式介绍

适配器模式的主要作用是把原本不兼容的接口通过适配修改做到统一,方便调用方使用。

适配器模式的简单使用

场景:MQ消息体

例如一些营销系统,常见的拉客,如邀请一位用户,或者邀请一位用户下单,平台就会返利,并且多邀多得。同时,随着拉新量的增多,平台开始设置每月首单返现等奖励。

开发这样一个营销系统就会遇到各种各样的MQ消息或接口,如果逐个开发,会耗费很高的成本,同时后期的扩展也有一定的难度。此时会希望有一个系统,配置后就能把外部的MQ接入,而适配器的思想也恰恰可以运用到这里。需要强调的是,适配器不只可以适配接口,可以适配一些属性信息。

这里模拟了三个不同类型的MQ消息:CreateAccount、OrderMq和POPOrderDelivered。在消息体中有一些必要的字段,如用户ID、时间和业务ID,但是每个MQ的字段名称并不同,就像用户ID在冉的MQ里也有不用的字段uId、userId等一样。另外,这里还提供了两种不同类型的接口:OrderService用于查询内部订单的下单数量,POPOrderService用于查询第三方是否是首单。

后面需要吧这些不用类型的MQ和接口进行适配兼容,这种场景在开发中也很常见。

注册开户MQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.bestrookie.ma;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author bestrookie
* @date 2021/11/4 2:07 下午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreateAccount {
/**
* 开户编号
*/
private String number;
/**
* 开户地
*/
private String address;
/**
* 开户时间
*/
private Date accountDate;
/**
* 开户描述
*/
private String desc;
}

内部订单MQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.bestrookie.ma;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author bestrookie
* @date 2021/11/4 2:10 下午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderMq {
/**
* 用户id
*/
private String uid;
/**
* 商品编号
*/
private String sku;
/**
* 订单id
*/
private String orderId;
/**
* 下单时间
*/
private Date createOrderTime;
}

第三方订单MQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.bestrookie.ma;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;

/**
* @author bestrookie
* @date 2021/11/4 2:14 下午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class POPOrderDelivered {
/**
* 用户id
*/
private String uId;
/**
* 订单号
*/
private String orderId;
/**
* 订单时间
*/
private Date orderTime;
/**
* 商品编号
*/
private String sku;
/**
* 商品名称
*/
private String skuName;
/**
* 商品金额
*/
private BigDecimal decimal;
}

查询用户内部下单数量接口

1
2
3
4
5
6
7
8
9
10
11
package com.bestrookie.service;
/**
* @author bestrookie
* @date 2021/11/4 2:34 下午
*/
public class OrderService {
public long queryUserOrderCount(String userId){
System.out.println("内部商家,查询用户的下单数量: "+userId);
return 10L;
}
}

查询用户第三方下单首单接口

1
2
3
4
5
6
7
8
9
10
11
package com.bestrookie.service;
/**
* @author bestrookie
* @date 2021/11/4 2:37 下午
*/
public class POPOrderService {
public boolean isFirstOrder(String uid){
System.out.println("POP商家,查询用户的订单是否为首单:"+uid);
return true;
}
}

违背设计模式接受消息实现

这个工程需要接受三个MQ消息,所以就有了三个对应类,这和平时的代码机几乎一模一样,吐过MQ消息数量不多,则这种写法没什么问题;但如果是中台服务,随着对接服务数量的增加,需要考虑用一些设计模式来解决。

CreateAccountMqService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.bestrookie.design;
import com.alibaba.fastjson.JSON;
import com.bestrookie.ma.CreateAccount;

/**
* @author bestrookie
* @date 2021/11/4 2:45 下午
*/
public class CreateAccountMqService {
public void onMessage(String message){
CreateAccount ma = JSON.parseObject(message,CreateAccount.class);
ma.getAccountDate();
//业务逻辑
}
}

OrderMqService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bestrookie.design;

import com.alibaba.fastjson.JSON;
import com.bestrookie.ma.OrderMq;

/**
* @author bestrookie
* @date 2021/11/4 3:04 下午
*/
public class OrderMqService {
public void onMessage(String message){
OrderMq mq = JSON.parseObject(message,OrderMq.class);
mq.getUid();
//业务逻辑
}
}

POPOrderDeliveredService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.bestrookie.design;
import com.alibaba.fastjson.JSON;
import com.bestrookie.ma.POPOrderDelivered;
/**
* @author bestrookie
* @date 2021/11/4 3:12 下午
*/
public class POPOrderDeliveredService {
public void onMessage(String message){
POPOrderDelivered mq = JSON.parseObject(message,POPOrderDelivered.class);
mq.getOrderId();
//业务逻辑
}
}

这三组MQ的消费类都是一样的,从这里也能看到它们的字段在使用上有一些相似。研发人员能够针对不规则的需求,按照统一的标准处理,降低开发成本,提高研发效率。

适配器模式重构代码

适配器模式解决的主要问题是如何针对多种差异化类型的接口实现统一输出。把不同类型的消息中的属性字段做统一处理,便减少后续人工硬编码方式对MQ的接受。

image-20211105104749858

适配器模式的工程提后了两种适配器:接口适配和MQ适配。之所以不只做接口适配的案例,因为这样的而开发很常见。所以把适配的思想应用的MQ消息体上,增加多设计模式的认知。先做MQ适配,接受各种各样的MQ消息。当业务发展得很快时,需要下单用户满足首单条件时才给予奖励,在这种场景下再增加对接口的操作。

MQ适配

为了满足产品功能的需求,提取此项功能中必须的字段信息,单独创建一个RebateInfo。后续所有的MQ信息都需要提供这些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.bestrookie.adapter;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author bestrookie
* @date 2021/11/4 3:37 下午
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RebateInfo {
/**
* 用户Id
*/
private String userId;
/**
* 业务id
*/
private String bizId;
/**
* 业务时间
*/
private String bizTime;
/**
* 业务描述
*/
private String desc;
}

MQ消息中会有多种多样的类型属性,虽然他们都同样提供给使用方,但是如果都这样接入,那么当MQ消息特别多时就会很耗时。所以,在这个案例中定义了通用的MQ消息体,后续把所有接入进来的消息进行统一的适配。

MQ消息统一适配类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.bestrookie.adapter;
import com.alibaba.fastjson.JSON;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;

/**
* @author bestrookie
* @date 2021/11/4 3:48 下午
*/
public class MQAdapter {
public static RebateInfo filter(String strJson, Map<String,String> link) throws InvocationTargetException, NoSuchMethodException, 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 value = obj.get(link.get(key));
RebateInfo.class.getMethod("set"+key.substring(0,1).toUpperCase()+key.substring(1),String.class).invoke(rebateInfo,value.toString());
}
return rebateInfo;
}
}

这个类里面的方法非常重要,主要用于把不同类型的MQ中的各种属性映射成需要的属性并返回。就像一个属性中有用户ID uId,将其映射到需要的userId,做统一处理。而这个处理过程需要把映射管理传递给Map<String,String>link,也就是准确地描述了当前MQ中某个属性的名称,映射为指定的某个属性名称。接受到的MQ消息基本是JSON格式,可以传唤为MAP结构。最后使用反射调用的方式对类型赋值。

接口适配

两个接口的判断逻辑和使用方法不同,不同的接口提供方也有不同的出参。一个是直接判断是否为首单,另一个需要根据订单数量判断,因此,需要适配器的模式实现。虽然使用if语句可以实现,但是这样的写法会导致后期难以维护。

定义统一适配接口

1
2
3
4
5
6
7
8
package com.bestrookie.adapter;
/**
* @author bestrookie
* @date 2021/11/4 4:50 下午
*/
public interface OrderAdapterService {
boolean isFirst(String uId);
}

分别实现两个不同的接口

1
2
3
4
5
6
7
8
9
10
11
12
package com.bestrookie.adapter;
import com.bestrookie.service.OrderService;
/**
* @author bestrookie
* @date 2021/11/4 4:51 下午
*/
public class InsideOrderService implements OrderAdapterService{
private OrderService orderService;
public boolean isFirst(String uId) {
return orderService.queryUserOrderCount(uId) >= 1;
}
}

第三方商品接口

1
2
3
4
5
6
7
8
9
10
11
12
package com.bestrookie.adapter;
import com.bestrookie.service.POPOrderService;
/**
* @author bestrookie
* @date 2021/11/4 4:53 下午
*/
public class POPOrderAdapterServiceImpl implements OrderAdapterService{
private POPOrderService popOrderService = new POPOrderService();
public boolean isFirst(String uId) {
return popOrderService.isFirstOrder(uId);
}
}

总结

即使不使用适配器模式,也可以实现这些功能。但是使用了适配器模式可以让代码更干净、整洁,减少大量重复的判断和使用,同时也让代码更易于维护和扩展。尤其对于MQ等多种消息体中有不同属性的同类值,进行适配在加上代理类,就可以使用简单的配置方式接入对方提供的MQ消息,而不需要重复地开发,非常利于扩展。

评论