JAVA设计模式-策略模型
策略模型定义
Strategy Policy
通用定义:定义了一组算法,将每个算法封装起来,并使它们可以互换,策略算法允许算法独立于使用它的客户而变化。
通俗的讲,一个具体的事物可以通过多种不同的方式和行为来达到目的,因此这些相应的行为可以组合在一起,在不同的条件下进行任意的替换和使用。
比如:下单支付,最终都是扣款,但支付的方式会分为信用卡,借记卡,第三方支付等,而信用卡下面,可能有招商银行,工商银行,每个银行的信用卡支付手续费等都有差异,就可以使用策略模式进行组合。
或者,对外推送数据,不同的外部平台要求的数据规则不一样,但内部其实是同一份数据,针对外部需要的数据,我们可以按外部平台分为不同的平台策略,按不同规则封装数据进行推送。
策略模型的使用场景
三个以上的if else场景,可以使用策略模式
多个switch case场景,可以使用策略模式
领域设计模型中,认为有些非技术层面,但在业务领域的一些过程行为,是有业务价值的,而当对多个过程进行选择时,选择的复杂性和过程本身的复杂性,就会使得系统和业务耦合。因此,我们需要把容易变的部分抽取到一个单独的策略对象中。将规则和它所控制的行为区分开。按照策略模式来实现规则和过程的可替换。
这样的业务场景很多:
比如支付场景
折扣计算场景
股票选股策略
技术层面
不同的日志采用不同的策略进行记录和处理
不同的请求按不同的数据路由规则路由到内部其他交互的系统等等
总而言之,当你发现一个系统中的一些对象,它们的区别仅仅是一些行为的不同,或者一个服务需要根据条件动态匹配不同算法,或者一个对象,有多个不同的行为,都可以考虑策略模式。
策略模型的实现方式
策略模式的实现包含三个基本对象
1 策略实现类本身
StrategyA,StrategyB...
2 Stratey接口,所有的具体策略都实现这个接口
3 一个策略查找或者适配的服务类 Context或者叫StrategyAdpater都可以
我们看下具体步骤,上代码
这里我拿实践用例举例,我们有一个日志服务,专门用于日志记录,名字叫ediLog,然后针对不同的日志类型,我们要做不同的处理,
举两个日志类型:
一 下单日志类型,不同平台的订单,要记录不同的关键字段。比如字节下单,我们要把字节的相关字段转化为我们统一的订单日志记录字段。拼多多下单,同理。
二 对外推送日志,这里会包含平台要求的脱敏信息使用日志,短信回执日志等。都按不同的逻辑进行记录。
数据的消费,我们通过filebeat采集,丢同一个的日志kafak,然后日志服务消费这个消息,并进行相应的策略适配。这里由于我们知道不同的类型的数据要做什么处理,因此采用传入一个唯一的key来查找策略类的模式。
第一步定义一个日志策略接口
LogStrategy
/**
* @author TAN Fox
* 2023/1/17 14:07
* 日志服务处理的策略模型
*/
public interface EdiLogStrategy {
/**
* 根据具体配置的枚举resource,进入不同的策略
*
* @return
*/
String matchHandlerResource();
/**
* 存储具体的日志
*
* @param ediLogDTO
*/
void save(EdiLogDTO ediLogDTO);
}
第二步 添加一个枚举对象,并实现一个具体的策略类
字节订单策略具体实现
/**
* 电子面单错误报文数据
*
* @author TAN Fox
* 2023/1/17 14:07
*/
@Service
@Slf4j
public class BytedanceEbillStrategy implements EdiLogStrategy {
@Autowired
EbillNormalService ebillNormalService;
@Override
public String matchHandlerResource() {
return EdiLogResourceEnum.BYTEDANCE_EBILL.getResource();
}
@Override
public void save(EdiLogDTO ediLogDTO) {
// 进行具体策略实现,这里就是差异化的地方,也可以继续封装和加工
ebillNormalService.save(ediLogDTO);
}
}
枚举对象
/**
* @author TAN Fox
* 2023/1/17 14:07
* 日志资源枚举类,用于策略适配,各种的策略,确保资源唯一
*/
public enum EdiLogResourceEnum {
/**
* 字节电子面日志
*/
BYTEDANCE_EBILL("ebillError", "字节电子面日志"),
SMS_ERROR_LOG("smsErrorLog", "短信错误日志");
private final String resource;
private final String description;
EdiLogResourceEnum(String resource, String description) {
this.resource = resource;
this.description = description;
}
public String getResource() {
return resource;
}
public String getDescription() {
return description;
}
}
第三步 策略查找适配器
/**
* @author TAN Fox
* 2023/1/17 14:07
* 策略匹配
*/
@Component
public class EdiLogStrategyAdapter {
private final Map<String, EdiLogStrategy> ediLogStrategyMap;
/**
* 批量初始化配置的策略模型
* @param strategyList
*/
public EdiLogStrategyAdapter(List<EdiLogStrategy> strategyList) {
ediLogStrategyMap = new HashMap<>();
strategyList.forEach(strategy -> ediLogStrategyMap.put(strategy.matchHandlerResource(),strategy));
}
public EdiLogStrategy matchEdiLogStrategy(String resource) {
EdiLogStrategy strategy = ediLogStrategyMap.get(resource);
if (strategy == null) {
throw new BizException("通过resource未匹配到具体的日志策略模型:" + resource);
}
return strategy;
}
}
第四步 使用
**
*
* 日志信息消费者,并调用策略模型适配策略
*
* @author 01403533
* @date 2021/1/6 15:27
*/
@Slf4j
@Component("ediSelfLogKafKaListener")
public class EdiSelfLogKafKaListener implements IStringMessageConsumeListener {
@Autowired
EdiLogStrategyAdapter ediLogStrategyAdapter;
@Override
public void onMessage(List<String> list) {
//降级开关
if (DisConfMessageLogPropertiesReload.getEdiLogTotalSwitchOff()) {
return;
}
//处理kafka发送过来的消息
for (String logStr : list) {
try {
EdiLogDTO ediLogDTO;
ediLogDTO = JsonUtil.fromJson(logStr, EdiLogDTO.class);
EdiLogStrategy ediLogStrategy = ediLogStrategyAdapter.matchEdiLogStrategy(ediLogDTO.getResource());
ediLogStrategy.save(ediLogDTO);
log.info("ediLogDTO:{}", ediLogDTO);
} catch (Exception e) {
//单独日志文件
log.error("日志处理异常,", e);
}
}
}
}
当然这里是因为实际情况需要,我给每个策略配了一个resource来区分。你也可以有不同的方法来匹配策略,比如直接注入或者枚举类型载入都可以。
看我单元测试示范代码
/**
* @author TAN Haifeng
* 2021/11/4 15:29
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
@Profile("test")
public class IsvSumTraceSchedulerTest {
@Autowired
MonitorRuleAdapter monitorRuleAdapter;
@Test
public void testStrategy() {
assertNotNull(monitorRuleAdapter);
IsvSumTraceQueryCheckStrategy isvSumTraceQueryCheckStrategy = new IsvSumTraceQueryCheckStrategy();
MonitorRuleAdapter monitorRuleAdapter = new MonitorRuleAdapter(isvSumTraceQueryCheckStrategy);
MonitorRuleEntity monitorRuleEntity = new MonitorRuleEntity();
boolean result = monitorRuleAdapter.check(monitorRuleEntity,isvSumTraceQueryCheckStrategy); //可枚举,JobRuleMapping
boolean result2 = monitorRuleAdapter.check(monitorRuleEntity, JobRuleMapping.ISV_TRACE_QUERY_SUM);
assertFalse(result,"测试策略模型,直接存入");
assertFalse(result2,"测试测试模型,枚举类型载入");
}
}
UML类图

使用心得:
1 策略模式,可以封装策略,但具体的策略何时何地使用,由调用方自己决定
2 策略模式可以灵活扩展和新增策略方法,修改原策略方法,提供更好的灵活性
3 解决单一原则问题
4 一些通用的行为,也可以继续抽象封装,用abstract抽象
5 使用方必须理解不同策略的区别,并了解什么时候使用