个别做业务开发,不太容易有大量应用设计模式的场景。这里总结一下在业务开发中应用较为频繁的设计模式。当然语言为 Java,基于 Spring 框架。
1 策略模式(Strategy Pattern)
一个类的行为或办法,在运行时能够依据条件的不同,有不同的策略 (行为、办法) 去执行。举个简略的例子:去下班,能够骑共享单车、能够抉择公交车、也能够乘坐地铁。这里的 乘坐什么交通工具
就是针对去下班这个行为的 策略(解决方案)
。
策略模式个别有 3 个角色:
- Context: 策略的上下文执行环境
- Strategy: 策略的形象
- ConcreteStrategy: 策略的具体实现
这个呈现的场景其实还很多。如之前做商城时遇到的登录(手机号、微信、QQ 等),及优惠券(满减券、代金券、折扣券等)。这里次要讲一下最近遇到的两种。一种是事后晓得要走哪个策略,一种是须要动静计算能力确定走哪种策略。
1.1 动态 (参数) 策略
在做增长零碎时,用户留资进线须要依据不同起源走不同的解决逻辑。而这种起源,在数据呈现时就能确定。
SyncContext
/**
* 同步上下文
*
*/
@Data
@Builder
public class SyncContext {
// 工作 ID
private Long taskId;
// 工作类型 1: 天然注册; 2: 团购用户; 3: 落地页留资
private Integer taskType;
// 所有留资相干信息(疏忽细节)
private Object reqVO;
// 存储执行策略名称(假装执行后果)
private String respVO;
}
SyncStrategy
/**
* 同步策略
*
*/
public interface SyncStrategy {
/**
* 具体策略
* @param ctx Context
*/
void process(SyncContext ctx);
}
OtSyncStrategy
/**
* 天然注册
*
*/
@Slf4j
@Service
public class OtSyncStrategy implements SyncStrategy, BeanNameAware {
private String beanName;
@Override
public void process(SyncContext ctx) {log.info("[天然注册] {}", ctx);
ctx.setRespVO(beanName);
}
@Override
public void setBeanName(String s) {beanName = s;}
}
AbSyncStrategy
/**
* 团购用户
*
*/
@Slf4j
@Service
public class AbSyncStrategy implements SyncStrategy, BeanNameAware {
private String beanName;
@Override
public void process(SyncContext ctx) {log.info("[团购用户] {}", ctx);
ctx.setRespVO(beanName);
}
@Override
public void setBeanName(String s) {beanName = s;}
}
DefaultSyncStrategy
/**
* 落地页注册(Default)
*
*/
@Slf4j
@Service
public class DefaultSyncStrategy implements SyncStrategy, BeanNameAware {
private String beanName;
@Override
public void process(SyncContext ctx) {log.info("[落地页注册] {}", ctx);
ctx.setRespVO(beanName);
}
@Override
public void setBeanName(String s) {beanName = s;}
}
至此,策略模式的三个角色已凑齐。但仿佛还有一些问题,SyncContext
中有 taskType
,然而该怎么与具体的策略匹配呢?咱们能够借助Spring
框架的依赖注入管理策略。
SyncStrategy
/**
* 同步策略
*
*/
public interface SyncStrategy {
String OT_STRATEGY = "otStrategy";
String AB_STRATEGY = "abStrategy";
String DEFAULT_STRATEGY = "defaultStrategy";
/**
* 具体策略
* @param ctx Context
*/
void process(SyncContext ctx);
}
同时批改一下具体策略,指定 @Service
别名。将 3 个具体策略类批改完即可。
OtSyncStrategy
/**
* 天然注册
*
*/
@Slf4j
@Service(SyncStrategy.OT_STRATEGY)
public class OtSyncStrategy implements SyncStrategy, BeanNameAware {
private String beanName;
@Override
public void process(SyncContext ctx) {log.info("[天然注册] {}", ctx);
ctx.setRespVO(beanName);
}
@Override
public void setBeanName(String s) {beanName = s;}
}
此时咱们仿佛还须要一个整合调用的类,否则的话就要把所有策略裸露进来。一个简略工厂即可搞定。
SyncStrategyFactory
/**
* 同步策略工厂类接口
*
*/
public interface SyncStrategyFactory {
Map<Integer, String> STRATEGY_MAP = Map.of(
1, SyncStrategy.OT_STRATEGY,
2, SyncStrategy.AB_STRATEGY,
3, SyncStrategy.DEFAULT_STRATEGY
);
/**
* 依据工作类型获取具体策略
*
* @param taskType 工作类型
* @return 具体策略
*/
SyncStrategy getStrategy(Integer taskType);
/**
* 执行策略 // XXX: 其实这块放这里有背繁多职责的,同时也不合乎 Factory 本意。*
* @param ctx 策略上下文
*/
void exec(SyncContext ctx);
}
SyncStrategyFactoryImpl
/**
* 策略工厂具体实现
*
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SyncStrategyFactoryImpl implements SyncStrategyFactory {
// 这块能够按 Spring Bean 别名注入
private final Map<String, SyncStrategy> strategyMap;
@Override
public SyncStrategy getStrategy(Integer taskType) {if (!STRATEGY_MAP.containsKey(taskType) || !strategyMap.containsKey(STRATEGY_MAP.get(taskType))) {return null;}
return strategyMap.get(STRATEGY_MAP.get(taskType));
}
@Override
public void exec(SyncContext ctx) {Optional.of(getStrategy(ctx.getTaskType())).ifPresent(strategy -> {log.info("[策略执行] 查找策略 {}, ctx=>{}", strategy.getClass().getSimpleName(), ctx);
strategy.process(ctx);
log.info("[策略执行] 执行实现 ctx=>{}", ctx);
});
}
}
至此,能够很不便的在 Spring 环境中,通过注入 SyncStrategyFactory
来调用。
最初补上单测
/**
* 策略单测
*
*/
@Slf4j
@SpringBootTest
class SyncStrategyFactoryTest {
@Autowired
SyncStrategyFactory strategyFactory;
@Test
void testOtStrategy() {final SyncContext ctx = SyncContext.builder().taskType(1).build();
strategyFactory.exec(ctx);
Assertions.assertEquals("otStrategy", ctx.getRespVO());
}
@Test
void testAbStrategy() {final SyncContext ctx = SyncContext.builder().taskType(2).build();
strategyFactory.exec(ctx);
Assertions.assertEquals("abStrategy", ctx.getRespVO());
}
@Test
void testDefaultStrategy() {final SyncContext ctx = SyncContext.builder().taskType(3).build();
strategyFactory.exec(ctx);
Assertions.assertEquals("defaultStrategy", ctx.getRespVO());
}
@Test
void testOtherStrategy() {final SyncContext ctx = SyncContext.builder().taskType(-1).build();
strategyFactory.exec(ctx);
Assertions.assertNull(ctx.getRespVO());
}
}
1.2 动静 (参数) 策略
其实在下面的策略模式中,也能够将 taskType
放到具体策略中,作为一个元数据处理。在抉择具体策略时,遍历所有策略实现类,当 taskType
与以后参数匹配时则终止遍历,由以后策略类解决。
在上述 落地页注册
中,向 CRM 同步数据时,须要校验的数据比拟多。因为不同地区落地页参数各不相同,同时有些历史落地页。
这种其实能够在 策略类
中增加校验办法,如boolean match(StrategyContext ctx)
。具体见代码
LayoutContext
/**
* 布局上下文
*
*/
@Data
@Builder
public class LayoutContext {// 落地页版本(Landing Page Version)
private String lpv;
// 国家地区
private String country;
// 渠道号
private String channel;
// 最终处理结果 拿到布局 ID
private String layoutId;
}
LayoutStrategy
/**
* 布局解决策略
*
*/
public interface LayoutStrategy {
/**
* 校验是否匹配该策略
*
* @param ctx 策略上下文
* @return bool
*/
boolean match(LayoutContext ctx);
/**
* 具体策略解决
*
* @param ctx 策略上下文
*/
void process(LayoutContext ctx);
}
具体布局解决策略
/**
* 幼儿布局
*
*/
@Slf4j
@Order(10)
@Service
public class LayoutChildStrategy implements LayoutStrategy {// 幼儿非凡渠道号(优先级最高)
private static final String CHILD_CHANNEL = "FE-XX-XX-XX";
@Override
public boolean match(LayoutContext ctx) {return Objects.nonNull(ctx) && CHILD_CHANNEL.equals(ctx.getChannel());
}
@Override
public void process(LayoutContext ctx) {log.info("[幼儿布局] 开始解决");
ctx.setLayoutId("111");
}
}
/**
* 依据 LPV 进行判断的策略
*/
@Slf4j
@Order(20)
@Service
public class LayoutLpvStrategy implements LayoutStrategy {
// 须要走 LPV 解决逻辑的渠道号
private static final Set<String> LPV_CHANNELS = Set.of("LP-XX-XX-01", "LP-XX-XX-02", "XZ-XX-XX-01", "XZ-XX-XX-02");
@Override
public boolean match(LayoutContext ctx) {return Objects.nonNull(ctx) && Objects.nonNull(ctx.getChannel()) && LPV_CHANNELS.contains(ctx.getChannel());
}
@Override
public void process(LayoutContext ctx) {log.info("[LPV 布局] 开始解决");
ctx.setLayoutId("222");
}
}
/**
* 默认解决策略
*/
@Slf4j
@Order(999)
@Service
public class LayoutDefaultStrategy implements LayoutStrategy {
@Override
public boolean match(LayoutContext ctx) {
// 兜底策略
return true;
}
@Override
public void process(LayoutContext ctx) {log.info("[默认布局] 开始解决");
ctx.setLayoutId("999");
}
}
最初,工厂类:
/**
* 布局解决工厂
*
*/
public interface LayoutProcessFactory {
/**
* 获取具体策略
*
* @param ctx 上下文
* @return Strategy
*/
Optional<LayoutStrategy> getStrategy(LayoutContext ctx);
/**
* 策略调用
*
* @param ctx 上下文
*/
void exec(LayoutContext ctx);
}
/**
* 布局解决工厂实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LayoutProcessFactoryImpl implements LayoutProcessFactory {
// Spring 会依据 @Order 注解程序注入
private final List<LayoutStrategy> strategyList;
@Override
public Optional<LayoutStrategy> getStrategy(LayoutContext ctx) {return strategyList.stream()
.filter(s -> s.match(ctx)).findFirst();}
@Override
public void exec(LayoutContext ctx) {log.info("[布局解决] 尝试解决 ctx=>{}", ctx);
getStrategy(ctx).ifPresent(s -> {s.process(ctx);
log.info("[布局解决] 解决实现 ctx=>{}", ctx);
});
}
}
最初的最初,单测:
@SpringBootTest
class LayoutProcessFactoryTest {
@Autowired
private LayoutProcessFactory processFactory;
@Test
void testChild() throws IllegalAccessException {
// 通过反射获取 Channel
final Field childChannel = ReflectionUtils.findField(LayoutChildStrategy.class, "CHILD_CHANNEL");
assertNotNull(childChannel);
childChannel.setAccessible(true); // XXX: setAccessible 后续可能会禁止这样应用
String childChannelStr = (String) childChannel.get(LayoutChildStrategy.class);
// 初始化 Context
LayoutContext ctx = LayoutContext.builder().channel(childChannelStr).build();
//
processFactory.exec(ctx);
assertEquals("111", ctx.getLayoutId());
}
@Test
void testLpv() {LayoutContext ctx = LayoutContext.builder().channel("LP-XX-XX-02").build();
processFactory.exec(ctx);
assertEquals("222", ctx.getLayoutId());
}
@Test
void testDefault() {final LayoutContext ctx = LayoutContext.builder().build();
processFactory.exec(ctx);
assertEquals("999", ctx.getLayoutId());
}
}
2 思考
策略模式能给咱们带来什么?
- 对业务逻辑进行了肯定水平的封装,将不易变和易变逻辑进行了拆散。使得后续的业务变更,仅批改相应的策略或者新增策略即可。
- 但再深层思考一下。之前易变和不易变逻辑批改代价可能相差不大,而应用设计模式之后,使得易变代码批改代价升高,但不易变代码批改代价则回升。所以在应用时要三思而后行。
- 策略模式打消了
if-else
吗?如同没有,只是把这个选择权向后移 (或者说交给调用者) 了。 - 策略让本来混淆在一个文件甚至是一个函数外面的代码,打散到数个文件中。如果每块逻辑只是简略的几行代码,应用策略反而会得失相当。还不如
if-else
或者switch
浅显易懂、高深莫测。
策略模式跟其余模式有啥区别?
- 跟
模板模式
有点像。不过模板模式次要是在父类 (下层) 对一些动作、办法做编排。而由不同子类去做具体动作、办法的实现。重点在于编排。 - 跟
桥接模式
有点像。不过桥接有多个维度的变动,策略能够认为是一维的桥接。
3 后续
本打算一篇文章将罕用的设计模式一块讲讲,贴上代码仿佛有点长,还是离开说吧。
封面图起源: https://refactoring.guru/desi…