策略模式-应用场景
1 项目背景
背景: 我司是sass平台,有许多的用户(公司,集团为单位)。我司业务就是帮各企业提供发票管理解决方案,需要帮用户请求税局,之后将开出的发票异步通过消息队列下发至用户自己的ERP系统。此次客户有四个他们自己的erp内部系统需要接收发票,且只想接受自己想要的发票。
直接上系统交互图更直观。耐心看完,保证不失所望~
tips:此次是抽象出的部分片段,但是足够掌握策略模式了
2 不使用策略模式该怎么做?
前言:如果不采用设计模式,我们的代码该是怎样的?直接上代码。
先看一下代码整体包结构
2.1 创建mq消息队列监听类
作用:监听发票队列,一旦消息队列有变化,就会触发监听类中的逻辑代码。
前提: 搭建好rabbitmq
package com.lt.strategy.listener;
import com.alibaba.fastjson.JSONObject;
import com.lt.strategy.config.QueueNames;
import com.lt.strategy.listener.strategy.InterfaceStrategy;
import com.lt.strategy.listener.strategy.InterfaceStrategyContext;
import com.lt.strategy.listener.strategy.InterfaceStrategyFactory;
import com.lt.strategy.pojo.domain.SellerInvoice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class InvoicePushListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RabbitListener(queues = {QueueNames.SELLER_INVOICE}) // 监听的队列名,抽取到常量类中
@RabbitHandler(isDefault = true) // 监听到队列变化,执行handle注解标识的方法
public void receiveMessage(String message) {
logger.info("收到的消息为{}",message);
}
}
2.2 抽取队列名常量类
前言:sass平台队列很多,应为很多场景都要用队列异步发送消息给用户的erp系统,原因是处理用户的请求不确定何时能处理完,肯定得用异步了。因此抽取一下队列的名字常量类比较好。
package com.lt.strategy.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueueNames {
// 销项发票下发队列
public static final String SELLER_INVOICE = "seller_invoice";
}
2.3 封装发票下发方法
前言:涉及给用户四个系统下发消息,功能都是一样,只是实现方式不一样,抽取出来后面方便调用和维护。本次模拟的demo假设用户两个系统xx和yy,多了是一样的往上加就是了。
package com.lt.strategy.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class xx {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// 实际上是要封装参数发送http请求调用户系统的,这里为了示例简便,目的掌握核心思想即可
public static void pushInvoiceMsg(String msg) {
// 组装数据
// 组装http请求体
// 向xx系统发送post请求
System.out.println("将发票数据"+msg+"发送给xx系统的发票接收接口");
}
}
package com.lt.strategy.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class yy {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// 实际上是要封装参数发送http请求调用户系统的,这里为了示例简便,目的掌握核心思想即可
public static void pushInvoiceMsg(String msg) {
// 组装数据
// 组装http请求体
// 向yy系统发送post请求
System.out.println("将发票数据"+msg+"发送给yy系统的发票接收接口");
}
}
2.4 发票实体类
作用:将下发的发票信息接收为java实体类
package com.lt.strategy.pojo.domain;
import lombok.Data;
import lombok.ToString;
// 实际上有接近100来个字段,简化一下抽出几个重要的
@Data
@ToString
public class SellerInvoice {
/**
* 业务单号
*/
private String sailBillNo;
/**
* 发票号码
*/
private String invoiceNo;
/**
* 发票代码
*/
private String invoiceCode;
/**
* 系统来源
*/
private String systemOrin;
/**
* 销方名称
*/
private String sellerName;
/**
* 购方名称
*/
private String purchaseName;
}
2.5 完善消息监听类代码
package com.lt.strategy.listener;
import com.alibaba.fastjson.JSONObject;
import com.lt.strategy.common.xx;
import com.lt.strategy.common.yy;
import com.lt.strategy.config.QueueNames;
import com.lt.strategy.listener.strategy.InterfaceStrategy;
import com.lt.strategy.listener.strategy.InterfaceStrategyContext;
import com.lt.strategy.listener.strategy.InterfaceStrategyFactory;
import com.lt.strategy.pojo.domain.SellerInvoice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class InvoicePushListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RabbitListener(queues = {QueueNames.SELLER_INVOICE}) // 监听的队列名,抽取到常量类中
@RabbitHandler(isDefault = true) // 监听到队列变化,执行handle注解标识的方法
public void receiveMessage(String message) {
logger.info("收到的消息为{}",message);
try {
SellerInvoice sellerInvoice = JSONObject.parseObject(message, SellerInvoice.class);
String systemOrin = sellerInvoice.getSystemOrin();
if ("xx".equals(systemOrin)) {
// 调用抽取出来的方法将数据传给xx系统
xx.pushInvoiceMsg(message);
}else if("yy".equals(systemOrin)){
// 调用抽取出来的方法将数据传给yy系统
yy.pushInvoiceMsg(message);
}
} catch (Exception e) {
logger.warn("下发数据失败"+e);
}
logger.info("下发数据成功");
}
}
2.6 编写接口,模拟推送发票消息至消息队列
package com.lt.strategy.controller;
import com.alibaba.fastjson.JSONObject;
import com.lt.strategy.config.QueueNames;
import com.lt.strategy.mapper.CategoryMapper;
import com.lt.strategy.pojo.domain.SellerInvoice;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/test")
public class TestController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
CategoryMapper mapper;
@Autowired
RabbitTemplate rabbitTemplate;
@PostMapping ("/sendMsg")
public void test(@RequestBody SellerInvoice sellerInvoice) {
logger.info("组装的销项发票为{}",sellerInvoice);
String jsonString = JSONObject.toJSONString(sellerInvoice);
rabbitTemplate.convertAndSend(QueueNames.SELLER_INVOICE,jsonString);
}
}
2.7 测试
现在向消息队列传一条系统来源为xx,表示这张发票是xx系统想开的,我们下发应该下发到xx系统
3 不使用策略模式有何缺点?
小结:
没错,和预期一样,消息成功下发至xx系统,如果将systemOrig改为yy,同样也会下发到yy系统
但是!!!!
问题1:实际开发中,组装数据发送http请求代码多至100多行,并且,客户系统想要的数据格式不一定一样,客户系统接口可能是普通的controller,也可能是webservice
问题2:如何客户系统非常多,那该如何?写大量的if else吗?后期维护怎么办?错一处,整个下发代码逻辑全崩,后期维护交到别人手上,同样也会抱怨,这货代码写的真垃圾,改起来要疯了。
问题3: 后期维护,一个发票下发,可能会出现很多方法名,pushInvoiceMsg?sendInvoiceMsg,明明是一个功能,方法名都不一样,维护起来是不是要麻了?
4 使用策略模式改造
接下来就要体现到java设计模式之一,设计模式的奥妙之处。我们一起来改造这份代码,看看策略模式如何完虐这种场景~
4.1 抽象出策略方法,抽象成接口
package com.lt.strategy.listener.strategy;
public interface InterfaceStrategy {
/**
* 抽象出策略方法(发票信息下发)
*/
void pushInvoiceMsg(String content);
}
4.2 定义接口的实现类
package com.lt.strategy.common;
import com.lt.strategy.listener.strategy.InterfaceStrategy;
import org.springframework.stereotype.Service;
/**
* 定义抽象策略接口的实现类
*/
@Service(value = "xx") // 注入到spring,且类名为xx
public class XXImpl implements InterfaceStrategy {
@Override
public void pushInvoiceMsg(String content) {
System.out.println("将发票信息"+content+"下发给xx系统");
}
}
';
package com.lt.strategy.common;
import com.lt.strategy.listener.strategy.InterfaceStrategy;
import org.springframework.stereotype.Service;
@Service(value = "yy")// 注入到spring,且类名为yy
public class YYImpl implements InterfaceStrategy {
@Override
public void pushInvoiceMsg(String content) {
System.out.println("将发票信息"+content+"下发给yy系统");
}
}
4.3 抽象出一个策略工厂,专门获取实现类
前言: 哥们,下面这个map,能自动将spring中的刚才注入的实现类封装到里面,比如key = xx,value为xx系统的实现类,绝了,也是刚学到。value用接口接受,就可以接收所有的实现类了,这就是面向接口编程的魅力。
package com.lt.strategy.listener.strategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Map;
@Slf4j
@Component
public class InterfaceStrategyFactory {
@Autowired
private Map<String, InterfaceStrategy> interfaceStrategyMap;
public InterfaceStrategy getInterfaceStrategyBySystemCode(InterfaceStrategyContext context) {
try {
if (null == context || StringUtils.isEmpty(context.getSystemOrin())) {
log.error("I can't have a strategy, because you didn't bring me a context. context is {}", context);
throw new Exception("I can't have a strategy, because you can't bring me a context. context is " + context);
}
return interfaceStrategyMap.get(context.getSystemOrin());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4.4 抽取出一个策略上下文,将执行方法顶级抽取
package com.lt.strategy.listener.strategy;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Data
@Component
public class InterfaceStrategyContext {
public static final String PUSH_INVOICE_MSG = "pushInvoiceMsg"; // 方法名
private InterfaceStrategy interfaceStrategy;
private String systemOrin;
// 将实现类注入给该类的属性
public void selectInterfaceStrategy(InterfaceStrategy interfaceStrategy) {
this.interfaceStrategy = interfaceStrategy;
}
// 公共处理方法,根据方法名动态invoke到对应的实现类,调用对应的方法
public void handle(String methodName, String content) {
try {
Method method = interfaceStrategy.getClass().getMethod(methodName, content.getClass());
method.invoke(interfaceStrategy.getClass().newInstance(),content);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.5 完善核心部分代码
package com.lt.strategy.listener;
import com.alibaba.fastjson.JSONObject;
import com.lt.strategy.common.xx;
import com.lt.strategy.common.yy;
import com.lt.strategy.config.QueueNames;
import com.lt.strategy.listener.strategy.InterfaceStrategy;
import com.lt.strategy.listener.strategy.InterfaceStrategyContext;
import com.lt.strategy.listener.strategy.InterfaceStrategyFactory;
import com.lt.strategy.pojo.domain.SellerInvoice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class InvoicePushListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
InterfaceStrategyContext strategyContext;
@Autowired
InterfaceStrategyFactory strategyFactory;
private final String PUSH_INVOICE_MSG = "pushInvoiceMsg";
@RabbitListener(queues = {QueueNames.SELLER_INVOICE}) // 监听的队列名,抽取到常量类中
@RabbitHandler(isDefault = true) // 监听到队列变化,执行handle注解标识的方法
public void receiveMessage(String message) {
logger.info("收到的消息为{}",message);
try {
// 核心代码
SellerInvoice sellerInvoice = JSONObject.parseObject(message, SellerInvoice.class);
String systemOrin = sellerInvoice.getSystemOrin();
strategyContext.setSystemOrin(systemOrin);
InterfaceStrategy StrategyImpl = strategyFactory.getInterfaceStrategyBySystemCode(strategyContext);
strategyContext.selectInterfaceStrategy(StrategyImpl);
strategyContext.handle(PUSH_INVOICE_MSG,message);
} catch (Exception e) {
logger.warn("下发数据失败"+e);
}
logger.info("下发数据成功");
}
}
运行项目,效果和刚才完全一样
但是!!!
- 后期维护客户要加系统,我们直接加实现类,核心代码完全不用修改。
- 客户根据传递的系统来源不同,我们将实现类名字定义为系统来源即可实现核心代码部分自动匹配上对应的实现类,代码完全活起来啦,多妙啊。
4.6 测试
如果您看到这里,项目跑起来了,相信您也可以大为震撼,完结。