【TypeScript 学习】TypeScript 枚举类型发散:基于位运算的权限管理 CRUD 操作

文章目录

  • [TypeScript 枚举类型发散:基于位运算的权限管理 CRUD 操作](#TypeScript 枚举类型发散:基于位运算的权限管理 CRUD 操作)
    • [1 问题由来](#1 问题由来)
    • [2 具体实现](#2 具体实现)
      • [2.1 新增权限](#2.1 新增权限)
      • [2.2 删除权限](#2.2 删除权限)
      • [2.3 查询权限(即判定存在与否)](#2.3 查询权限(即判定存在与否))
      • [2.4 修改权限](#2.4 修改权限)
      • [2.5 完整测试](#2.5 完整测试)
    • [3 小结](#3 小结)

TypeScript 枚举类型发散:基于位运算的权限管理 CRUD 操作

1 问题由来

TypeScript 枚举类型常用于约束一组固定的选项(如星期、月份)、表示几种可选的状态(如用户角色、订单状态)、或者替换一些难写的取值(如常用颜色的十六进制值等)。在这些应用场景中,有一类场景在开发后台管理系统时尤为常见------用户权限管理。假设有三种权限:可读(Readable)、可写(Writable)、可执行(Executable),则可以用 TypeScript 枚举类型表示为:

ts 复制代码
enum Permission = {
    Readable   = 0b001,   // i.e. 2^0 = 1
    Writable   = 0b010,   // i.e. 2^1 = 2
    Executable = 0b100,   // i.e. 2^2 = 4 
}

于是可以类比 Lunix 中的权限管理,结合位运算实现权限的基本操作(增、删、改、查,即 CRUD)。

2 具体实现

准备工作就是上面定义好的 TS 枚举类型 Permission

2.1 新增权限

新增可通过 位或(| 运算实现:

ts 复制代码
/**
 * Add a permission to the permission set
 * 
 * @param perms original permission set
 * @param permToAdd permission to add
 * @returns the new permission set
 */
function addPermission(perms: Permission, permToAdd: Permission): Permission {
  return perms | permToAdd;
}

相当于求并集。

2.2 删除权限

既然有新增,自然就有删除。通常有两种写法:

  • 位与(& + 位非(~p = p1 & ~p2
  • 位异或(^p = p1 ^ p2

我觉得第二个更简洁,于是实现为:

ts 复制代码
/**
 * Delete a permission from the permission set
 * 
 * @param perms original permission set
 * @param permToDel permission to delete
 * @returns the new permission set
 */
function delPermission(perms: Permission, permToDel: Permission): Permission {
  return perms ^ permToDel;
}

2.3 查询权限(即判定存在与否)

所谓查询,就是判断当前权限集合 perms 是否包含某个权限(或权限组合)permTarget。这可以通过 位与 运算实现:

ts 复制代码
/**
 * Check if the permission set has the target permission
 * 
 * @param perms original permission set
 * @param permTarget target permission to check
 * @returns true if the permission set has the target permission; otherwise false
 */
function hasPermission(perms: Permission, permTarget: Permission): boolean {
  return (perms & permTarget) === permTarget;
}

2.4 修改权限

实际工作中,权限的修改无非是将原来的权限组合 重新替换成 前端传来的新组合。这里为了练习位运算,稍微做了一下调整:令目标函数接收三个参数:当前权限、待删权限、待增权限。也就是说,把权限的 修改 看成是 删除添加 的组合操作,先删后加。

v1.0 版实现如下:

ts 复制代码
function updatePermission(perms: Permission, permToDel: Permission, permToAdd: Permission): Permission {
  return (perms ^ permToDel) | permToAdd;
}

实践 DRY 原则(Don't Repeat Yourself),复用前面的两个方法 delPermissionaddPermission,变成 v2.0 版实现:

ts 复制代码
function updatePermission(perms: Permission, permToDel: Permission, permToAdd: Permission): Permission {
  return addPermission(delPermission(perms, permToDel), permToAdd)
}

结果一测就出问题了:

ts 复制代码
let perms = addPermission(Permission.Readable, Permission.Executable);  // 101
const pDel = addPermission(Permission.Readable, Permission.Writable);  // 011
const pAdd = Permission.Executable;  // 100
perms = updatePermission(perms, pDel, pAdd);
console.assert(perms === 0b100, `updatePermission failed: \n  Expected '0b100', but got '0b${perms.toString(2)}' instead.`);

/* => 
Assertion failed: updatePermission failed:
  Expected '0b100', but got '0b110' instead.
*/

原因说来也简单:删除的时候需要先判定一下,不然本来没有的权限,也可能因为位异或操作而被意外引入。于是有了第 3 版实现:

ts 复制代码
function updatePermission(perms: Permission, permToDel: Permission, permToAdd: Permission): Permission {
  // return addPermission(delPermission(perms, permToDel), permToAdd);
  const permDelReal = perms & permToDel;
  if(hasPermission(perms, permDelReal)) {
    const permPreAdd = delPermission(perms, permDelReal);
    return addPermission(permPreAdd, permToAdd);
  } else {
    return addPermission(perms, permToAdd);
  }
}

注意到第 11 行,单独的 位与 运算其实是类似求交集的效果(同真为真,其余为假),并且与查询逻辑的部分代码重复了。这说明 位与 运算也是一个原子操作,应该单独提出来:

ts 复制代码
/**
 * Get the shared permission between two permission sets
 * 
 * @param perm1 the 1st permission set
 * @param perm2 the 2nd permission set
 * @returns the shared permission
 */
function sharedPermission(perm1: Permission, perm2: Permission): Permission {
  return perm1 & perm2;
}

于是再次重构 updatePermission 函数,有了第 4 版实现:

ts 复制代码
/**
 * Update the permission set by adding and deleting permissions
 * 
 * @param perms original permission set
 * @param permToDel permission to delete
 * @param permToAdd permission to add
 * @returns the new permission set
 */
function updatePermission(perms: Permission, permToDel: Permission, permToAdd: Permission): number {
  const permDelReal = sharedPermission(perms, permToDel);
  if(hasPermission(perms, permDelReal)) {
    const permPreAdd = delPermission(perms, permDelReal);
    return addPermission(permPreAdd, permToAdd);
  } else {
    return addPermission(perms, permToAdd);
  }
}

大功告成。

2.5 完整测试

搞定了 CRUD 操作,最后再完整测试一遍:

ts 复制代码
// Test cases
let perms = addPermission(Permission.Readable, Permission.Writable);
console.log('perms:', perms.toString(2));  // 011

// Test permission deletion
perms = delPermission(perms, Permission.Writable);
console.log('permsAfterDel:', perms.toString(2));  // 001

// Test permission addition
perms = addPermission(perms, Permission.Executable);
console.log('permsAfterAdd:', perms.toString(2));  // 101

// Test shared permission
console.log('has readable:', hasPermission(perms, Permission.Readable));  // true
console.log('has writable:', hasPermission(perms, Permission.Writable));  // false
console.log('has executable:', hasPermission(perms, Permission.Executable));  // true

// Test permission update
console.log('Before update, perms =', perms.toString(2));               // 101 (i.e. 5)

const pDel = addPermission(Permission.Readable, Permission.Writable);  // 011
const pAdd = addPermission(Permission.Executable, Permission.Executable);  // 100
console.log('perms to delete =', pDel.toString(2));  // 011 (i.e. 3)
console.log('perms to add =', pAdd.toString(2));  // 110 (i.e. 6)

perms = updatePermission(perms, pDel, pAdd);
console.assert(perms === 0b100, `updatePermission failed: \nExpected '0b100', but got '0b${perms.toString(2)}'`);

console.log('After update perms =', perms.toString(2));  // 110

实测结果如下:

3 小结

  • TypeScript 的枚举类型非常适合做权限管理;

  • 借助 TypeScript 枚举类型(enum)和基本的位运算(|&^<<~ 等),可轻松实现权限的 CRUD 操作(修改操作勉强也算轻松吧);

  • 代码重构过程中,应该将通用的、原子级的操作抽出来,以便复用;

  • 逻辑或流程设计得再完备,都不可忽视测试环节;信任不能代替监督;

  • 也可以将权限修改的部分逻辑并入删除方法中(随个人喜好或特定需求);

  • 分离出所有原子操作后,就可以在此基础上组合出更多类似修改这样的接口方法(函数式编程的基础);

  • 定义枚举值阶段,还可以使用 左移 运算符 <<,方便赋值:

    ts 复制代码
    enum Permission {
      Readable   = 1 << 0, // 0b001,  // i.e. 2^0 = 1
      Writable   = 1 << 1, // 0b010,  // i.e. 2^1 = 2
      Executable = 1 << 2, // 0b100,  // i.e. 2^2 = 4
    }
    // or refactor into a initialization function
    /**
     * Initialize a permission value with left-shift operation
     * @param order the order of the permission
     * @returns the permission value
     */
    function initPermission(order: number = 0): number | never {
      if (order < 0) throw new Error("Order must be a non-negative integer");
      if (order > 10) throw new Error("Order must be no greater than 10");
      return 1 << order;
    }
    enum PermissionNew {
      Readable   = initPermission(),  // 0b001,  // i.e. 2^0 = 1
      Writable   = initPermission(1), // 0b010,  // i.e. 2^1 = 2
      Executable = initPermission(2), // 0b100,  // i.e. 2^2 = 4
    }
  • 完整代码已上传 Gitee,欢迎交流。

相关推荐
齐尹秦10 分钟前
HTML 音频(Audio)学习笔记
学习
木木黄木木11 分钟前
HTML5图片裁剪工具实现详解
前端·html·html5
念九_ysl13 分钟前
基数排序算法解析与TypeScript实现
前端·算法·typescript·排序算法
海石13 分钟前
vue2升级vue3踩坑——【依赖注入】可能成功了,但【依赖注入】成功了不太可能
前端·vue.js·响应式设计
uhakadotcom25 分钟前
Vite 与传统 Bundler(如 Webpack)在 Node.js 应用的性能对比
前端·javascript·面试
瞌睡不来31 分钟前
(学习总结32)Linux 基础 IO
linux·学习·io
Moonnnn.36 分钟前
运算放大器(四)滤波电路(滤波器)
笔记·学习·硬件工程
uhakadotcom37 分钟前
Socket.IO 简明教程:实时通信的基础知识
前端·javascript·面试
weixin_4578858242 分钟前
JavaScript智能对话机器人——企业知识库自动化
开发语言·javascript·自动化