单一字段存储多状态思路

最近在做一个类似百度知道类的系统,针对每个回答以及问题都有可能有多重组合状态:

  • 是否过期
  • 是否重复
  • 是否优质
  • ... 这些状态有个共同的特点,就是都是 Yes Or Not,如果拍平,每个状态都用一个独立的字段进行存储,字段存储信息过于简单先不说,将来如果增加状态的话,数据库结构也需要同步修改。

正在研究怎么处理的时候,感谢同事的提点,给我了一个单一字段存储这些值的思路。经过实践后,发觉用的挺好,于是总结一下。

基本原理

首先我们已知这些状态都是 Yes Or Not,且互斥的关系,不相交。因此我们可以将这些状态都看做是一个二进制数的一位,例如过期看做是 0001,重复是 0010,优质是 0100,这样,如果数据库中字段值是 3=(0b0011),就表示既过期,又重复。 这样如果我们将来状态不止这几种,只需要扩充二进制的位数即可。

计算

我们思路有了,如何提取结果就是目前的重要问题。 即:我给一个数据库值 3=(0b0011),如何反推出是优质和过期。 这就涉及到三个按位操作符——&、|、^。

&(按位与)

逐位进行与运算 A|B|结果 -|-|- 0|0|0 0|1|0 1|0|0 1|1|1 例如: 0011 & 0001 = 0001 0010 & 0001 = 0000

|(按位或)

逐位进行与运算 A|B|结果 -|-|- 0|0|0 0|1|1 1|0|1 1|1|1 例如: 0011 | 0001 = 0011 0010 | 0001 = 0011

^(按位异或)

逐位进行与运算,相同为 0,不相同为 1 A|B|结果 -|-|- 0|0|0 0|1|1 1|0|1 1|1|0 例如: 0011 ^ 0001 = 0010 0010 & 0001 = 0000

状态值计算

有个这三个工具后,就可以进行相应值的计算了 首先**计算状态组合,用|运算**, 例如: 一个问题是重复,不过期,不优质,就用 0001 | 0000 | 0000 = 0001 = 1(int) 一个问题是重复,过期,不优质,就用 0001 | 0010 | 0000 = 0011 =3(int) 每一种组合都对应唯一的一个二进制结果,这样将结果保存后,就一个用一个字段存储所有组合了。

然后我们有了这个计算结果后,还可以进行**反推状态,这里用&操作** 例如,刚才我们的一个计算结果是 3,0b0011 我们首先用 0011 与“重复”对应的 0001 进行&操作 0011 & 0001 = 0001 ≠ 0,表示这个计算结果是包含“重复”状态的。 我们在用 0011 与“过期”对应的 0010 进行&操作 0011 & 0010 = 0010 ≠ 0,表示这个计算结果是包含“过期”状态的。 我们在用 0011 与“优质”对应的 0100 进行&操作 0011 & 0100 = 0000 = 0,表示这个计算结果是不包含“优质”状态的。 通过与相应二进制码进行&操作后结果是否等于 0,可以判断状态。

除了进行反推,还需要个清除状态,可以去掉某个状态值。这一用到^操作 例如已知 0011 是包含重复状态,去除重复的话 0011 ^ 0001 = 0010 ,0010 表示只过期状态。

一句话总结下: 添加状态用|,去掉状态用^(这里有个小坑,之后说),判断状态用&

具体代码

public enum Status {

  DEFAULT(0b00000000, "默认", "default"),
  REPEAT(0b00000001, "重复", "repeat"),
  EXPIRE(0b00000010, "过期", "expire");

  private int code;
  private String desc;
  private String property;


  public static int getMark(Status... status) {
    int init = 0;
    for (AnswerStatus s : status) {
      init = init | (s.getCode());
    }
    return init;
  }

  public static boolean status(int mark, Status status) {
    return (mark & status.getCode()) == status.getCode();//判断是否相等也行,判断是否不等于0也可以
  }

  public static void handle(Consumer<Status> consumer) {
    Arrays.stream(values()).forEach(consumer::accept);
  }
}

生成状态值时,代码如下:

//保存mark
        setMark(Status.getMark(
            answer.isRepeat() ? Status.REPEAT : Status.DEFAULT,
            answer.isExpire() ? Status.EXPIRE : Status.DEFAULT
        ))
//获取状态
        repeat(AnswerStatus.status(answer.getMark(), AnswerStatus.REPEAT))
        expire(AnswerStatus.status(answer.getMark(), AnswerStatus.EXPIRE))

这里生成状态值时,只能给出所有组合,进行保存,如果想根据当前状态与给定状态,进行组合更新,需要像下面这样调用: 例如目前,已经是 0011 了,我们给定 expire=false,需要结果更新为 0001

  1. 0011 & 0010 = 0010 ≠0,表示目前是过期,去掉过期通过^处理,0011^0010 = 0001
  2. 0001 & 0010 = 0000 =0,表示原来就不是过期,所以不需要进行操作 这种单独修改,需要判断状态的转化,所以一般分为两步: 1、获取当前状态与目标状态 2、是-》是,否-》否,不需要操作;是到否,异或方式去掉状态;否到是,用或操作,将状态加上。
//mark:{repeat:true/false,expire:true/false}
    Status.handle(element -> {
      if (mark.containsKey(element.getProperty())) {
        //需要进行调整
        if ((pojo.getMark() & element.getCode()) != 0 && !mark.getBoolean(element.getProperty())) {
          pojo.setMark(pojo.getMark() ^ (element.getCode()));
        } else if ((pojo.getMark() & element.getCode()) == 0 && mark
            .getBoolean(element.getProperty())) {
          pojo.setMark(pojo.getMark() | (element.getCode()));
        }
      }
    });

后续

这样就可以用一个字段进行存储了。其实我目前的方法不是最优解,实际上安卓系统在处理这种多状态时,已经在用这种二进制方式了,后续在研读安卓后,会尝试写一篇后续文章,优化目前的代码

← 返回文章列表