摘要
JDK:1.8.0_202
# 一:场景问题
# 1.1 实现在线投票
考虑一个在线投票的应用,要实现控制同一个用户只能投一票,如果一个用户反复投票,而且投票次数超过5次,则判定为恶意刷票,要取消该用户投票的资格,当然同时也要取消他所投的票。如果一个用户的投票次数超过8次,将进入黑名单,禁止再登录和使用系统。
该怎么实现这样的功能呢?
# 1.2 不用模式的解决方案
分析上面的功能,为了控制用户投票,需要记录用户所投票的记录,同时还要记录用户投票的次数,为了简单,直接使用两个Map来记录。
在投票的过程中,又有四种情况:
一是用户是正常投票;
二是用户正常投票过后,有意或者无意的重复投票;
三是用户恶意投票;
四是黑名单用户;
这几种情况下对应的处理是不一样的。
/**
* 投票管理
*/
public class VoteManager {
/**
* 记录用户投票的结果,Map<String,String>对应Map<用户名称,投票的选项>
*/
private Map<String, String> mapVote = new HashMap<>();
/**
* 记录用户投票次数,Map<String,Integer>对应Map<用户名称,投票的次数>
*/
private Map<String, Integer> mapVoteCount = new HashMap<>();
/**
* 投票
*
* @param user 投票人,为了简单,就是用户名称
* @param voteItem 投票的选项
*/
public void vote(String user, String voteItem) {
// 1:先为该用户增加投票的次数
// 先从记录中取出已有的投票次数
Integer oldVoteCount = mapVoteCount.get(user);
if (oldVoteCount == null) {
oldVoteCount = 0;
}
oldVoteCount = oldVoteCount + 1;
mapVoteCount.put(user, oldVoteCount);
// 2:判断该用户投票的类型,到底是正常投票、重复投票、恶意投票
// 还是上黑名单,然后根据投票类型来进行相应的操作
if (oldVoteCount == 1) {
// 正常投票
// 记录到投票记录中
mapVote.put(user, voteItem);
System.out.println("恭喜你投票成功");
} else if (oldVoteCount > 1 && oldVoteCount < 5) {
// 重复投票
// 暂时不做处理
System.out.println("请不要重复投票");
} else if (oldVoteCount >= 5 && oldVoteCount < 8) {
// 恶意投票
// 取消用户的投票资格,并取消投票记录
String s = mapVote.get(user);
if (s != null) {
mapVote.remove(user);
}
System.out.println("你有恶意刷票行为,取消投票资格");
} else if (oldVoteCount >= 8) {
// 黑名单
// 记入黑名单中,禁止登录系统了
System.out.println("进入黑名单,将禁止登录和使用本系统");
}
}
}
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
客户端:
public class Client {
public static void main(String[] args) {
VoteManager vm = new VoteManager();
for (int i = 0; i < 8; i++) {
vm.vote("u1", "A");
}
}
}
2
3
4
5
6
7
8
运行结果:
# 1.3 有何问题
看起来很简单,是不是?幸亏这里只是示意,否则,你想想,在vote()方法中那么多判断,还有每个判断对应的功能处理都放在一起,是不是有点太杂乱了,那简直就是个大杂烩,如果把每个功能都完整的实现出来,那vote()方法会很长的。
一个问题是:如果现在要修改某种投票情况所对应的具体功能处理,那就需要在那个大杂烩里面,找到相应的代码块,然后进行改动。
另外一个问题是:如果要添加新的功能,比如投票超过8次但不足10次的,给个机会,只是禁止登录和使用系统3天,如果再犯,才永久封掉账号,该怎么办呢?那就需要改动投票管理的源代码,在上面的if-else结构中再添加一个else if块进行处理。
不管哪一种情况,都是在一大堆的控制代码里面找出需要的部分,然后进行修改,这从来都不是好方法,那么该如何实现才能做到,既能够很容易的给vote()方法添加新的功能,又能够很方便的修改已有的功能处理呢?
# 二:解决方案
用来解决上述问题的一个合理的解决方案就是状态模式。
状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看来起似乎修改了它的类。
# 2.1 解决思路
仔细分析上面的问题,会发现,那几种用户投票的类型,就相当于是描述了人员的几种投票状态,而各个状态和对应的功能处理具有很强的对应性,有点类似于 "一个萝卜一个坑",各个状态下的处理基本上都是不一样的,也不存在可以相互替换的可能。
为了解决上面提出的问题,很自然的一个设计就是 首先把状态和状态对应的行为从原来的大杂烩代码中分离出来,把每个状态所对应的功能处理封装在一个独立的类里面,这样选择不同处理的时候,其实就是在选择不同的状态处理类。
然后为了统一操作这些不同的状态类,定义一个状态接口来约束它们,这样外部就可以面向这个统一的状态接口编程,而无需关心具体的状态类实现了。
这样一来,要修改某种投票情况所对应的具体功能处理,那就是直接修改或者扩展某个状态处理类的功能就可以了。而要添加新的功能就更简单,直接添加新的状态处理类就可以了,当然在使用Context的时候,需要设置使用这个新的状态类的实例。
# 2.2 模式结构和说明
状态模式的结构如图所示:
- Context:环境,也称上下文,通常用来定义客户感兴趣的接口,同时维护一个来具体处理当前状态的实例对象。
- State:状态接口,用来封装与上下文的一个特定状态所对应的行为。
- ConcreteState:具体实现状态处理的类,每个类实现一个跟上下文相关的状态的具体处理。
# 2.3 示例代码
状态接口:
/**
* 封装与Context的一个特定状态相关的行为
*/
public interface State {
/**
* 状态对应的处理
*
* @param sampleParameter 示例参数,说明可以传入参数,具体传入
* 什么样的参数,传入几个参数,由具体应用来具体分析
*/
void handle(String sampleParameter);
}
2
3
4
5
6
7
8
9
10
11
12
具体的状态实现:
/**
* 实现一个与Context的一个特定状态相关的行为
*/
public class ConcreteStateA implements State {
public void handle(String sampleParameter) {
// 实现具体的处理
}
}
2
3
4
5
6
7
8
/**
* 实现一个与Context的一个特定状态相关的行为
*/
public class ConcreteStateB implements State {
public void handle(String sampleParameter) {
// 实现具体的处理
}
}
2
3
4
5
6
7
8
上下文的具体实现:
/**
* 定义客户感兴趣的接口,通常会维护一个State类型的对象实例
*/
public class Context {
/**
* 持有一个State类型的对象实例
*/
private State state;
/**
* 设置实现State的对象的实例
*
* @param state 实现State的对象的实例
*/
public void setState(State state) {
this.state = state;
}
/**
* 用户感兴趣的接口方法
*
* @param sampleParameter 示意参数
*/
public void request(String sampleParameter) {
// 在处理中,会转调state来处理
state.handle(sampleParameter);
}
}
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
# 2.4 重写案例
要使用状态模式,首先就需要把投票过程的各种状态定义出来,然后把这些状态对应的处理从原来大杂烩的实现中分离出来,形成独立的状态处理对象。而原来的投票管理的对象就相当于Context了。
把状态对应的行为分离出去过后,怎么调用呢?
按照状态模式的示例,是在Context中,处理客户请求的时候,转调相应的状态对应的具体的状态处理类来进行处理。
那就引出下一个问题:那么这些状态怎么变化呢?
看原来的实现,就是在投票方法里面,根据投票的次数进行判断,并维护投票类型的变化。那好,也依葫芦画瓢,就在投票方法里面来维护状态变化。
这个时候的程序结构如图所示:
状态接口:
/**
* 封装一个投票状态相关的行为
*/
public interface VoteState {
/**
* 处理状态对应的行为
*
* @param user 投票人
* @param voteItem 投票项
* @param voteManager 投票上下文,用来在实现状态对应的功能处理的时候,
* 可以回调上下文的数据
*/
void vote(String user, String voteItem, VoteManager voteManager);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
实现各个状态对应的处理:(NormalVoteState.java、RepeatVoteState.java、SpiteVoteState.java 和 BlackVoteState.java)
public class NormalVoteState implements VoteState {
public void vote(String user, String voteItem, VoteManager voteManager) {
// 正常投票
// 记录到投票记录中
voteManager.getMapVote().put(user, voteItem);
System.out.println("恭喜你投票成功");
}
}
2
3
4
5
6
7
8
public class RepeatVoteState implements VoteState {
public void vote(String user, String voteItem, VoteManager voteManager) {
// 重复投票
// 暂时不做处理
System.out.println("请不要重复投票");
}
}
2
3
4
5
6
7
public class SpiteVoteState implements VoteState {
public void vote(String user, String voteItem, VoteManager voteManager) {
// 恶意投票
// 取消用户的投票资格,并取消投票记录
String s = voteManager.getMapVote().get(user);
if (s != null) {
voteManager.getMapVote().remove(user);
}
System.out.println("你有恶意刷票行为,取消投票资格");
}
}
2
3
4
5
6
7
8
9
10
public class BlackVoteState implements VoteState {
public void vote(String user, String voteItem, VoteManager voteManager) {
// 黑名单
// 记入黑名单中,禁止登录系统了
System.out.println("进入黑名单,将禁止登录和使用本系统");
}
}
2
3
4
5
6
7
定义好了状态接口和状态实现,看看现在的投票管理,相当于状态模式中的上下文,相对而言,它的改变如下:
添加持有状态处理对象;
添加能获取记录用户投票结果的Map的方法,各个状态处理对象,在进行状态对应的处理的时候,需要获取上下文中的记录用户投票结果的Map数据;
在vote()方法实现里面,原来判断投票类型就变成了判断投票的状态,而原来每种投票类型对应的处理,现在已经封装到对应的状态对象里面去了,因此直接转调对应的状态对象的方法即可。
/**
* 投票管理
*/
public class VoteManager {
/**
* 持有状态处理对象
*/
private VoteState state = null;
/**
* 记录用户投票的结果,Map<String,String>对应Map<用户名称,投票的选项>
*/
private Map<String, String> mapVote = new HashMap<>();
/**
* 记录用户投票次数,Map<String,Integer>对应Map<用户名称,投票的次数>
*/
private Map<String, Integer> mapVoteCount = new HashMap<>();
/**
* 获取记录用户投票结果的Map
*
* @return 记录用户投票结果的Map
*/
public Map<String, String> getMapVote() {
return mapVote;
}
/**
* 投票
*
* @param user 投票人,为了简单,就是用户名称
* @param voteItem 投票的选项
*/
public void vote(String user, String voteItem) {
// 1:先为该用户增加投票的次数
// 先从记录中取出已有的投票次数
Integer oldVoteCount = mapVoteCount.get(user);
if (oldVoteCount == null) {
oldVoteCount = 0;
}
oldVoteCount = oldVoteCount + 1;
mapVoteCount.put(user, oldVoteCount);
// 2:判断该用户投票的类型,就相当于是判断对应的状态
// 到底是正常投票、重复投票、恶意投票还是上黑名单的状态
if (oldVoteCount == 1) {
state = new NormalVoteState();
} else if (oldVoteCount > 1 && oldVoteCount < 5) {
state = new RepeatVoteState();
} else if (oldVoteCount >= 5 && oldVoteCount < 8) {
state = new SpiteVoteState();
} else if (oldVoteCount >= 8) {
state = new BlackVoteState();
}
// 然后转调状态对象来进行相应的操作
state.vote(user, voteItem, this);
}
}
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
58
59
60
客户端:
public class Client {
public static void main(String[] args) {
VoteManager vm = new VoteManager();
for (int i = 0; i < 8; i++) {
vm.vote("u1", "A");
}
}
}
2
3
4
5
6
7
运行结果:
状态的转换基本上都是内部行为,主要在状态模式内部来维护。比如对于投票的人员,任何时候他的操作都是投票,但是投票管理对象的处理却不一定一样,会根据投票的次数来判断状态,然后根据状态去选择不同的处理。
# 三:模式讲解
# 3.1 优缺点
1. 简化应用逻辑控制
状态模式使用单独的类来封装一个状态的处理。如果把一个大的程序控制分成很多小块,每块定义一个状态来代表,那么就可以把这些逻辑控制的代码分散到很多单独的状态类当中去,这样就把着眼点从执行状态提高到整个对象的状态,使得代码结构化和意图更清晰,从而简化应用的逻辑控制。
对于依赖于状态的if-else,理论上来讲,也可以改变成应用状态模式来实现,把每个if或else块定义一个状态来代表,那么就可以把块内的功能代码移动到状态处理类去了,从而减少if-else,避免出现巨大的条件语句。
2. 更好的分离状态和行为
状态模式通过设置所有状态类的公共接口,把状态和状态对应的行为分离开来,把所有与一个特定的状态相关的行为都放入一个对象中,使得应用程序在控制的时候,只需要关心状态的切换,而不用关心这个状态对应的真正处理。
3. 更好的扩展性
引入了状态处理的公共接口后,使得扩展新的状态变得非常容易,只需要新增加一个实现状态处理的公共接口的实现类,然后在进行状态维护的地方,设置状态变化到这个新的状态即可。
4. 显式化进行状态转换
状态模式为不同的状态引入独立的对象,使得状态的转换变得更加明确。而且状态对象可以保证上下文不会发生内部状态不一致的情况,因为上下文中只有一个变量来记录状态对象,只要为这一个变量赋值就可以了。
5. 引入太多的状态类
状态模式也有一个很明显的缺点,一个状态对应一个状态处理类,会使得程序引入太多的状态类,使程序变得杂乱。
# 3.2 思考状态模式
1. 状态模式的本质
状态模式的本质:根据状态来分离和选择行为。
仔细分析状态模式的结构,如果没有上下文,那么就退化回到只有接口和实现了,正是通过接口,把状态和状态对应的行为分开,才使得通过状态模式设计的程序易于扩展和维护。 而上下文主要负责的是公共的状态驱动,每当状态发生改变的时候,通常都是回调上下文来执行状态对应的功能。当然,上下文自身也可以维护状态的变化,另外,上下文通常还会作为多个状态处理类之间的数据载体,在多个状态处理类之间传递数据。
2. 何时选用状态模式
建议在如下情况中,选用状态模式:
如果一个对象的行为取决于它的状态,而且它必须在运行时刻根据状态来改变它的行为。可以使用状态模式,来把状态和行为分离开,虽然分离开了,但状态和行为是有对应关系的,可以在运行期间,通过改变状态,就能够调用到该状态对应的状态处理对象上去,从而改变对象的行为。
如果一个操作中含有庞大的多分支语句,而且这些分支依赖于该对象的状态。可以使用状态模式,把各个分支的处理分散包装到单独的对象处理类里面,这样,这些分支对应的对象就可以不依赖于其它对象而独立变化了。
# 3.3 相关模式
1. 状态模式和策略模式
这是两个结构相同,功能各异的模式,具体的在策略模式里面讲过了,这里就不再赘述了。
2. 状态模式和观察者模式
这两个模式乍一看,功能是很相似的,但是又有区别,可以组合使用。
这两个模式都是在状态发生改变的时候触发行为,只不过观察者模式的行为是固定的,那就是通知所有的观察者,而状态模式是根据状态来选择不同的处理。
从表面来看,两个模式功能相似,观察者模式中的被观察对象就好比状态模式中的上下文,观察者模式中当被观察对象的状态发生改变的时候,触发的通知所有观察者的方法;就好比是状态模式中,根据状态的变化,选择对应的状态处理。
但实际这两个模式是不同的,观察者模式的目的是在被观察者的状态发生改变的时候,触发观察者联动,具体如何处理观察者模式不管;而状态模式的主要目的在于根据状态来分离和选择行为,当状态发生改变的时候,动态改变行为。
这两个模式是可以组合使用的,比如在观察者模式的观察者部分,当被观察对象的状态发生了改变,触发通知了所有的观察者过后,观察者该怎么处理呢?这个时候就可以使用状态模式,根据通知过来的状态选择相应的处理。
3. 状态模式和单例模式
这两个模式可以组合使用,可以把状态模式中的状态处理类实现成单例。
4. 状态模式和享元模式
这两个模式可以组合使用。
由于状态模式把状态对应的行为分散到多个状态对象中,会造成很多细粒度的状态对象,可以把这些状态处理对象通过享元模式来共享,从而节省资源。