写在前面

之前搞过一个报名送油卡的活动,这个活动不定期举办,因此需要考虑可配置性,起初考虑后台做个时间配置框,根据指定的时间来生效,但是后续发现这样有个弊端,即只能针对一个活动或者相同时间的多个活动生效,而实际上我们的活动有多个,权衡再三决定使用功能开关来实现。

功能开关

功能开关其实是一种代码可配置性的实践,说到底就是通过控制开关的状态来实现对功能的决定控制。

功能开关的实现有很多种方式,可以使用MySQL或者Redis等数据库,出于对后续数据扩展和对存储容量的考量,此处使用Bit数组来实现。

Bit数组原理

既然是数组,那么下标必然从0开始,bit只有两种取值,要么为0,要么为1:

而0和1正好对应开关的关闭和启用,即ON和OFF状态。之后开发者只需定义好每个开关所在的Bit数组的索引号和状态即可,这样后续就可以通过判断开关的状态来实现对功能的控制:

可以看到使用这种方式所占用的内存空间非常少,理论上只需占用2n位的内存,n为开关的数量。

实战演示

Java中对于Bit数组可以使用BitSet来实现 ,里面有很多方法,这里我们摘几个用到的方法:

1
2
3
4
5
6
7
8
public class BitSet implements Cloneable, java.io.Serializable {
//给指定位置设置值
public void set(int bitIndex, boolean value) {}
//给指定位置的值取反操作
public void clear(int bitIndex) {}
//获取指定位置的值
public boolean get(int bitIndex) {}
}

第一步,新建开关状态常量SwitchConst:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 开关常量
*/
public class SwitchConst {
/**
* 启用
*/
public static final boolean ON = true;
/**
* 关停
*/
public static final boolean OFF = false;
}

由于开关只有开和闭这两种状态,因此可以不使用枚举类。

第二步,新建功能开关枚举类SwitchEnum,这个就是开发者所要定义的开关:

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
/**
* 功能开关枚举类
*/
public enum SwitchEnum {
/**
* HTTPS
*/
HTTPS(0,SwitchConst.ON,"HTTPS设置"),
/**
* 充值会员
*/
RECHARGE(1,SwitchConst.OFF,"充值送油卡");

/**
* 下标
*/
private int index;
/**
* 默认状态
*/
private boolean status;
/**
* 描述
*/
private String desc;

SwitchEnum(int index,boolean status,String desc){
this.index = index;
this.status = status;
this.desc = desc;
}

public Integer getIndex(){
return this.index;
}

public boolean getStatus(){
return this.status;
}

public String getDesc(){
return this.desc;
}

@Override
public String toString() {
return String.format("name=%s,desc=%s",name(),desc);
}

第三步,新建开关状态切换接口Switch,里面定义切换开关状态和判断接口状态的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 开关转态切换
*/
public interface Switch {
/**
* 启用开关
* @param switchEnum
*/
void turnOn(SwitchEnum switchEnum);

/**
* 停用开关
* @param switchEnum
*/
void turnOff(SwitchEnum switchEnum);

/**
* 判断开关状态
* @param switchEnum
*/
boolean judgeStatus(SwitchEnum switchEnum);
}

第四步,定义一个BitSetSwitch,表示基于BitSet实现的开关,因此需要实现Switch接口:

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
public enum BitSetSwitch implements Switch{
MYBITSET;

BitSetSwitch(){
init();
}

private BitSet bitSet = new BitSet();

@Override
public void turnOn(SwitchEnum switchEnum) {
bitSet.set(switchEnum.getIndex(),SwitchConst.ON);
}

@Override
public void turnOff(SwitchEnum switchEnum) {
bitSet.clear(switchEnum.getIndex());
}

@Override
public boolean judgeStatus(SwitchEnum switchEnum) {
return bitSet.get(switchEnum.getIndex());
}

private void init(){
Stream.of(SwitchEnum.values()).forEach(item->bitSet.set(item.getIndex(),item.getStatus()));
}
}

可以看到这里我们将BitSetSwitch设置为了一个枚举类,目的就是通过单例生成一个BitSetSwitch对象。注意不能将其设置为普通的类,如果那样的话,在使用的时候就需要通过new关键字生成BitSetSwitch对象,而且每调用一次就得新创建一个对象,这肯定是不行的,功能开关肯定是全局唯一的。

第五步,新建测试类SwitchTest,测试一下功能开关的使用:

1
2
3
4
5
6
7
8
9
public class SwitchTest {
public static void main(String[] args) {
BitSetSwitch bitSetSwitch = BitSetSwitch.MYBITSET;
bitSetSwitch.turnOn(SwitchEnum.HTTPS);
bitSetSwitch.turnOff(SwitchEnum.RECHARGE);
System.out.println(String.format("开关【%s】,状态为:%s",SwitchEnum.HTTPS,bitSetSwitch.judgeStatus(SwitchEnum.HTTPS)));
System.out.println(String.format("开关【%s】,状态为:%s",SwitchEnum.RECHARGE,bitSetSwitch.judgeStatus(SwitchEnum.RECHARGE)));
}
}

之后运行该方法,可以发现控制台输出如下信息:

1
2
开关【name=HTTPS,desc=HTTPS设置】,状态为:true
开关【name=RECHARGE,desc=充值送油卡】,状态为:false

这说明当前项目开启了HTTPS设置,但是关停了充值送油卡这一活动。后期要实现灵活控制,可配合配置中心在配置文件中进行控制。

小结

功能开关在实际开发过程中非常实用,因此有必要在理解的基础上进行灵活使用。