写点什么

工作中常用的设计模式 -- 策略模式

作者:lpe234
  • 2022-12-05
    北京
  • 本文字数:5844 字

    阅读完需:约 19 分钟

工作中常用的设计模式--策略模式

一般做业务开发,不太容易有大量使用设计模式的场景。这里总结一下在业务开发中使用较为频繁的设计模式。当然语言为 Java,基于 Spring 框架。

1 策略模式(Strategy Pattern)

一个类的行为或方法,在运行时可以根据条件的不同,有不同的策略(行为、方法)去执行。举个简单的例子:去上班,可以骑共享单车、可以选择公交车、也可以乘坐地铁。这里的乘坐什么交通工具就是针对去上班这个行为的策略(解决方案)


策略模式一般有 3 个角色:


  • Context: 策略的上下文执行环境

  • Strategy: 策略的抽象

  • ConcreteStrategy: 策略的具体实现


这个出现的场景其实还很多。如之前做商城时遇到的登录(手机号、微信、QQ 等),及优惠券(满减券、代金券、折扣券等)。这里主要讲一下最近遇到的两种。一种是预先知道要走哪个策略,一种是需要动态计算才能确定走哪种策略。

1.1 静态(参数)策略

在做增长系统时,用户留资进线需要根据不同来源走不同的处理逻辑。而这种来源,在数据出现时就能确定。


SyncContext


/** * 同步上下文 * */@Data@Builderpublic 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@Servicepublic 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@Servicepublic 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@Servicepublic 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@RequiredArgsConstructorpublic 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@SpringBootTestclass 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@Builderpublic 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)@Servicepublic 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)@Servicepublic 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)@Servicepublic 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@RequiredArgsConstructorpublic 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); }); }}
复制代码


最后的最后,单测:


@SpringBootTestclass 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 思考

策略模式能给我们带来什么?


  1. 对业务逻辑进行了一定程度的封装,将不易变和易变逻辑进行了分离。使得后续的业务变更,仅修改相应的策略或者新增策略即可。

  2. 但再深层思考一下。之前易变和不易变逻辑修改代价可能相差不大,而使用设计模式之后,使得易变代码修改代价降低,但不易变代码修改代价则上升。所以在使用时要三思而后行。

  3. 策略模式消除了if-else吗?好像没有,只是把这个选择权向后移(或者说交给调用者)了。

  4. 策略让原本混杂在一个文件甚至是一个函数里面的代码,打散到数个文件中。如果每块逻辑只是简单的几行代码,使用策略反而会得不偿失。还不如if-else或者switch浅显易懂、一目了然。


策略模式跟其他模式有啥区别?


  1. 模板模式有点像。不过模板模式主要是在父类(上层)对一些动作、方法做编排。而由不同子类去做具体动作、方法的实现。重点在于编排。

  2. 桥接模式有点像。不过桥接有多个维度的变化,策略可以认为是一维的桥接。

3 后续

本打算一篇文章将常用的设计模式一块讲讲,贴上代码似乎有点长,还是分开说吧。




封面图来源: https://refactoring.guru/design-patterns/strategy




echo '5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp' | base64 -d
复制代码


发布于: 刚刚阅读数: 8
用户头像

lpe234

关注

路漫漫其修远兮 2018-07-04 加入

还未添加个人简介

评论

发布
暂无评论
工作中常用的设计模式--策略模式_Java_lpe234_InfoQ写作社区