个别做业务开发,不太容易有大量应用设计模式的场景。这里总结一下在业务开发中应用较为频繁的设计模式。当然语言为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/desi...