likes
comments
collection
share

【策略模式】打造前端代码的黄金分支结构:优化与实践指南

作者站长头像
站长
· 阅读数 5

引言

合理的、清晰的分支结构是维持代码扩展性的重要基石,除此之外也能显著提高代码可读性,降低出错概率。

本文将探讨优化分支结构的各种方式,并尝试给出一种最佳实践。

本文内容特针对前端领域的分支结构优化,不采用面向对象的写法。设计模式是解决开发问题的设计思想和方法论,无需执着于代码层面的具体实现。

令人发狂的复杂分支结构

  • 嵌套层级深
  • 同级分支数量多
function complexBranching(condition1: boolean, condition2: boolean, value1: number, value2: number): string {
  let result: string

  if (condition1) {
    if (value1 > 10) {
      result = 'Value 1 is greater than 10'
      if (condition2) {
        result += ' and condition 2 is true'
      } else {
        if (value2 > 20) {
          result += ' but value 2 is greater than 20'
        } else {
          result += ' and value 2 is less than or equal to 20'
        }
      }
    } else {
      result = 'Value 1 is less than or equal to 10'
      if (value2 > 5) {
        if (condition2) {
          result += ' and value 2 is greater than 5, condition 2 is true'
        } else {
          result += ' but value 2 is greater than 5, condition 2 is false'
        }
      } else {
        result += ' and value 2 is less than or equal to 5'
      }
    }
  } else {
    if (condition2) {
      result = 'Condition 1 is false but condition 2 is true'
      if (value1 + value2 > 30) {
        result += ' and the sum of value 1 and value 2 is greater than 30'
      } else {
        result += ' but the sum of value 1 and value 2 is less than or equal to 30'
      }
    } else {
      result = 'Both condition 1 and condition 2 are false'
      if (value1 === value2) {
        result += ' and value 1 is equal to value 2'
      } else {
        result += ' and value 1 is not equal to value 2'
      }
    }
  }

  return result
}

优化方案

提前return

主要目的是减少嵌套层级。

function checkAccess(user) {
  if (user) {
    if (user.role) {
      if (user.role === 'admin' || user.role === 'manager') {
        return true
      } else {
        return false
      }
    } else {
      return false
    }
  } else {
    return false
  }
}
function checkAccess(user) {
  if (!user) {
    return false
  }
  if (!user.role) {
    return false
  }
  return user.role === 'admin' || user.role === 'manager'
}

调整数据结构

主要针对分支嵌套层级深的问题,提前规律化要处理的数据结构,可能会有效地简化分支逻辑。

// 将后端返回的route信息转换为前端所需的
function mapRoute(route) {
  const newRoute = route
  // 省略转换过程
  // ...
  return newRoute
}

// 动态构建路由树
function buildRouteTree(routes) {
  const tree = []
  const mappedRoutes = new Map()

  routes.forEach((route) => {
    // 根节点
    if (!route.parentId) {
      const mappedRoute = mapRoute(route)
      tree.push(mappedRoute)
      mappedRoutes.set(mappedRoute.id, mappedRoute)
      return
    }

    // 寻找父节点
    const parent = mappedRoutes.get(route.parentId)
    if (parent) {
      if (!parent.children) {
        parent.children = []
      }
      const mappedRoute = mapRoute(route)
      parent.children.push(mappedRoute)
      mappedRoutes.set(mappedRoute.id, mappedRoute)
      return
    }

    // 如果找不到父节点,处理逻辑就比较复杂
    // 若提前将routes进行排序,让上层节点在前面,则必然存在父节点,就可有效地简化逻辑
    // ...
  })

  return tree
}

存储策略,按需匹配

此类方案的通用做法是将策略作为数据存储起来。每一个策略通常包含准入条件和执行方法两个部分。使用时将参数与策略进行匹配,检查参数是否满足策略的准入条件,若找到可用的策略则执行。

最简单的形态。

const httpErrorMap = {
  400: 'Bad Request', // 请求无效,服务器无法理解该请求
  401: 'Unauthorized', // 未经授权,请求需要用户认证
  403: 'Forbidden', // 禁止访问,服务器拒绝响应
  404: 'Not Found', // 未找到,请求的资源不存在
  408: 'Request Timeout', // 请求超时,服务器等待了过长的时间
  500: 'Internal Server Error', // 服务器内部错误,无法完成请求
  501: 'Not Implemented', // 未实现,服务器不具备完成请求的功能
  502: 'Bad Gateway', // 错误网关,服务器作为网关或代理,从上游服务器收到无效响应
  503: 'Service Unavailable', // 服务不可用,服务器目前无法处理请求
  504: 'Gateway Timeout' // 网关超时,服务器作为网关或代理,没有及时从上游服务器收到响应
}

// 使用方法
function getErrorMessage(httpStatusCode) {
  return httpErrorMap[httpStatusCode] ?? 'Unknown Error'
}

console.log(getErrorMessage(404)) // 输出: Not Found
console.log(getErrorMessage(500)) // 输出: Internal Server Error
console.log(getErrorMessage(999)) // 输出: Unknown Error (对于不常见的错误码)

上述例子中,若httpErrorMap的key是引用类型,则以上方法失效。为解决这个问题,常见的思路是使用JSON.stringify或其他方法将key转为string/number类型。但在性能、可靠性方面均非上选。这是典型的在惯性思维下想出来的方法。退一步,究其本质,key是什么,是策略准入条件的一部分,key === arg是策略真正的准入条件。上例使用半自动方式实现准入判断,限制了灵活性,出现了key不能为引用类型的问题,我们将准入条件改为纯手动操作,即可真正解决问题。

const httpErrorMap = [[(code) => code.value === 400, 'Bad Request']]

// 使用方法
function getErrorMessage(httpStatusCode) {
  const strategy = httpErrorMap.find((x) => x[0](httpStatusCode))
  if (strategy) {
    return strategy[1]
  }
  return 'Unknown Error'
}

console.log(getErrorMessage({ value: 400 })) // 输出: Bad Request
console.log(getErrorMessage({ value: 999 })) // 输出: Unknown Error (对于不常见的错误码)

部分读者可能认为这样多写了代码,没有JSON.stringify方便。请思考三个问题。第一,请问参数是引用类型的情况占多数还是少数。第二,请问即使参数是引用类型,可以直接使用JSON.stringify进行粗略的全等比较的情况是多数还是少数。第三,封装一个函数(args) => JSON.stringify(args) === value作为策略准入条件同样可减少代码量,但是否更加灵活。

使用函数定义策略准入条件的另一个优势是可以选择性地扁平化分支结构,即减少嵌套层级。

function foo(a, b) {
  if (a > 1) {
    if (b > 3) {
      return 0
    } else {
      return 1
    }
  }
  return 2
}

const strategies = [
  [(a, b) => a > 1 && b > 3, 0],
  [(a, b) => a > 1 && b <= 3, 1],
  [(a, _) => a <= 1, 2]
]

下面使用ts实现一个通用的策略调用函数。需注意:

  • 多数功能扩展场景下,新功能应与原有功能具备相同的输入输出类型。因此该函数设计为所有策略的执行函数必须具备相同的输入输出类型,同时也导致了若存在多个可用策略,只有第一个生效。
  • 该函数同时兼容以key作为策略准入条件和使用函数定义策略准入条件两种方法。
  • 默认ts类型正确,简略了校验逻辑,未在运行时检查被调用的变量是否为函数类型。
function strategy<A, B extends A, R>(
  guard: (args: A) => args is B,
  impl: (args: B) => R
): [guard: (args: A) => args is B, impl: (args: B) => R]
function strategy<A, R>(
  guard: (args: A) => boolean,
  impl: (args: A) => R
): [guard: (args: A) => boolean, impl: (args: A) => R]
function strategy<A, B extends A, R>(guard: (args: A) => boolean, impl: (args: A | B) => R) {
  return [guard, impl]
}

function useStrategies<T extends string | symbol | number, A extends T, R>(
  strategies: Record<T, (args: A) => R>,
  args: A
): R
function useStrategies<T extends string | symbol | number, A extends T, R>(
  strategies: Record<T, (args: A) => R>
): (args: A) => R
function useStrategies<A, R>(strategies: [guard: (args: A) => boolean, impl: (args: any) => R][], args: A): R
function useStrategies<A, R>(strategies: [guard: (args: A) => boolean, impl: (args: any) => R][]): (args: A) => R
function useStrategies<T extends string | symbol | number, A, R>(
  strategies: [guard: (args: A) => boolean, impl: (args: any) => R][] | Record<T, (args: A) => R>,
  args?: A
): R | ((args: A) => R) {
  if (args === undefined) {
    return (args) => useStrategies(strategies as any, args) as R
  }

  if (Array.isArray(strategies)) {
    for (const strategy of strategies) {
      if (strategy[0](args)) {
        return strategy[1](args)
      }
    }
  } else {
    const fn = strategies[args as unknown as keyof typeof strategies]
    if (fn) {
      return fn(args)
    }
  }

  throw new Error('The strategies do not fully cover all cases')
}

使用案例

const httpErrorMap = {
  400: (code: number) => code + ': Bad Request',
  401: (code: number) => code + ': Unauthorized'
}
const getErrorMessage = useStrategies(httpErrorMap)
console.log(getErrorMessage(400))

const httpErrorMap2 = [
  strategy(
    (code: number) => code === 400,
    (code: number) => code + ': Bad Request'
  ),
  strategy(
    (code: number) => code === 401,
    (code: number) => code + ': Unauthorized'
  )
]
const getErrorMessage2 = useStrategies(httpErrorMap2)
console.log(getErrorMessage2(400))

部分读者可能认为这样写多写了代码,且不如if else直观。请注意,这些例子仅为演示原理,实际操作时strategy一般独立实现,然后注册到集合中。该写法的真正的目的是抽离各策略的实现逻辑。

最佳实践

任何方法都有适用条件,换言之没有一种方法可以在任何情况下都达到最佳。分支的优化手段是多样的,方案是灵活的。

  • 对于简单分支或复杂分支中的一部分,考虑使用提前return减少嵌套层级。
  • 对于复杂分支,考虑预先规律化要处理的数据结构。
  • 对单个策略实现逻辑复杂的情况,考虑封装函数抽离该策略的实现。
  • 对于策略实现逻辑复杂且分支数量多的情况,考虑使用存储策略并按需匹配的方式抽离策略实现逻辑。

灵活应用数据结构和算法,封装更多适用于特殊情况的策略调用方法。

  • 若策略存在优先级,或可考虑使用堆。
  • 若需执行所有匹配的策略且策略之间存在依赖关系,或可考虑拓扑排序。