【设计模式】结构型-①适配器模式
【设计模式】结构型-①适配器模式
注
学习核心
- 适配器模式概念核心(接口转接器、万能充)
- 概念:将原本不兼容的接口通过指定规范进行适配达到统一
- 组成:
- 属性适配:统一属性定义、属性适配器(将属性和具体业务属性进行关联,例如通过反射进行转化)
- 接口适配:适配接口定义、相应接口实现
- 业务场景案例
- 【Spring框架】:SpringMVC框架核心中的HandlerAdapter(处理相应的映射请求)
- 【search-platform】接入数据源:针对不同数据源的接入,通过定义接口规范,不同数据源基于接口实现业务逻辑(search方法)
- 【营销活动场景】(属性适配、接口适配):
- 属性适配(MQ消息体):业务场景中涉及多个MQ消息传递、处理,通过统一消息体定义格式,增加MQ消息处理的灵活度
- 接口适配(同类型业务处理适配):例如不同类型的订单服务处理(判断订单是否为首单),定义统一接口规范,由相应的服务实现填充业务逻辑
基本概念
概念说明
适配器模式的主要作⽤就是把原本不兼容的接⼝,通过适配修改做到统⼀。使得⽤户⽅便使⽤,就像我们提到的万能充、数据线、MAC笔记本的转换头、出国旅游买个插座等等,他们都是为了适配各种不同的口做的兼容。在业务开发中会经常需要做不同接⼝的兼容,尤其是中台服务,中台需要把各个业务线的各种类型服务做统⼀包装,再对外提供接⼝进⾏使⽤
场景案例分析
1.数据源接入(DataSource设计)
类似数据源接入场景(DataSource接口定义、不同xxxDataSource接入)
场景分析
概念说明:本场景中使用适配器模式,通过转换,让两个系统能够完成对接。例如要调用一个对方接口,但是对方接口提供的调用参数和自身项目预期的参数不一致,因此借助适配器模式进行转化,可以和现实场景的转换器对照。
为什么要定制统一的规范,可以适当避免一些盲目接入接口的场景
定制统一的数据源接入规范(标准) :
什么数据源允许接入?
数据源接入时要满足什么要求?
需要接入方注意什么事情?
本系统要求:任何接入我们系统的数据,它必须要能够根据关键词搜索、并且支持分页搜索。通过声明接口的方式来定义规范。
❓问题扩展:假如说数据源已经支持了搜索,但是原有的方法参数和规范不一致,怎么办?
例如此处提供了Picture、User、Post等多个数据源,但其中Post是接入本地数据库进行查找,需要对请求信息进行校验 ,但这个request又不是各个数据搜索所必须的条件,遇到这种情况如何去做相应处理
【1】最直接了断的方式:不满足规范考虑剔除,对接参数需求额外提供接口/方法进行处理
【2】想办法解决参数问题:尽量自主获取到参数信息,此处借助RequestContextHolder获取请求信息从而拿到所需数据,但也会引申一个问题:当请求来源不同的时候这个request可能和系统所需的有所出入(或者如果借助shiro等一些权限校验框架,可以考虑通过其提供的工具类获取)
【3】有待考究的方式:修改规范,确认其他接口是否也是需要这个参数,但这个改造成本可能在后期会显得大,因为一些现有的接口已经按照既定规范执行,唯恐牵一发动全身
上述方式都是基于不同场景的考虑,需要结合实际选择一种改动最优的方式去解决,由于一开始就统一了规范,要规避一些已经上线结构因规范调整而牵动的联动
适配器模式:
针对不同数据源接入:定义DataSource接口和search接口方法(约定查找参数和返回结果),不同的数据源接入可通过实现DataSource接口方法完成不同的检索操作
// 1.定义DataSource接口统一规范,提供方法入口:统一入参和出参
public interface DataSource<T> {
/**
* 搜索(确定入参、出参):例如此处根据查找类型
*/
void doSearch(String searchText);
}
// 2.多个不同DataSource实现
public class XXXDataSource implements DataSource<Picture> {
@Override
public void doSearch(String searchText) {
return null;
}
}
// 3.项目中应用:获取到指定数据源,随后调用对应的doSearch方法
DataSource dataSource = new XXXDataSource();
dataSource.doSearch("xxxx");
设计实现
实现:定义DataSource接口统一接口方法规范,不同数据源接入PictureDataSource、UserDataSource、PostDataSource实现接口实现对应业务逻辑封装。如果说要接入新的数据源,则定义xxxDataSource去实现相应的接口方法
// 1.定义DataSource接口统一规范,提供方法入口:统一入参和出参
// 入参:查询条件、出参:分页数据(为了便于前后端统一分页数据统一为dataList)
public interface DataSource<T> {
/**
* 搜索
*/
Page<T> doSearch(String searchText, long pageNum, long pageSize);
}
// 2.多个不同DataSource实现
public class PictureDataSource implements DataSource<Picture> {
@Override
public Page<Picture> doSearch(String searchText, long pageNum, long pageSize) {
return null;
}
}
public class UserDataSource implements DataSource<User> {
@Override
public Page<User> doSearch(String searchText, long pageNum, long pageSize) {
return null;
}
}
public class PostDataSource implements DataSource<Post> {
@Override
public Page<Post> doSearch(String searchText, long pageNum, long pageSize) {
return null;
}
}
// 3.响应交互:对外提供接口,根据入参确认访问的数据库(伪代码参考)
public BaseResponse<SearchVO> searchAllByCondAdaptor(@RequestBody SearchRequest searchRequest, HttpServletRequest request) {
// a.参数校验
// b.根据不同数据源进行接入
// c.封装响应数据并返回
----------------------------------------数据源接入参考----------------------------------------------------------------
DataSource dataSource = null;
// 根据Type类别分别处理
switch (searchTypeEnum){
case PICTURE:
// 指定图片数据源
dataSource = pictureDataSource;
break;
case USER:
// 指定用户数据源
dataSource = userDataSource;
break;
case POST:
// 指定文章数据源
dataSource = postDataSource;
break;
default:
}
// 根据数据源调用适配器方法获取相应的分页数据
Page page = dataSource.doSearch(searchText, searchRequest.getCurrent(), searchRequest.getPageSize());
// 最终将查询到的数据信息进行封装并返回
return 封装后的响应数据;
----------------------------------------------------------------------------------------------------------------
}
}
// 4.业务扩展:需要接入新的数据源(例如此处需要接入视频数据源,则只需要实现相应的DataSource规范的接口)
-- a.接入新数据源
public class VideoDataSource implements DataSource<Object> {
@Override
public Page<Object> doSearch(String searchText, long pageNum, long pageSize) {
return null;
}
}
-- b.业务逻辑引申处理
// 参考步骤3中controller层处理,只需要在条件语句中补充新接入的数据源即可
switch (searchTypeEnum){
case PICTURE:
// 指定图片数据源
dataSource = pictureDataSource;
break;
case VIDEO:
// 新接入视频数据源
dataSource = videoDataSource;
break;
case MORE:
// 更多数据源接入
dataSource = moreDataSource;
break;
default:
}
// 根据数据源调用适配器方法获取相应的分页数据
Page page = dataSource.doSearch(searchText, searchRequest.getCurrent(), searchRequest.getPageSize());
2.MQ消息适配场景(营销活动场景)
核心:定义统一适配规范,将多个MQ消息结构适配为统一的消息字段进行处理
针对营销活动场景中,可能涉及到多个MQ消息的传递和处理,不同业务的MQ消息体有所不同。传统思路则是根据不同的业务定义多个不同的消息体,然后针对每个消息进行传递、处理。但随着业务的发展,这种实现思路到后期就会使得项目变得臃肿。因此引入适配器模式,通过自定义消息适配器,将MQ消息转换为统一的消息体进行处理
✨传统实现思路
例如营销活动场景中有开户、内部订单、外部订单这几个MQ消息体,需要相应进行传递和处理。随着业务扩展,后续可能需要定义各种各样的MQ消息或进行相应接口开发,一方面是开发成本累加,另一方面是后期扩展难度上升。而适配器的概念不仅仅是适配接口定义,也可以是适配属性信息转化
- MQ 消息体定义
/**
* MQ 消息体:开户MQ信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateCountMq {
private String number; // 开户编号
private String address;// 开户地址
private Date accountDate;// 开户时间
private String desc;// 开户描述
@Override
public String toString() {
return "CreateCountMq{" +
"number='" + number + '\'' +
", address='" + address + '\'' +
", accountDate=" + accountDate +
", desc='" + desc + '\'' +
'}';
}
}
/**
* MQ 消息体:内部订单MQ信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InnerOrderMq {
private String uid; // 用户ID
private String orderId; // 订单ID
private Date orderTime;// 下单时间
private String sku; // 商品信息
private String skuName; // 商品名称
private BigDecimal decimal;// 订单金额
}
/**
* MQ 消息体:第三方订单MQ信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ThirdOrderMq {
private String uid; // 用户ID
private String orderId; // 订单ID
private Date orderTime;// 下单时间
private String sku; // 商品信息
private String skuName; // 商品名称
private BigDecimal decimal;// 订单金额
}
- MQ 消息处理定义
/**
* MQ 消息处理服务类:CreateAccount
*/
public class CreateAccountMqService {
public void onMessage(String message) {
// 解析MQ消息,然后调用服务进行处理
CreateCountMq createCountMq = JSON.parseObject(message,CreateCountMq.class);
// 业务逻辑处理
System.out.println("开户处理" + createCountMq.toString());
}
}
/**
* MQ 消息处理服务类:内部订单处理服务接口
*/
public class InnerOrderMqService {
// 查询用户订单是否为首单
public long queryUserOrderCount(String uid){
System.out.println("自营商家(内部订单),查询用户订单是否为首单:" + uid);
return 10L; // 模拟返回
}
}
/**
* MQ 消息处理服务类:第三方订单处理服务
*/
public class ThirdOrderMqService {
// 查询用户订单是否为首单
public boolean isFirstOrder(String uid){
System.out.println("POP商家(第三方订单),查询用户订单是否为首单:" + uid);
return true; // 模拟返回
}
}
✨适配器模式实现思路
引入适配器模式,此处可以从属性适配(MQ消息定义适配)、接口适配(同类型处理接口适配)两方面进行切入
(1)属性适配(MQ 消息适配)
例如针对rebate营销活动场景,定义一个统一的消息体来进行适配,此处主要涉及两个定义:
- 统一消息体定义(RebateMqInfo):公共的属性定义、扩展业务属性定义(根据链接关系,通过反射机制动态映射到对应属性)
- 针对链接关系,可以通过数据库配置进行关联或者在公共配置平台中进行配置,便于统一管理
- 统一消息体适配器(MqAdapter):过滤处理相应的消息信息,将其转化为RebateMqInfo
/**
* 统一MQ消息体定义
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RebateMqInfo {
private String userId; // 用户ID
private String bizId; // 业务ID
private Date bizTime; // 业务时间
private String desc; // 业务描述
// 可设定Map额外接收业务相关扩展参数,通过反射机制动态生成并映射
private Map<String,Object> params;
}
/**
* MQ 消息适配(将传递的字符串类型的MQ消息转换成统一的MQ消息体)
*/
public class MqAdapter {
/**
* 将传递的strJson映射到相应的字段中
* @param strJson
* @param link
* @return
*/
public static RebateMqInfo filter(String strJson, Map<String,String> link) {
Map obj = JSON.parseObject(strJson);
RebateMqInfo rebateMqInfo = new RebateMqInfo();
link.keySet().forEach(key->{
Object val = obj.get(link.get(key));
try {
// RebateMqInfo.class.getMethod("set" + key.substring(0,1).toUpperCase() + key.substring(1),String.class).invoke(rebateMqInfo,val);
// 根据不同属性类型进行设定
Field field = RebateMqInfo.class.getDeclaredField(key);
String type = String.valueOf(field.getType()); // getDeclaredField 私有属性获取
if(type.equals("class java.lang.String")){
RebateMqInfo.class.getMethod("set" + key.substring(0,1).toUpperCase() + key.substring(1),String.class).invoke(rebateMqInfo,val);
}else if(type.equals("class java.util.Date")){
field.setAccessible(true);
field.set(rebateMqInfo,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(val.toString()));
}else if(type.equals("interface java.util.Map")){
field.setAccessible(true);
field.set(rebateMqInfo, (Map<String,Object>)obj);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return rebateMqInfo;
}
}
测试MQ消息属性适配
/**
* MQ 消息适配(将传递的字符串类型的MQ消息转换成统一的MQ消息体)
*/
public class MqAdapterDemo {
public static void main(String[] args) {
/**
* 模拟开户信息适配
*/
CreateCountMq createCountMq = new CreateCountMq("1001","浙江杭州",new Date(),"新开户");
// 构建开户信息属性关联
Map<String,String> createCountLink = new HashMap<>();
createCountLink.put("userId","number");
createCountLink.put("bizId","number");
createCountLink.put("bizTime","accountDate");
createCountLink.put("desc","desc");
// 通过适配器进行转化
RebateMqInfo rebateMqInfo1 = MqAdapter.filter(JSON.toJSONString(createCountMq),createCountLink);
System.out.println("适配前:" + JSON.toJSONString(createCountMq));
System.out.println("适配后:" + JSON.toJSONString(rebateMqInfo1));
/**
* 模拟订单信息适配
*/
InnerOrderMq innerOrderMq = new InnerOrderMq("1001","o1001",new Date(),"电脑","acer 电脑",new BigDecimal("10000"));
// 构建开户信息属性关联
Map<String,String> innerOrderLink = new HashMap<>();
innerOrderLink.put("userId","uid");
innerOrderLink.put("bizId","orderId");
innerOrderLink.put("bizTime","orderTime");
innerOrderLink.put("params",JSON.toJSONString(innerOrderMq));
// 通过适配器进行转化
RebateMqInfo rebateMqInfo2 = MqAdapter.filter(JSON.toJSONString(innerOrderMq),innerOrderLink);
System.out.println("适配前:" + JSON.toJSONString(innerOrderMq));
System.out.println("适配后:" + JSON.toJSONString(rebateMqInfo2));
}
}
-- output
适配前:{"accountDate":"2024-09-23 11:25:46.889","address":"浙江杭州","desc":"新开户","number":"1001"}
适配后:{"bizId":"1001","bizTime":"2024-09-23 11:25:46","desc":"新开户","userId":"1001"}
适配前:{"decimal":10000,"orderId":"o1001","orderTime":"2024-09-23 11:25:47.119","sku":"电脑","skuName":"acer 电脑","uid":"1001"}
适配后:{"bizId":"o1001","bizTime":"2024-09-23 11:25:47","params":{"decimal":10000,"orderId":"o1001","orderTime":"2024-09-23 11:25:47.119","sku":"电脑","skuName":"acer 电脑","uid":"1001"},"userId":"1001"}
(2)接口适配(接口处理适配)
针对订单处理,定义统一适配接口,然后通过接口实现来进行业务逻辑封装:OrderAdapterService(interface)、InnerOrderService(class)、ThirdOrderService
- 定义统一的接口格式,由相应的业务服务实现接口逻辑,外部不需要关注接口内部的调度逻辑
/**
* 订单处理适配器(接口定义)
*/
public interface OrderAdapterService{
// 判断是否为首单
public boolean isFirstOrder(String uid);
}
/**
* 内部订单服务处理
*/
public class InnerOrderServiceImpl implements OrderAdapterService{
InnerOrderMqService innerOrderMqService = new InnerOrderMqService();
@Override
public boolean isFirstOrder(String uid) {
System.out.println("inner order");
// 模拟业务逻辑处理
long count = innerOrderMqService.queryUserOrderCount(uid);
return count==0;
}
}
/**
* 第三方订单服务处理
*/
public class ThirdOrderServiceImpl implements OrderAdapterService{
ThirdOrderMqService thirdOrderMqService = new ThirdOrderMqService();
@Override
public boolean isFirstOrder(String uid) {
System.out.println("third order");
// 模拟业务逻辑处理
return thirdOrderMqService.isFirstOrder(uid);
}
}
demo 测试
/**
* 订单服务接口适配 demo 测试
*/
public class OrderAdapterServiceDemo {
public static void main(String[] args) {
OrderAdapterService innerOrderService = new InnerOrderServiceImpl();
System.out.println("innerOrderService 适配器:" + innerOrderService.isFirstOrder("1001"));
OrderAdapterService thirdOrderService = new ThirdOrderServiceImpl();
System.out.println("thirdOrderService 适配器:" + thirdOrderService.isFirstOrder("2001"));
}
}
-- output
inner order
自营商家(内部订单),查询用户订单是否为首单:1001
innerOrderService 适配器:false
third order
POP商家(第三方订单),查询用户订单是否为首单:2001
thirdOrderService 适配器:true