策略模式在多维度活动中的应用

731 阅读8分钟

前置概念:

策略模式(Strategy Pattern)是一种行为设计模式,它定义了一系列算法(策略),并将每一个算法封装起来,使它们可以相互替换,可以动态选择需要的算法。

1. 需求背景

我们要做一个活动,该活动是一个4人组队的形式参加。组队完成后,由队长报名选择相应的活动场次去参加活动。报名成功后经过区域预选赛,区域决赛,全国赛,最终决出全国冠军。

在活动列表中,无论你是不是会员,有没有组队,组队是否完成,是不是队长等,都可以看到该活动,都可以点击到活动详情中查看详情。如果你已经组队完成,且满足报名条件,活动详情底部会由立即预约的按钮,否则该按钮会动态变化(如图1)

image.png

(图1)

该按钮的动态展示逻辑流程图(如图2):

(图2)

那这种场景的按钮文案怎么处理呢?今天就来聊聊策略模式在该场景下的应用,写的不好,希望大家多给建议

2. 需求分析

2.1 场景整理

整理了一下活动涉及到的不同场景的按钮文案及事件,展示逻辑如下图:

场景按钮文案是否禁用点击事件
活动未开始,组队未开始组队参赛
活动未开始,组队进行中,未入队组队参赛去创建队伍
活动未开始,组队进行中,未满员,已入队邀请好友去自己的队伍
活动未开始,未报名,已满员, 队长立即预约预约当前活动
活动未开始,未报名,已满员, 队员等待预约
活动未开始,未报名,已满员, 不可候补, 无库存名额已满
活动未开始,未报名,已满员, 可候补, 无库存,队长候补报名预约当前活动
活动未开始,未报名,已满员, 可候补, 无库存, 队员等待候补
活动未开始,已预约,当前场次已预约
活动未开始,候补中,当前场次候补中
活动未开始,已预约,非当前场次不可重复预约
活动未开始,已候补,非当前场次不可重复候补
活动未开始,组队结束,未入队或者未满员组队已超时
活动进行中正在进行
活动已结束已结束
活动已取消已取消

2.2 涉及的维度

从上面的列表中,我们可以提取出,该案例中涉及到的维度有以下:

  • 要报名的活动相关
  • 活动是否取消
  • 活动状态(未开始/进行中/已结束)
  • 活动库存
  • 预约状态(未预约/已预约/候补预约)
  • 是否可候补(无库存时)
  • 是否当前场次(已预约/候补时)
  • 组队进程相关
  • 组队状态(组队未开始/组队进行中/组队已结束)
  • 是否入队
  • 是否队长
  • 队伍是否满员

3. 实现方式

3.1 【Bad】常规处理方式

方法1:用if,else处理

// 判断不同场景
if(!isCance) {
	// 返回按钮信息
} else if ( actStatus === 1 ) {
	if ( isHasStock ) {
		if ( isBackUp ) {
			if ( activityStep === 1 ) {
				if ( isTeamFull ) {
					...
				} else {
					// 返回按钮信息
				}
			} else if ( activityStep === 2 ) {
				// 返回按钮信息
			} else {
				// 返回按钮信息
			}
		} else {
			// 返回按钮信息
		}
	} else {
		// 返回按钮信息
	}
} else if ( actStatus === 2 ) {
	// 返回按钮信息
} else {
	// 返回按钮信息
}

用上面的方法也能实现实现这个功能,但是代码比较臃肿和难以维护,可读性差,扩展性差且不利于代码重用,总体看起来很不优雅。

方法2:用switch来处理

const {
	isCance, //活动是否取消
	actStatus, // 活动状态
	isHasStock, // 是否有库存
	isBackUp, // 是否可候补
	activityStep, // 组队活动状态
	isApplication, // 预约状态
	isCurSession, // 是否当前场次
	isTeamFull, // 队伍是否已满
	identity, // 是否队长
	isInTeam, // 是否入队

} = this.data
// 活动是否取消_活动状态_是否有库存_是否可候补_组队活动状态_预约状态_是否当前场次_队伍是否已满_是否队长_是否入队

const btnStatus = `${isCancel}_
		${actStatus}_
		${isHasStock}_
		${isBackUp }_
		${activityStep}_
		${isApplication}_
		${isCurSession}_
		${isTeamFull}_
		${identity}_
		${isInTeam}`

  switch (btnStatus){
      case '0_0_0_0_2_0_0_0_1_1':
      case '0_0_0_1_2_0_0_0_1_1':
      case '0_0_1_0_2_0_0_0_1_1':
      case '0_0_1_1_2_0_0_0_1_1':
        ...
        // 返回按钮信息
        break
     case '0_0_1_1_2_0_._0_(0|1)_1':
         // 返回按钮信息
         break
     default:
         // 返回按钮信息
         break
  }

因为switch的特性,我们只能对单一条件进行处理,想要处理多条件,我们要么通过嵌套switch,或者嵌套if else来解决,但这并不是我们想要的结果。我们选择了另一种方式,就是我们先预处理一下多条件,使它变成单一条件,然后再对它的case进行处理,但这里却有一个致命的缺点,case不支持正则匹配,只能匹配固定code,我们必须要枚举出所有的状态,这个数量级是一个指数级的,太可怕了。

由此也引出了本文想要聊的第三种方法:策略模式

3.2 【Good】应用策略模式

有了方法2的经验,我们想到了用new Map来处理。因为Map数据结构支持正则去匹配code,完美解决了code指数级数量的问题。在正则中,不需要关注的状态可以用”“.”来代替,如果只关注其中部分状态可以用类似”(1|2)”来处理,代码如下:

// 活动是否取消_活动状态_是否有库存_是否可候补_组队活动状态_预约状态_是否当前场次_队伍是否已满_是否队长_是否入队
// 活动是否取消:0-未取消,1-已取消
// 活动状态:0-未开始,1-进行中,2-已结束
// 是否有库存:0-否,1-是
// 是否可候补:0-否,1-是
// 组队活动状态: 1-组队未开始,2-组队进行中,3-组队已结束
// 预约状态:0-未预约,1-已预约, 2-后补预约
// 是否当前场次:0-否,1-是
// 是否满员:0-未满员,1-已满员
// 是否队长: 0-队员,1-队长
// 是否入队: 0-未入队,1-已入队

export const ACT_BTN_MAP = new Map([
  [
    // 活动未开始,组队进行中,未满员,已入队
    /^0_0_._._2_0_._0_(0|1)_1$/,
    param => {
      return {
        ssgBtnList: [
          {
            text: '邀请好友',
            type: 'routeTo',
            path: `/package-24-ssg/pages/team/team?teamCode=${param.teamCode}`,
            event: '',
          },
        ],
      }
    },
  ],
  // ...
  [
    // 活动已取消
    /^1_.*$/,
    param => {
      return {
        ssgBtnList: [
          {
            text: '已取消',
            disabled: true,
            event: '',
          },
        ],
      }
    },
  ]
])

在js方法里我们这样处理:

const {
	isCance, //活动是否取消
	actStatus, // 活动状态
	isHasStock, // 是否有库存
	isBackUp, // 是否可候补
	activityStep, // 组队活动状态
	isApplication, // 预约状态
	isCurSession, // 是否当前场次
	isTeamFull, // 队伍是否已满
	identity, // 是否队长
	isInTeam, // 是否入队
	teamInfo // 队伍信息
} = this.data
// 活动是否取消_活动状态_是否有库存_是否可候补_组队活动状态_预约状态_是否当前场次_队伍是否已满_是否队长_是否入队

const key = `${isCancel}_
		${actStatus}_
		${isHasStock}_
		${isBackUp }_
		${activityStep}_
		${isApplication}_
		${isCurSession}_
		${isTeamFull}_
		${identity}_
		${isInTeam}`

const curValid = [...ACT_BTN_MAP].find(([item]) => item.test(key))
const validRes = (curValid && curValid[1](teamInfo)) || {}

this.setData({
  bottomBtnList: validRes?.ssgBtnList || [],
})

这样我们就拿到了当前场景下对应的按钮相关信息,然后再把它放到html里去渲染,用事件分发的方式去处理事件,就可以比较轻松的实现,在超级复杂状态下的多场景按钮的动态处理。

另外,在map里,我们用list的方式去返回按钮,保留了对多按钮场景去扩展的可能性。

4. 优缺点分析

优点:

  • 避免使用多重条件判断
  • 策略模式通过使用多态和委托来实现不同的行为,从而避免了在客户端代码中使用大量的条件分支语句(如if-elseswitch)。
  • 扩展性良好
  • 新的策略可以很方便地添加进来,只需实现相应的接口,不需要修改现有系统的代码。
  • 提高代码的可维护性和可读性
  • 每个策略都有独立的类来实现,职责单一,符合单一职责原则,有助于代码的理解和维护。
  • 符合开闭原则
  • 增加新的策略时,无需修改现有策略和上下文,符合开闭原则(对扩展开放,对修改封闭)。

缺点:

  • 增加对象数量
  • 每个具体策略都是一个类,这会导致类的数量增加,复杂的系统可能会有许多策略类,从而增加管理和维护的难度。
  • 必须了解所有策略
  • 开发者必须知道所有的策略,并根据场景去匹配不同策略,复杂性较高。
  • 可能导致过度设计
  • 如果策略过多或策略之间差异很小,使用策略模式可能显得有些臃肿和不必要。这可能导致过度设计,使得系统变得不必要的复杂。
  • 策略没有复用性
  • 一些策略类可能不会被其他模块复用,这样的结果是代码重用性较低。

5. 总结

总的来说,策略模式是一种强大且灵活的设计模式,适用于需要动态选择算法或行为的场景。它通过将不同的算法封装成独立的类,提升了代码的可维护性和扩展性,同时避免了多重条件判断的复杂性。然而,使用策略模式也需要注意可能带来的对象数量增加和客户端复杂性上升的问题。在实际应用中,应根据具体需求权衡其优缺点,以确保设计的简洁性和高效性。

参考文档:JavaScript 复杂判断的更优雅写法