侧边栏壁纸
博主头像
qingtian博主等级

喜欢是一件细水流长的事,是永不疲惫的双向奔赴~!

  • 累计撰写 104 篇文章
  • 累计创建 48 个标签
  • 累计收到 1 条评论

设计模式之适配器模式

qingtian
2020-11-08 / 0 评论 / 0 点赞 / 1,023 阅读 / 12,264 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2020-12-31,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

设计模式之适配器模式

来源自《重学Java设计模式》链接提取码:ytc3

我们利用前几天的时间学习了设计模式中的创建型模式,接下来我们开始学习结构型模式的有关内容

适配器模式

适配器模式的主要作用就是把原本不兼容的接口,通过适配修改做到统一。使得用户方便使用,就像我们平时使用的数据线、MAC的转换头、出国旅游买个插座等等,它们都是为了适配各种不同的接口来做的兼容。

在业务开发中我们会经常的需要做不同接口兼容,尤其是中台服务,中台需要把各个业务线的各种类型服务做统一包装,再对外提供接口进行使用。而这在我们平常的开发中也是非常常见的。

案例场景模拟

截图1

随着公司业务的不断发展,当基础的系统逐步成型以后。业务运营就需要开始做用户的拉新和促活,从而保障DUA的增速以及最终ROI转化。

而这个时候就会需要做一些营销系统,大部分常见的都是裂变、拉客,例如:你邀请一个用户开户、或者邀请一个用户下单,那么平台就会给你返利,多邀多得。同时随着拉新的量越来越多开始设置每月下单都会给首单奖励,等等,各种营销场景。

那么这个时候做这样一个系统就会接收到各种各样的MQ消息或者接口,如果一个个的去开发,就会耗费很大的成本,同时对于后期的扩展也有一定的难度。此时就会希望有一个系统可以配置一下就把外部的MQ接入进行,这些MQ就像上面提到的可能是一些注册开户消息、商品下单消息。

而适配器的思想方式也恰恰可以运用到这里,并且我想强调一下,适配器不只是可以适配接口往往也可以适配一下属性信息。

场景模拟工程

  • 这里模拟了三个不同类型的MQ消息,而在消息体中都有一些必要的字段,比如:用户ID、时间、业务ID,但是每一个MQ的字段属性并不一样。就像用户ID在不同的MQ里也有不同的字段:uId、userId等。
  • 同时还提供了两个不同类型的接口,一个用于查询内部订单下单数量,一个用于查询第三方是否首单。
  • 后面会把这些不同类型的MQ和接口做适配兼容。

场景简述

  1. 注册用户MQ

    /**
     * 开户
     */
    public class create_account {
    
        private String number;      // 开户编号
        private String address;     // 开户地
        private Date accountDate;   // 开户时间
        private String desc;        // 开户描述
    
        public String getNumber() {
            return number;
        }
    
        public void setNumber(String number) {
            this.number = number;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        public Date getAccountDate() {
            return accountDate;
        }
    
        public void setAccountDate(Date accountDate) {
            this.accountDate = accountDate;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public void setDesc(String desc) {
            this.desc = desc;
        }
    
        @Override
        public String toString() {
            return JSON.toJSONString(this);
        }
    }
    
  2. 内部订单MQ

    public class OrderMq {
    
        private String uid;           // 用户ID
        private String sku;           // 商品
        private String orderId;       // 订单ID
        private Date createOrderTime; // 下单时间
    
        public String getUid() {
            return uid;
        }
    
        public void setUid(String uid) {
            this.uid = uid;
        }
    
        public String getSku() {
            return sku;
        }
    
        public void setSku(String sku) {
            this.sku = sku;
        }
    
        public String getOrderId() {
            return orderId;
        }
    
        public void setOrderId(String orderId) {
            this.orderId = orderId;
        }
    
        public Date getCreateOrderTime() {
            return createOrderTime;
        }
    
        public void setCreateOrderTime(Date createOrderTime) {
            this.createOrderTime = createOrderTime;
        }
    
        @Override
        public String toString() {
            return JSON.toJSONString(this);
        }
    }
    
  3. 第三方订单MQ

    /**
     * 订单妥投消息
     */
    public class POPOrderDelivered {
    
        private String uId;     // 用户ID
        private String orderId; // 订单号
        private Date orderTime; // 下单时间
        private Date sku;       // 商品
        private Date skuName;   // 商品名称
        private BigDecimal decimal; // 金额
    
        public String getuId() {
            return uId;
        }
    
        public void setuId(String uId) {
            this.uId = uId;
        }
    
        public String getOrderId() {
            return orderId;
        }
    
        public void setOrderId(String orderId) {
            this.orderId = orderId;
        }
    
        public Date getOrderTime() {
            return orderTime;
        }
    
        public void setOrderTime(Date orderTime) {
            this.orderTime = orderTime;
        }
    
        public Date getSku() {
            return sku;
        }
    
        public void setSku(Date sku) {
            this.sku = sku;
        }
    
        public Date getSkuName() {
            return skuName;
        }
    
        public void setSkuName(Date skuName) {
            this.skuName = skuName;
        }
    
        public BigDecimal getDecimal() {
            return decimal;
        }
    
        public void setDecimal(BigDecimal decimal) {
            this.decimal = decimal;
        }
    
        @Override
        public StringtoString() {
            return JSON.toJSONString(this);
        }
    
    }
    
  4. 查询用户内部下单数量接口

    public class OrderService {
    
        private Logger logger = LoggerFactory.getLogger(POPOrderService.class);
    
        public long queryUserOrderCount(String userId){
            logger.info("自营商家,查询用户的订单是否为首单:{}", userId);
            return 10L;
        }
    
    }
    
  5. 查询用户第三方下单首单接口

    public class POPOrderService {
    
        private Logger logger = LoggerFactory.getLogger(POPOrderService.class);
    
        public boolean isFirstOrder(String uId) {
            logger.info("POP商家,查询用户的订单是否为首单:{}", uId);
            return true;
        }
    
    }
    
  • 以上几项就是不同的MQ以及不同的接口的一个体现,后面我们将使用这样的MQ消息和接口,给它们做相应的适配。

常规实现

其实大部分时候接MQ消息都是创建⼀个类⽤于消费,通过转换它的MQ消息属性给自己的方法。

  • 目前需要接收三个MQ消息,所以就有了三个对应的类,和我们的平时的代码几乎一样。如果你的MQ量不多,这样的写法也没有什么问题,但是随着数量的增加,就需要考虑用一些设计模式来解决。

MQ接收消息实现

public class create_accountMqService {

    public void onMessage(String message) {

        create_account mq = JSON.parseObject(message, create_account.class);

        mq.getNumber();
        mq.getAccountDate();

        // ... 处理自己的业务
    }

}
  • 三组MQ的消息都是一样模拟使用。

适配器模式重构代码

### 适配器模型结构

截图2

  • 这里包括了两个类型的适配:接口适配、MQ适配。之所以不只是模拟接口适配,因为很多时候大家都很常见了,所以把适配的思想换一下到MQ消息体上,多增加大家对设计模式的认知。
  • 先是做MQ配置,接收各种各样的MQ消息。当业务发展的很快,需要发展的很快,需要对下单用户首单才给奖励,在这样的场景下再增加对接口的适配操作。

代码实现

统一的MQ消息体

public class RebateInfo {

    private String userId;  // 用户ID
    private String bizId;   // 业务ID
    private Date bizTime;   // 业务时间
    private String desc;    // 业务描述

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getBizId() {
        return bizId;
    }

    public void setBizId(String bizId) {
        this.bizId = bizId;
    }

    public Date getBizTime() {
        return bizTime;
    }

    public void setBizTime(Date bizTime) {
        this.bizTime = bizTime;
    }

    public void setBizTime(String bizTime) {
        this.bizTime = new Date(Long.parseLong("1591077840669"));
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}
  • 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体的属性的映射。
  • 最终因为我们接收到的MQ消息基本都是json格式,可以转化为MAP结构。最后使用反射调用的方式给我们的类型赋值。

测试适配类

@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");


    create_account create_account = new create_account();
    create_account.setNumber("100001");
    create_account.setAddress("河北省.廊坊市.广阳区.大学里职业技术学院");
    create_account.setAccountDate(parse);
    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(parse);

    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消息,并设置字段的映射关系。
  • 等真的业务场景开发中,就可以配这种映射关系交给配置文件或者数据库后台配置,减少编码。

result

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"}

  • 从上面可以看到,同样的字段值在做了适配前后分别有统一的字段属性,进行处理。这样业务开发中也就简单了。
  • 另外有一个非常重要的地方,在实际业务开发中,除了反射的使用外,还可以加入代理类把映射的配置交给它。这样就不需要每一个MQ都手动创建类了。

代码实现(接口使用适配)

就像我们前面提到随着业务的发展,营销活动本身要修改,不能只是接了MQ就发奖励。因为此时已经拉新的越来越多了,需要做一些限制。

因为增加了只有首单用户才给奖励,也就是你一年或者新人或者一个月的第一单才给你奖励,而不是你之前每次下单都给奖励。

那么就需要对此种方式进行限制,而此时MQ中并没有判断首单的功能。只能通过接口进行查询,而拿到的接口如下:

接口描述
OrderService.queryUserOrderCount(String userId)出参long,查询订单数量量
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"));
}

result

22:43:06.385 [main] INFO  o.i.d.design.service.POPOrderService - POP商家,查询用户的订单是否为首单:100001
判断首单,接口适配(POP):true
22:43:06.405 [main] INFO  o.i.d.design.service.POPOrderService - 自营商家,查询用户的订单是否为首单:100001
判断首单,接口适配(自营):false
  • 从测试结果上来看,此时的接口已经做了统一的包装,外部使用时候就不需要关心内部的具体逻辑了。而且在调用的时候只需要传入统一的参数即可,这样就满足了适配的作用。

总结

  • 从上文中可以看到不使用适配器模式这些功能同样可以实现,但是使用了适配器模式就可以让代码变得干净整洁易于维护,减少大量重复的判断和使用、让代码易于维护和拓展。
  • 尤其我们对MQ这样的多种消息体中不同属性同类的值,进行适配再加上代理类,就可以使用简单的配置方式接入对方提供的MQ消息,而不需要大量重复的开发,非常利于拓展。
0

评论区