巧用二进制表示不同的状态

我们在开发的过程中会遇到这种场景,在系统的权限、选项设置,只有两种状态开启或者关闭。

具体我们以哔哩哔哩的隐私设置为例:
隐私设置

这么几种开关你会怎么设计数据表?按照正常的操作针对一个开关添加字段进行表示。可是后续就会有下面的这样的情况:

隐私设置

哈哈,产品需求迭代增加了后续几种开关,你准备怎么做?还是继续增加字段进行保存么?增加字段进行保存也是可以的,但是感觉上会有那么一点点low!新的开关出现增加字段进行保存,既要修改表结构,又要改实体属性,还是比较麻烦的!所以,对于这种类似的需求,我们要有一个良好的设计来应对,那我们如何解决呢?

用过linux系统的同学会知道这样一个知识点:

1
2
chomd 777 something
# 可读r=4,可写w=2,可执行x=1,即 二进制的001代表可执行,010代表可写,100代表可读

上面的linux命令代表把something的权限修改成所有者、用户组、其他用户都可以进行可读、可写、可执行。

数字 权限 rwx 二进制
7 读 + 写 + 执行 rwx 111
6 读 + 写 rw- 110
5 读 + 执行 r-x 101
4 只读 r– 100
3 写 + 执行 -wx 011
2 只写 -w- 010
1 只执行 –x 001
0 000

用的是二进制的001代表可执行,010代表可写,100代表可读,那么只需要3位二进制数即可表示这三种状态的混合搭配。借用这种思想,我们来解决上述的问题,我们用一个int类型字段就可以表示上述所有的状态开关。使用2的次幂值代表一种状态,比如我们用

  • 2的零次方 $2^{0}$ = 1 表示打开我的收藏

  • 2的一次方 $2^{1}$ = 2 表示打开追番追剧

  • 2的二次方 $2^{2}$ = 4 表示打开订阅标签

  • 2的三次方 $2^{3}$ = 8 表示打开最近投币的视频

  • 2的四次方 $2^{4}$ = 16 表示打开个人资料

  • 2的五次方 $2^{5}$ = 32 表示打开最近玩过的游戏

    博客使用hexo搭建,采用默认的landscape主题不支持LaTeX 公式,所以上面的次方公式显示的有问题。

二进制 数字(十进制) 含义
000 000 0 全部关闭
000 001 1 开启我的收藏
000 011 3 开启我的收藏、追番追剧
000 111 7 开启我的收藏、追番追剧、订阅标签
111 111 63 全部开启

其实就是用bit位表示开启或者关闭,如果是1表示开启,0表示关闭。

1
2
3
或运算:bit位上有1为1。例如:2 | 1 == 0000 0010 | 0000 0001 == 3
与运算:bit位都为1才为1。 例如 5 & 2 == 0000 0101 & 0000 0010 == 0
异或运算:0^0=0; 0^1=1; 1^0=1; 1^1=0;即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
那我们就来编码实现上述功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.ydstudio.flashsale.module.business.bit;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* @author 刘洋 Sam
* @date 2021/1/7
* @since 1.0.0
*/
@Getter
@AllArgsConstructor
public enum MemberOption {

FAVORITE(1, "我的收藏", 1),
FOLLOW(2, "追番追剧", 2),
SUBSCRIBE(3, "订阅标签", 4),
VIDEO(4, "最近投币视频", 8),
PROFILE(5, "个人资料", 16),
GAME(6, "最近玩的游戏", 32),;
/**
* 枚举
*/
private Integer code;

/**
* 描述
*/
private String desc;

/**
* 状态位
*/
private Integer tag;

/**
* 是否有某个tag
*
* @param tags 拥有的tag集合
* @return
*/
public boolean hasTag(int tags) {
return (tags & this.tag) == this.tag;
}

public static void main(String[] args) {
int tags = 37;
// true
System.out.println(MemberOption.FAVORITE.hasTag(tags));
// true
System.out.println(MemberOption.SUBSCRIBE.hasTag(tags));
// false
System.out.println(MemberOption.VIDEO.hasTag(tags));
// true
System.out.println(MemberOption.GAME.hasTag(tags));
}
}
开启我的收藏和最近玩的游戏,其他选项都关闭,则计算过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 我的收藏 tag = 1 最近玩的游戏 tag = 32
tags = 1 + 32 = 33
或者采用位或运算 tags = 1 | 32 = 33
/**
* 计算状态位
* tags: 已有状态位
* values: 需要添加的状态值
*/
public static int addTag(int tags, int... values) {
for (int value : values) {
tags |= value;
}
return tags;
}
关闭最近玩的游戏,则计算过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面计算的tags = 33 ,最近玩的游戏 tag = 32
关闭最近玩的游戏,
tags = 33 ^ 32 = 0010 0001 ^ 0010 0000 = 0000 0001 = 1

/**
* 移除状态位
* tags: 已有状态位
* tag: 需要移除的状态值
*/
public static int delTag(int tags, int tag) {
// tags和value与运算不等于tag,说明没有开启tag对应的选项,则直接返回tags
if ((tags & tag) != tag) return tags;
return tags ^ tag;
}
判断是否开启最近玩的游戏,则计算过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
上面计算的tags = 33 ,最近玩的游戏 tag = 32
33 & 32 = 0010 0001 & 0010 0000 = 0010 0000 = 32 (0010 0000)

/**
* 是否有某个tag
*
* @param tags 拥有的tag集合
* @param tag 待判断的tag
* @return
*/
public boolean hasTag(int tags, int tag) {
return (tags & tag) == tag;
}
如何用SQL查询开启最近玩的游戏选项的用户
1
2
# MySQL也是支持位运算的,其他的数据库应该也是支持的
select member_id from table_name where member_option & #{option} = #{option}

就这样我们就可以用一个int型字段保存4*8 = 32个状态位(无符号的情况下),看起来还是很不错,运算的效率高,只用一个字段可以保存较多的状态。缺点嘛也是有的,就是可读性差,不能很直观的看出到底有哪些状态。其实这种方法可以用一个字段表示多种状态,例如 订单一般有支付状态:待支付 1 、支付中 2 、支付成功4 、支付失败8和发货状态:待发货16、已发货32、在途64、已收货128,这样的话用一个orderStatus来表示这两种状态:支付成功4 + 待发货16 = 4 | 16 = 20,也是可以的但是如上面所说,优缺点都有,看你的实际情况进行选择了!

  • 作者: Sam
  • 发布时间: 2021-01-06 21:28:00
  • 最后更新: 2021-01-07 23:21:55
  • 文章链接: https://ydstudios.gitee.io/post/b7145215.html
  • 版权声明: 本网所有文章除特别声明外, 禁止未经授权转载,违者依法追究相关法律责任!