@[toc]
结构型模式
结构型模式主要是解决如何将对象和类组装成较大的结构, 并同时保持结构的灵活和⾼效。
结构型模式包括:适配器、桥接、组合、装饰器、外观、享元、代理,这7类
概述
适配器模式的主要作⽤就是把原本不兼容的接⼝,通过适配修改做到统⼀。使得⽤户⽅便使⽤,就像我们提到转换头、出国旅游买个插座等等,都是为了适配各种不同的口 ,做的兼容。
在业务开发中我们会经常的需要做不同接⼝的兼容,尤其是中台服务,中台需要把各个业务线的各种类型服务做统⼀包装,再对外提供接⼝进⾏使⽤。⽽这在平常的开发中也是⾮常常⻅的。
Case
营销系统 接收各种各样的MQ消息或者接⼝,如果⼀个个的去开发,就会耗费很⼤的成本,同时对于后期的拓展也有⼀定的难度。
此时就会希望有⼀个系统可以配置⼀下就把外部的MQ接⼊进⾏,这些MQ就像上⾯提到的可能是⼀些注册开户消息、商品下单消息等等。
场景模拟⼯程
- 模拟了三个不同类型的MQ消息,⽽在消息体中都有⼀些必要的字段,⽐如: ⽤户ID、时间、业务ID,但是每个MQ的字段属性并不⼀样。就像⽤户ID在不同的MQ⾥也有不同的字段:uId、userId等。
- 同时还提供了两个不同类型的接⼝,⼀个⽤于查询内部订单订单下单数量,⼀个⽤于查询第三⽅是否⾸单。
- 后⾯会把这些不同类型的MQ和接⼝做适配兼容。
【注册开户MQ】
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateAccount {
/**
* 开户编号
*/
private String number;
/**
* 开户地
*/
private String address;
/**
* 开户时间
*/
private Date accountDate;
/**
* 开户描述
*/
private String desc;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
【内部订单MQ】
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderMq {
/**
* 用户ID
*/
private String uid;
/**
* 商品
*/
private String sku;
/**
* 订单ID
*/
private String orderId;
/**
* 下单时间
*/
private Date createOrderTime;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
【第三⽅订单MQ】
@Data
@NoArgsConstructor
@AllArgsConstructor
public class POPOrderDelivered {
/**
* 用户ID
*/
private String uId;
/**
* 订单号
*/
private String orderId;
/**
* 下单时间
*/
private Date orderTime;
/**
* 商品
*/
private Date sku;
/**
* 商品名称
*/
private Date skuName;
/**
* 金额
*/
private BigDecimal decimal;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
【查询⽤户内部下单数量接⼝】
public class OrderService {
private Logger logger = LoggerFactory.getLogger(POPOrderService.class);
public long queryUserOrderCount(String userId){
logger.info("自营商家,查询用户的订单是否为首单:{}", userId);
return 10L;
}
}
【查询⽤户第三⽅下单⾸单接⼝】
public class POPOrderService {
private Logger logger = LoggerFactory.getLogger(POPOrderService.class);
public boolean isFirstOrder(String uId) {
logger.info("POP商家,查询用户的订单是否为首单:{}", uId);
return true;
}
}
以上这⼏项就是不同的MQ以及不同的接⼝的⼀个体现,后⾯将使⽤这样的MQ消息和接⼝,给它们做相应的适配。
Bad Impl
其实⼤部分时候接MQ消息都是创建⼀个类⽤于消费,通过转换他的MQ消息属性给⾃⼰的⽅法。
我们接下来也是先体现⼀下这种⽅式的实现模拟,但是这样的实现有⼀个很⼤的问题就是,当MQ消息越来越多后,甚⾄⼏⼗⼏百以后,作为中台系统要怎么优化呢?
⽬前需要接收三个MQ消息,所有就有了三个对应的类,和我们平时的代码⼏乎⼀样。如果MQ量不多,这样的写法也没什么问题,但是随着数量的增加,就需要考虑⽤⼀些设计模式来解决。
public class CreateAccountMqService {
public void onMessage(String message) {
CreateAccount mq = JSON.parseObject(message, CreateAccount.class);
mq.getNumber();
mq.getAccountDate();
// ... 处理自己的业务
}
}
Better Impl (适配器模式重构代码)
接下来使⽤适配器模式来进⾏代码优化,也算是⼀次很⼩的重构。
适配器模式要解决的主要问题就是多种差异化类型的接⼝做统⼀输出。 把不同类型的消息做统⼀的处理,便于减少后续对MQ接收。如果我们接收MQ后,在配置不同的消费类时,如果不希望⼀个个开发类,那么可以使⽤代理类的⽅式进⾏处理。
【工程结构】
【适配器模型结构】
- 这⾥包括了两个类型的适配: 接⼝适配、MQ适配。之所以不只是模拟接⼝适配,因为很常⻅了,所以把适配的思想换⼀下到MQ消息体上,增加对设计模式的认知。
- 先是做MQ适配,接收各种各样的MQ消息。当业务发展的很快,需要对下单⽤户⾸单才给奖励,在这样的场景下再增加对接⼝的适配操作。
MQ消息适配
【统⼀的MQ消息体】
public class RebateInfo {
private String userId; // 用户ID
private String bizId; // 业务ID
private Date bizTime; // 业务时间
private String desc; // 业务描述
// set get
}
- 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 消息基本都是 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");
CreateAccount create_account = new CreateAccount();
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消息,并设置字段的映射关系。
- 业务场景开发中,就可以配这种映射配置关系交给配置⽂件或者数据库后台配置,减少编码。
- 从上⾯可以看到,同样的字段值在做了适配前后分别有统⼀的字段属性,进⾏处理。这样业务开发中也就⾮常简单了。
- 另外有⼀个⾮常᯿要的地⽅,在实际业务开发中,除了反射的使⽤外,还可以加⼊代理类把映射的配置交给它。这样就可以不需要每⼀个mq都⼿动创建类了。
接口适配
随着业务的发展,营销活动本身要修改,不能只是接了MQ就发奖励。因为此时已经拉新的越来越多了,需要做⼀些限制。
新需求: 只有⾸单⽤户才给奖励,那么就需要对此种⽅式进⾏限制,⽽此时MQ中并没有判断⾸单的属性。只能通过接⼝进⾏查询,⽽拿到的接⼝如下
OrderService.queryUserOrderCount(String userId)
出参long,查询订单数量OrderService.POPOrderService.isFirstOrder(String uId)
出参boolean,判断是否⾸单
两个接⼝的判断逻辑和使⽤⽅式都不同,不同的接⼝提供⽅,也有不同的出参。⼀个是直接判断是否⾸单,另外⼀个需要根据订单数量判断。
这⾥需要使⽤到适配器的模式来实现,当然如果你去编写if语句也是可以实现的,但是我们经常会提到这样的代码很难维护。
【定义统⼀适配接⼝】
public interface OrderAdapterService {
boolean isFirst(String uId);
}
后⾯的实现类都需要完成此接⼝,并把具体的逻辑包装到指定的类中,满⾜单⼀职责。
【分别实现两个不同的接⼝】
内部商品接⼝
public class InsideOrderServiceImpl 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 InsideOrderServiceImpl();
System.out.println("判断首单,接口适配(自营):" + insideOrderService.isFirst("100001"));
}
接⼝已经做了统⼀的包装,外部使⽤时候就不需要关⼼内部的具体逻辑了。⽽且在调⽤的时候只需要传⼊统⼀的参数即可,这样就满⾜了适配的作⽤。
小结
使⽤了适配器模式就可以让代码⼲净整洁易于维护、减少⼤量重复的判断和使⽤、让代码更加易于维护和拓展。尤其是对MQ这样的多种消息体中不同属性同类的值,进⾏适配再加上代理类,就可以使⽤简单的配置⽅式接⼊对⽅提供的MQ消息,⽽不需要⼤量重复的开发,⾮常利于拓展。