部署DeepSeek模型,进群交流最in玩法!
立即加群
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >工作中常用的设计模式--策略模式

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

作者头像
lpe234
发布于 2022-11-28 07:43:57
发布于 2022-11-28 07:43:57
28820
代码可运行
举报
文章被收录于专栏:若是烟花若是烟花
运行总次数:0
代码可运行

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

1 策略模式(Strategy Pattern)

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

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

  • Context: 策略的上下文执行环境
  • Strategy: 策略的抽象
  • ConcreteStrategy: 策略的具体实现

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

1.1 静态(参数)策略

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

SyncContext

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 同步上下文
 *
 */
@Data
@Builder
public class SyncContext {
    // 任务ID
    private Long taskId;
    // 任务类型 1: 自然注册; 2: 团购用户; 3: 落地页留资
    private Integer taskType;
    // 所有留资相关信息(忽略细节)
    private Object reqVO;

	// 存储执行策略名称(伪装执行结果)
    private String respVO;
}

SyncStrategy

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 同步策略
 *
 */
public interface SyncStrategy {

    /**
     * 具体策略
     * @param ctx Context
     */
    void process(SyncContext ctx);
}

OtSyncStrategy

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 自然注册
 *
 */
@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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 团购用户
 *
 */
@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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 落地页注册(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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 同步策略
 *
 */
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 自然注册
 *
 */
@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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 同步策略工厂类接口
 *
 */
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 策略工厂具体实现
 *
 */
@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来调用。

最后补上单测

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 策略单测
 *
 */
@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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 布局上下文
 *
 */
@Data
@Builder
public class LayoutContext {
    // 落地页版本(Landing Page Version)
    private String lpv;

    // 国家地区
    private String country;
    // 渠道号
    private String channel;

    // 最终处理结果 拿到布局ID
    private String layoutId;
}

LayoutStrategy

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 布局处理策略
 *
 */
public interface LayoutStrategy {

    /**
     * 校验是否匹配该策略
     *
     * @param ctx 策略上下文
     * @return bool
     */
    boolean match(LayoutContext ctx);

    /**
     * 具体策略处理
     *
     * @param ctx 策略上下文
     */
    void process(LayoutContext ctx);
}

具体布局处理策略

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 幼儿布局
 *
 */
@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");
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 根据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");
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 默认处理策略
 */
@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");
    }
}

最后,工厂类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 布局处理工厂
 *
 */
public interface LayoutProcessFactory {

    /**
     * 获取具体策略
     *
     * @param ctx 上下文
     * @return Strategy
     */
    Optional<LayoutStrategy> getStrategy(LayoutContext ctx);

    /**
     * 策略调用
     *
     * @param ctx 上下文
     */
    void exec(LayoutContext ctx);
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 布局处理工厂实现
 */
@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);
        });
    }
}

最后的最后,单测:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 思考

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

  1. 对业务逻辑进行了一定程度的封装,将不易变和易变逻辑进行了分离。使得后续的业务变更,仅修改相应的策略或者新增策略即可。
  2. 但再深层思考一下。之前易变和不易变逻辑修改代价可能相差不大,而使用设计模式之后,使得易变代码修改代价降低,但不易变代码修改代价则上升。所以在使用时要三思而后行。
  3. 策略模式消除了if-else吗?好像没有,只是把这个选择权向后移(或者说交给调用者)了。
  4. 策略让原本混杂在一个文件甚至是一个函数里面的代码,打散到数个文件中。如果每块逻辑只是简单的几行代码,使用策略反而会得不偿失。还不如if-else或者switch浅显易懂、一目了然。

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

  1. 模板模式有点像。不过模板模式主要是在父类(上层)对一些动作、方法做编排。而由不同子类去做具体动作、方法的实现。重点在于编排。
  2. 桥接模式有点像。不过桥接有多个维度的变化,策略可以认为是一维的桥接。

3 后续

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


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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
2 条评论
热度
最新
照做弹出这个错误,是我哪里不对吗
照做弹出这个错误,是我哪里不对吗
回复回复点赞举报
Uncaught (in promise) Error: errCode: -404011 cloud function execution error | errMsg: cloud.callFunction:fail requestID , cloud function service error code -501000, error message 找不到对应的FunctionName.; at cloud.callFunction api; at new u (WAService.js:1) at d (WAService.js:1) at f (WASer
Uncaught (in promise) Error: errCode: -404011 cloud function execution error | errMsg: cloud.callFunction:fail requestID , cloud function service error code -501000, error message 找不到对应的FunctionName.; at cloud.callFunction api; at new u (WAService.js:1) at d (WAService.js:1) at f (WASer
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
几行代码获取微信绑定的手机号 小程序云开发
之前也有过说过相关教程,但不够简单,现在采取官方云调用免鉴权,着实方便,非常简单。
许坏
2020/03/26
2.9K1
【微信小程序】获取手机号码
获取微信用户绑定的手机号,需先调用wx.login接口。 因为需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 button 组件的点击来触发。
前端小tips
2021/11/28
2.1K0
【微信小程序】获取手机号码
小程序使用云开发获取手机号的实现方法
新手注意:云函数要定义在 cloudfunctions 中,且需要在 index.js 中导出。同时需要在云开发页面新增 事件名。
玖柒的小窝
2021/10/19
2.3K0
小程序使用云开发获取手机号的实现方法
微信小程序获取用户信息、获取用户手机号码
button 按钮中open-type有两种方式获取用户的信息:getUserInfo/getPhoneNumber
前端小tips
2021/12/11
2.5K0
微信小程序获取用户信息、获取用户手机号码
纯云开发获取用户微信绑定的手机号码
今天开始在写带支付版的二手书了,涉及到用户注册信息需要获取手机号,这里有两个办法,第一是购买短信接口,第二是直接小程序开放数据获取手机号。
许坏
2019/09/05
3.6K2
纯云开发获取用户微信绑定的手机号码
Thinkphp5实现小程序授权获取手机号码
使用Thinkphp框架开发接口,小程序授权获取微信用户绑定的手机号码,全网最简单的方式。
程序猿的栖息地
2022/04/29
1.4K0
Thinkphp5实现小程序授权获取手机号码
微信小程序 获取手机号 JS
本文原创首发CSDN,链接 https://blog.csdn.net/qq_41464123/article/details/105214094 ,作者博客https://blog.csdn.net/qq_41464123 ,转载请带上本段内容,尤其是脚本之家、码神岛等平台,谢谢配合。 ----
Designer 小郑
2023/08/01
1.3K0
微信小程序 获取手机号 JS
【黄啊码】PHP配合微信小程序实现获取手机号码【直接抄即可】
今天刚好做项目的时候用到这块功能,黄啊码就直接上手了,奈何网上的教程各式各样,就是没有个直接可以抄的,啊码最烦说话说一半,今天就直接弄个给大家抄的。
黄啊码
2022/09/28
9720
【黄啊码】PHP配合微信小程序实现获取手机号码【直接抄即可】
零基础学做电商小程序,手把手教学!(内有福利)
本次课程为电商小程序的基础讲解,通过该教程学习云开发的基本知识、了解小程序开发步骤、掌握云模板的基本使用。
腾讯云开发TCB
2024/07/15
2610
零基础学做电商小程序,手把手教学!(内有福利)
详解:小程序如何授权登录并获取用户绑定手机号?
微信小程序中有许多地方需要用户注册信息的地方,需要填写手机号等。下面给大家分享微信小程序获取手机号授权用户登录功能,有了这个组件可以快速获取微信绑定手机号码,无须用户填写。
极乐君
2020/08/27
16.7K1
详解:小程序如何授权登录并获取用户绑定手机号?
微信小程序+Java获取用户授权手机号码
小程序内可以直接通过授权获取用户微信号绑定的手机号码或用户添加的其他手机号码,这样可以使得小程序在进行账户的身份可控上又提高了一步,那么应该如何来获取手机号码呢?这篇文章就和大家一起来研究一下。
炒香菇的书呆子
2021/02/16
1.9K0
微信小程序+Java获取用户授权手机号码
从 0 到 1 使用云开发开发一个小程序
先上我们最终使用云开发开发的小程序,小程序码如下: 小程序名字为家物馆,主要用来管理家中物品。涉及到用户账号系统,物品管理,分类及搜索等功能,使用了云开发的云函数,数据库,存储,CMS 内容管理等能力。 一、快速开始 如果对云开发不熟悉的话,可以先按照官方文档,快速新建一个云开发的小程序用于参考。文档暂时不用看太多,小程序跑起来了就可以(说实话云开发的文档太多了,如果刚入门就想先把文档读一遍那绝对得懵圈,我们下面会对所涉及的内容给出具体的文档链接)。 由于我们要开发的小程序涉及到数据库、云函数及云存储,接
用户1097444
2022/06/29
9340
从 0 到 1 使用云开发开发一个小程序
用小程序·云开发轻松构建二手书商城小程序丨实战
使用组件开发效率会高很多,避免重复工作,同时可以参考部分组件的写法,还是有很多值得学习的地方的。
腾讯云开发TCB
2019/09/29
1.8K0
收纳控福音!从0到1用云开发制作物品管理小程序
小程序名字为家物馆,主要用来管理家中物品。涉及到用户账号系统,物品管理,分类及搜索等功能,使用了云开发的云函数,数据库,存储,CMS 内容管理等能力。
腾讯云开发TCB
2021/08/05
7500
收纳控福音!从0到1用云开发制作物品管理小程序
微信小程序开发
要求开发者有一些前端知识(HTML,CSS ,JavaScript), “工欲善其事必先利其器”,我们得先:
狂奔滴小马
2021/11/15
7.4K0
微信小程序开发
用小程序·云开发轻松构建二手书商城小程序(上)丨实战
今天是“世界读书日”,传承知识,手有余香~本文教你用小程序·云开发轻松制作二手书交易商城小程序,让智慧延续,让温暖传递。
腾讯云开发TCB
2020/06/03
1.9K0
小程序云开发全套实战教程(最全)
在学习云开发的时候将自己的学习过程记录下来了,放在了网上,收获了一波好评,今天下午在办公室没有事情,也发现之前有人在博客里面评论,你这个教程还有一半哩,可能是csdn的自动搬运功能出来一点小问题,没有搬运成功吧,这里就手动复制粘贴成为一篇了,篇幅比较长,如果有不足或者不注意写错的地方,欢迎大家提出纠正哦。
王小婷
2019/01/28
16.2K2
小程序完整demo
一个多月的时间研究和编写,做了个测试项目,由于本人的css不好,做的界面很是难看,希望不要在意。 项目地址:https://github.com/dt8888/MyselfProject/tree/m
honey缘木鱼
2018/06/13
1.6K0
腾讯云语音识别云开发微信小程序
通过录音管理器 RecorderManager调用手机的录音功能实现音频的在线采集,通过采集到的音频的base64字符串调用云开发侧实现的腾讯云一句话识别云函数,然后将识别结果回调到小程序页面中。
张世强
2020/04/26
22.1K8
腾讯云语音识别云开发微信小程序
微信小程序获取用户信息
微信小程序获取用户手机号码(类似膜拜手机号授权),自己写的程序也用到了,查看微信小程序文档,原来微信提供了方法, <button open-type="getPhoneNumber" bindget
honey缘木鱼
2018/06/13
19.1K1
推荐阅读
相关推荐
几行代码获取微信绑定的手机号 小程序云开发
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验