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

news2024/10/5 20:26:34

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

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

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

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

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

3 后续

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/37606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

verilog练习——基础语法

目录 基础语法 VL1 四选一多路器 VL2 异步复位的串联T触发器 VL3 奇偶校验 VL4 移位运算与乘法 VL5 位拆分与运算 VL6 多功能数据处理器 VL7 求两个数的差值 VL8 使用generate…for语句简化代码 VL9 使用子模块实现三输入数的大小比较 VL10 使用函数实现数据大小端转…

数据结构题目收录(二十五)

1、排序趟数与序列的原始状态无关的排序方法是&#xff08;&#xff09;。 Ⅰ、直接插入排序 Ⅱ、简单选择排序 Ⅲ、冒泡排序 Ⅳ、基数排序 A&#xff1a;Ⅰ、ⅢB&#xff1a;Ⅰ、Ⅱ、ⅣC&#xff1a;Ⅰ、Ⅱ、ⅢD&#xff1a;Ⅰ、Ⅳ 解析 交换类的排序&#xff0c;其趟数和…

GUI编程--PyQt5--QDiaglog

文章目录QDialogQFontDialogQColorDialogQFileDialogQInputDialogQDialog 对话框基类&#xff0c;继承QWidget&#xff1b;用于短期任务&#xff0c;分为模态、非模态 模态&#xff0c;阻塞在当前窗口&#xff1b;分为应用程序级别&窗口级别&#xff08;仅阻塞关联的窗口&…

关于使用鼠标时间mouseMove拖拽元素及元素抖动的解决方案

最近在做一个画布相关的项目时有一个场景是移动画布&#xff0c;最先开始想到的是拖拽事件&#xff0c;但是用户希望元素是实时的变化&#xff0c;所以决定使用mouseMove事件来做。 思路 通过mouseDown事件确定鼠标按下的位置根据mouseMove事件来计算出鼠标相对初始状态的横向…

MySQL锁杂谈

【说明】 1.MySQL版本5.7.37 2.事务隔离级别 REPEATABLE-READ 3.表结构 Create Table: CREATE TABLE isolation_innodb (id bigint(20) NOT NULL AUTO_INCREMENT,name varchar(10) DEFAULT NULL,money int(11) DEFAULT NULL,PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT4…

jsp库存管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 库存管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开 发&#xff0c;数据库为Mysql&#xff0c;使用ja…

VMware Cloud Director数据库操作

VMware Cloud Director 是一个云服务平台&#xff0c;以自助服务模式提供安全、隔离、弹性的虚拟数据中心计算、网络、存储和安全。在NFV整个架构中属于VIM层&#xff0c;与NFVI层&#xff08;对VMware来说即vSphere虚拟化环境&#xff09;对接获取虚拟化资源并提供给租户。 NF…

服务访问质量(QoS)——QoS技术概述与配置

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.QoS技术概述 1.QoS的应用需求 ①网络拥塞的影响&#xff1a…

【Linux】项目自动化构建工具:make/Makefile的使用

文章目录一、背景1、make和makefile是什么&#xff1f;2、为什么要使用make和makefile?二、原理1、使用make和makefile2、依赖关系和依赖方法3、具体原理4、项目清理一、背景 1、make和makefile是什么&#xff1f; make是一个命令&#xff0c;是一个解释makefile中指令的命令…

Linux项目:自主web服务器

文章目录项目描述Web与 http 发展史DNSURI URL URNHTTP 概述项目纲要项目架构套接字的封装HTTP服务启动日志信息与工具类请求、响应类读取请求读取请求行读取请求报头分析请求行分析请求报头读取正文构建响应预处理返回静态网页CGI机制CGI机制的基本概念CGI函数的实现子CGI程序…

【点云处理】点云法向量估计及其加速(4)

上篇文章【点云处理】点云法向量估计及其加速(3)介绍了如何使用pcl提供的gpu版本法向量计算接口对点云发向量计算进行加速。不足之处在于点云k近邻查找依然比较耗时&#xff0c;成为影响整体计算性能的瓶颈。这篇文章就如何优化点云K近邻查找效率进行实验。上一篇文章的示例代码…

redis数据库的下载安装/免安装版

文章目录下载方式一下载方式二免安装版redis是一款高性能的NOSQL系列的非关系型数据库这里分享三个下载源&#xff0c;只介绍免安装版下载方式一 官网下下载https://redis.io&#xff08;国外网站下载速度比较慢&#xff09; 下载方式二 Redis中文网http://www.redis.net.cn…

想你所想,华为云桌面Workspace助你轻松办公

想你所想&#xff0c;华为云桌面Workspace助你轻松办公 双11作为近年来最受关注的购物季&#xff0c;从最开始的电商&#xff0c;到现在各行各业纷纷下场推出活动&#xff0c;期望在此段时间内迅速积累用户&#xff0c;从而提升产品知名度和用户基础。华为云也不例外&#xff0…

学生个人博客网页设计作品 学生个人网页模板 个人网页制作 HTML学生个人网站作业设计

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

应用商店的ASO和搜索引擎的SEO的区别

ASO和SEO&#xff0c;目标相似&#xff0c;有着异曲同工之妙&#xff0c;两者都是提高搜索排名的方式&#xff0c;具体有什么区别呢&#xff1f;今天柚鸥ASO给大家做一下总结。 SEO是指搜索引擎优化&#xff0c;利用搜索引擎的规则来提高网站&#xff08;例如&#xff1a;百度…

室内定位解决方案-最新全套文件

室内定位解决方案-最新全套文件一、建设背景二、建设思路三、建设方案四、获取 - 室内定位全套最新解决方案合集一、建设背景 室内定位顾名思义就是定位室内目标对象的位置&#xff0c;可以是人也可以是物体的位置的一种技术方案&#xff0c;根据定位精度的不同&#xff0c;被…

C++ Primer Plus第五版笔记(p1-50)

1 在unix中 echo 获得状态 2iostream 包含istream以及ostream:随着时间的推移&#xff0c;字符是按照顺序生成或者是消耗的 3cin标准输入 cout标准输出 Cerr标准错误 clog 一般性信息 4cin>>c1>>c2&#xff1b;连续输入 5cout是ostream的对象&#xff0c;第一个<…

VirtualBox安装openEuler

下载&#xff1a; https://www.openeuler.org/zh/mirror/list/ 根据设备架构选择对应的版本&#xff1a;windows是x86架构 选择下面这个4.2G大小的&#xff1a; 安装&#xff1a; 1&#xff0c;点击新建&#xff1a; 按下图设置 设置内存大小&#xff0c;使用的cpu数…

SpringBoot项目在使用Maven打包war中遇到的问题

问题描述 在使用maven打包&#xff08;package&#xff09;springboot项目为war项目后&#xff0c;在本地机器上使用Tomcat跑这个项目&#xff0c;访问资源时出现下面的错误&#xff1a; o.s.b.w.servlet.support.ErrorPageFilter : Cannot forward to error page for reque…

TIKTOK出海公会为什么是2022出海风口?有哪些机遇与挑战?

近两年在全球迅速扩张市场的TIKTOK一举跃为全球下载量第一的APP&#xff0c;背靠十几亿月活用户的流量矿山&#xff0c;成为首个非Facebook系达成此成就的应用&#xff0c;可谓是赚足了全世界的目光。与此同时&#xff0c;大量跨境商家与自媒体从业者也盯上了TIKTOK的造富潜力&…