重学 Java 设计模式:实战适配器模式

image

做者:小傅哥
博客:https://bugstack.cnhtml

沉淀、分享、成长,让本身和他人都能有所收获!😄

1、前言

擦屁屁纸80%的面积都是保护手的!java

工做到3年左右很大一部分程序员都想提高本身的技术栈,开始尝试去阅读一些源码,例如SpringMybaitsDubbo等,但读着读着发现愈来愈难懂,一会从这过来一会跑到那去。甚至怀疑本身技术太差,慢慢也就不肯意再触碰这部分知识。程序员

而这主要的缘由是一个框架随着时间的发展,它的复杂程度是愈来愈高的,从最开始只有一个很是核心的点到最后开枝散叶。这就像你本身开发的业务代码或者某个组件同样,最开始的那部分核心代码也许只能占到20%,而其余大部分代码都是为了保证核心流程能正常运行的。因此这也是你读源码费劲的一部分缘由。数据库

框架中用到了设计模式吗?json

框架中不只用到设计模式还用了不少,并且有些时候根本不是一个模式的单独使用,而是多种设计模式的综合运用。与大部分小伙伴平时开发的CRUD可就不同了,若是都是if语句从上到下,也就算得不上什么框架了。就像你到Spring的源码中搜关键字Adapter,就会出现不少实现类,例如;UserCredentialsDataSourceAdapter。而这种设计模式就是咱们本文要介绍的适配器模式。设计模式

适配器在生活里随处可见框架

若是提到在平常生活中就不少适配器的存在你会想到什么?在没有看后文以前能够先思考下。ide

2、开发环境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三个,能够经过关注公众号bugstack虫洞栈,回复源码下载获取(打开获取的连接,找到序号18)
工程 描述
itstack-demo-design-6-00 场景模拟工程;模拟多个MQ消息体
itstack-demo-design-6-01 使用一坨代码实现业务需求
itstack-demo-design-6-02 经过设计模式优化改造代码,产生对比性从而学习

3、适配器模式介绍

适配器模式,图片来自 refactoringguru.cn

适配器模式的主要做用就是把本来不兼容的接口,经过适配修改作到统一。使得用户方便使用,就像咱们提到的万能充、数据线、MAC笔记本的转换头、出国旅游买个插座等等,他们都是为了适配各类不一样的,作的兼容。。单元测试

万能充、数据线

除了咱们生活中出现的各类适配的场景,那么在业务开发中呢?学习

在业务开发中咱们会常常的须要作不一样接口的兼容,尤为是中台服务,中台须要把各个业务线的各类类型服务作统一包装,再对外提供接口进行使用。而这在咱们日常的开发中也是很是常见的。

4、案例场景模拟

场景模拟;接收多类型MQ消息

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

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

那么这个时候作这样一个系统就会接收各类各样的MQ消息或者接口,若是一个个的去开发,就会耗费很大的成本,同时对于后期的拓展也有必定的难度。此时就会但愿有一个系统能够配置一下就把外部的MQ接入进行,这些MQ就像上面提到的多是一些注册开户消息、商品下单消息等等。

而适配器的思想方式也偏偏能够运用到这里,而且我想强调一下,适配器不仅是能够适配接口每每还能够适配一些属性信息。

1. 场景模拟工程

itstack-demo-design-6-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── mq
                │   ├── create_account.java
                │   ├── OrderMq.java
                │   └── POPOrderDelivered.java
                └── service
                    ├── OrderServicejava
                    └── POPOrderService.java
  • 这里模拟了三个不一样类型的MQ消息,而在消息体中都有一些必要的字段,好比;用户ID、时间、业务ID,可是每一个MQ的字段属性并不同。就像用户ID在不一样的MQ里也有不一样的字段:uId、userId等。
  • 同时还提供了两个不一样类型的接口,一个用于查询内部订单订单下单数量,一个用于查询第三方是否首单。
  • 后面会把这些不一样类型的MQ和接口作适配兼容。

2. 场景简述

1.1 注册开户MQ

public class create_account {

    private String number;      // 开户编号
    private String address;     // 开户地
    private Date accountDate;   // 开户时间
    private String desc;        // 开户描述

    // ... get/set     
}

1.2 内部订单MQ

public class OrderMq {

    private String uid;           // 用户ID
    private String sku;           // 商品
    private String orderId;       // 订单ID
    private Date createOrderTime; // 下单时间     

    // ... get/set      
}

1.3 第三方订单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      
}

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

public class OrderService {

    private Logger logger = LoggerFactory.getLogger(POPOrderService.class);

    public long queryUserOrderCount(String userId){
        logger.info("自营商家,查询用户的订单是否为首单:{}", userId);
        return 10L;
    }

}

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

public class POPOrderService {

    private Logger logger = LoggerFactory.getLogger(POPOrderService.class);

    public boolean isFirstOrder(String uId) {
        logger.info("POP商家,查询用户的订单是否为首单:{}", uId);
        return true;
    }

}
  • 以上这几项就是不一样的MQ以及不一样的接口的一个体现,后面咱们将使用这样的MQ消息和接口,给它们作相应的适配。

5、用一坨坨代码实现

其实大部分时候接MQ消息都是建立一个类用于消费,经过转换他的MQ消息属性给本身的方法。

咱们接下来也是先体现一下这种方式的实现模拟,可是这样的实现有一个很大的问题就是,当MQ消息愈来愈多后,甚至几十几百之后,你做为中台要怎么优化呢?

1. 工程结构

itstack-demo-design-6-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── create_accountMqService.java
                └── OrderMqService.java
                └── POPOrderDeliveredService.java
  • 目前须要接收三个MQ消息,全部就有了三个对应的类,和咱们平时的代码几乎同样。若是你的MQ量很少,这样的写法也没什么问题,可是随着数量的增长,就须要考虑用一些设计模式来解决。

2. Mq接收消息实现

public class create_accountMqService {

    public void onMessage(String message) {

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

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

        // ... 处理本身的业务
    }

}
  • 三组MQ的消息都是同样模拟使用,就不一一展现了。能够获取源码后学习。

6、适配器模式重构代码

接下来使用适配器模式来进行代码优化,也算是一次很小的重构。

适配器模式要解决的主要问题就是多种差别化类型的接口作统一输出,这在咱们学习工厂方法模式中也有所提到不一样种类的奖品处理,其实那也是适配器的应用。

在本文中咱们还会再另外体现出一个多种MQ接收,使用MQ的场景。来把不一样类型的消息作统一的处理,便于减小后续对MQ接收。

在这里若是你以前没要开发过接收MQ消息,可能听上去会有些不理解这样的场景。对此,我我的建议先了解下MQ。另外就算不了解也不要紧,不会影响对思路的体会。

再者,本文所展现的MQ兼容的核心部分,也就是处理适配不一样的类型字段。而若是咱们接收MQ后,在配置不一样的消费类时,若是不但愿一个个开发类,那么可使用代理类的方式进行处理。

1. 工程结构

itstack-demo-design-6-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │   ├── InsideOrderService.java
                │   └── POPOrderAdapterServiceImpl.java
                ├── MQAdapter,java
                ├── OrderAdapterService,java
                └── RebateInfo,java

适配器模型结构

适配器模型结构

  • 这里包括了两个类型的适配;接口适配、MQ适配。之因此不仅是模拟接口适配,由于不少时候你们都很常见了,因此把适配的思想换一下到MQ消息体上,增长你们多设计模式的认知。
  • 先是作MQ适配,接收各类各样的MQ消息。当业务发展的很快,须要对下单用户首单才给奖励,在这样的场景下再增长对接口的适配操做。

2. 代码实现(MQ消息适配)

2.1 统一的MQ消息体

public class RebateInfo {

    private String userId;  // 用户ID
    private String bizId;   // 业务ID
    private Date bizTime;   // 业务时间
    private String desc;    // 业务描述
    
    // ... get/set
}
  • MQ消息中会有多种多样的类型属性,虽然他们都有一样的值提供给使用方,可是若是都这样接入那么当MQ消息特别多时候就会很麻烦。
  • 因此在这个案例中咱们定义了通用的MQ消息体,后续把全部接入进来的消息进行统一的处理。

2.2 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结构。最后使用反射调用的方式给咱们的类型赋值。

2.3 测试适配类

2.3.1 编写单元测试类
@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消息,并设置字段的映射关系。
  • 等真的业务场景开发中,就能够配这种映射配置关系交给配置文件或者数据库后台配置,减小编码。
2.3.2 测试结果
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都手动建立类了。

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

就像咱们前面提到随着业务的发展,营销活动自己要修改,不能只是接了MQ就发奖励。由于此时已经拉新的愈来愈多了,须要作一些限制。

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

那么就须要对此种方式进行限制,而此时MQ中并无判断首单的属性。只能经过接口进行查询,而拿到的接口以下;

接口 描述
org.itstack.demo.design.service.OrderService.queryUserOrderCount(String userId) 出参long,查询订单数量
org.itstack.demo.design.service.OrderService.POPOrderService.isFirstOrder(String uId) 出参boolean,判断是否首单
  • 两个接口的判断逻辑和使用方式都不一样,不一样的接口提供方,也有不一样的出参。一个是直接判断是否首单,另一个须要根据订单数量判断。
  • 所以这里须要使用到适配器的模式来实现,固然若是你去编写if语句也是能够实现的,可是咱们常常会提到这样的代码很难维护。

3.1 定义统一适配接口

public interface OrderAdapterService {

    boolean isFirst(String uId);

}
  • 后面的实现类都须要完成此接口,并把具体的逻辑包装到指定的类中,知足单一职责。

3.2 分别实现两个不一样的接口

内部商品接口

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,以此判断是否为首单。

3.3 测试适配类

3.3.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"));
}
3.3.2 测试结果
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
  • 从测试结果上来看,此时已经的接口已经作了统一的包装,外部使用时候就不须要关心内部的具体逻辑了。并且在调用的时候只须要传入统一的参数便可,这样就知足了适配的做用。

7、总结

  • 从上文能够看到不使用适配器模式这些功能一样能够实现,可是使用了适配器模式就可让代码:干净整洁易于维护、减小大量重复的判断和使用、让代码更加易于维护和拓展。
  • 尤为是咱们对MQ这样的多种消息体中不一样属性同类的值,进行适配再加上代理类,就可使用简单的配置方式接入对方提供的MQ消息,而不须要大量重复的开发。很是利于拓展。
  • 设计模式的学习学习过程可能会在一些章节中涉及到其余设计模式的体现,只不过不会重点讲解,避免喧宾夺主。但在实际的使用中,每每不少设计模式是综合使用的,并不会单一出现。

8、推荐阅读

相关文章
相关标签/搜索