结构型模式包括:适配器、桥接、组合、装饰器、外观、享元、代理

这类模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。

第一节、适配器模式

一、适配器模式介绍

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

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

二、案例场景模拟


随着公司的业务的不断发展,当基础的系统逐步成型以后。业务运营就需要开始做⽤户的拉新和促活,从⽽保障 DUA(日活跃用户数量) 的增速以及最终 ROI(投资回报率) 转换。

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

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

1.场景模拟工程

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

2.场景简述

1.1注册开户MQ
public class create_account {
private String number;
private String address;
private Date accountDate;
private String desc;

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = 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;
}

@Override
public String toString() {
return JSON.toJSONString(this);
}

public void setAccountDate(Date accountDate) {
this.accountDate = accountDate;
}
}
1.2内部订单MQ
public class OrderMq {
private String uid;
private String sku;
private String orderId;
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);
}
}
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; // 金额

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 String toString() {
return JSON.toJSONString(this);
}
}
1.4查询用户内部下单数量接口
public class OrderService {
private Logger logger = LoggerFactory.getLogger(OrderService.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;
}
}

三、直接实现

1.工程结构

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的消息一样模拟使用,这里不再赘述。

四、适配器模式重构代码

适配器模式要解决的主要问题就是多种差异化类型的接⼝做统⼀输出,这在我们学习⼯⼚⽅法模式中也有所提到不同种类的奖品处理,其实那也是适配器的应⽤。

在本⽂中我们还会再另外体现出⼀个多种MQ接收,使⽤MQ的场景。来把不同类型的消息做统⼀的处理,便于减少后续对MQ接收。

1.工程结构


适配器模型结构

  1. 这里包括了两个类型的适配:接口适配、MQ适配。
  2. 先做MQ适配,接收各种MQ信息。

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

2.1 统一的MQ消息体
public class RebateInfo {
private String userId;
private String bizId;
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(String bizTime) {
this.bizTime = new Date(Long.parseLong("1591077840669"));
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}
}

通用的MQ消息体,后续只需要将传入的值与统一的消息体做适配。

2.2 MQ消息体适配类
public class MQAdapter {
// 这个方法接受一个JSON字符串和一个映射关系的Map作为参数。它使用 JSON.parseObject 方法将JSON字符串解析为一个Map对象,并将该Map对象和映射关系传递给下一个 filter 方法进行处理。
public static RebateInfo filter(String strJson, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return filter(JSON.parseObject(strJson, Map.class), link);
}

//这个方法接受一个Map对象和一个映射关系的Map作为参数。它创建一个 RebateInfo 对象,并根据映射关系设置 RebateInfo 对象的属性。它通过反射调用 RebateInfo 类的相应的setter方法来设置属性值。
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;
}
}
  1. 这个类的方法十分重要,主要作用是把不同类型的MQ的各种属性,映射成我们需要的属性并返回。
  2. ⽽在这个处理过程中需要把映射管理传递给 Map<String, String> link ,也就是准确的描述了,当前MQ中某个属性名称,映射为我们的某个属性名称。
  3. 最终因为我们接收到的 mq 消息基本都是 json 格式,可以转换为MAP结构。最后使⽤反射调⽤的⽅式给我们的类型赋值。
2.3 测试适配类
2.3.1 编写单元测试类
public class ApiTest {
@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));
}
}
  1. 在这⾥我们分别模拟传⼊了两个不同的MQ消息,并设置字段的映射关系。
  2. 等真的业务场景开发中,就可以配这种映射配置关系交给配置⽂件或者数据库后台配置,减少编码。
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"}
  1. 从上面可以看到,同样的字段值在做了适配前后分别有统一的字段属性,进行处理。
  2. 在实际业务开发中,除了反射的使用外,还可以加入代理类把映射的配置交给它。这样就不需要每一个mq都手动创建类了。

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

上述业务中的判断是否首单

接口 描述
org.levtio.demo.design.service.OrderService 出参Long,查询订单数量
org.levtio.demo.design.service.POPOrderService 出参Boolean,查询订单是否首单
  1. 这两个接口的判断逻辑和使用方式都不相同,不同的接口提供方,也有不同的出参。一个是直接判断是否首单,另外一个需要根据订单数量判断。
  2. 因此这里需要使用适配器去实现
3.1 定义统一适配接口
public interface OrderAdapterService {
boolean isFirst(String uId);
}

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

3.2 分别实现两个不同的接口

内部商品接口

public class InsideOrderServiceImpl implements OrderAdapterService {
private OrderService orderService = new OrderService();
@Override
public boolean isFirst(String uId) {
return orderService.queryUserOrderCount(uId) <= 1;
}
}

第三方商品接口

public class POPOrderAdapterServiceImpl implements OrderAdapterService {
private POPOrderService popOrderService = new POPOrderService();

@Override
public boolean isFirst(String uId) {
return popOrderService.isFirstOrder(uId);
}

}

在这两个接⼝中都实现了各⾃的判断⽅式,尤其像是提供订单数量的接⼝,需要⾃⼰判断当前接到mq时订单数量是否 <= 1 ,以此判断是否为⾸单。

3.3 测试适配类
3.3.1 编写单元测试类
public class ApiTest {
@Test
public void test_itfAdapter() {
OrderAdapterService popOrderAdapterService = new POPOrderAdapterServiceImpl();
System.out.println("判断首单,接口适配(POP):" + popOrderAdapterService.isFirst("100001"));

OrderAdapterService insideOrderService = new InsideOrderServiceImpl();
System.out.println("判断首单,接口适配(自营):" + insideOrderService.isFirst("100001"));
}
}
3.3.2 测试结果
17:37:40.639 [main] INFO  o.l.d.design.service.POPOrderService - POP商家,查询用户的订单是否为首单:100001
判断首单,接口适配(POP):true
17:37:40.642 [main] INFO o.l.demo.design.service.OrderService - 自营商家,查询用户的订单是否为首单:100001
判断首单,接口适配(自营):false

从测试结果上来看,此时已经的接⼝已经做了统⼀的包装,外部使⽤时候就不需要关⼼内部的具体逻辑了。⽽且在调⽤的时候只需要传⼊统⼀的参数即可,这样就满⾜了适配的作⽤

五、总结

  1. 适配器模式可以让代码:干净整洁易于维护、减少大量重复的判断和使用、让那个代码更加易于维护和拓展。
  2. 对MQ这种的多种消息体中不同属性同类的值,进行适配再加上代理类,就可以使用简单的配置方式接入对方提供的MQ信息,而不需要大量重复的开发。