跳至主要內容

【设计模式】结构型-①适配器模式

holic-x...大约 12 分钟设计模式设计模式

【设计模式】结构型-①适配器模式

学习核心

  • 适配器模式概念核心(接口转接器、万能充)
    • 概念:将原本不兼容的接口通过指定规范进行适配达到统一
    • 组成:
      • 属性适配:统一属性定义、属性适配器(将属性和具体业务属性进行关联,例如通过反射进行转化)
      • 接口适配:适配接口定义、相应接口实现
  • 业务场景案例
    • 【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
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3