行为型-状态模式(State)

4/9/2022 设计模式

摘要

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("进入黑名单,将禁止登录和使用本系统");
        }
    }
}
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

客户端:

public class Client {
    public static void main(String[] args) {
        VoteManager vm = new VoteManager();
        for (int i = 0; i < 8; i++) {
            vm.vote("u1", "A");
        }
    }
}
1
2
3
4
5
6
7
8

运行结果:

# 1.3 有何问题

看起来很简单,是不是?幸亏这里只是示意,否则,你想想,在vote()方法中那么多判断,还有每个判断对应的功能处理都放在一起,是不是有点太杂乱了,那简直就是个大杂烩,如果把每个功能都完整的实现出来,那vote()方法会很长的。

一个问题是:如果现在要修改某种投票情况所对应的具体功能处理,那就需要在那个大杂烩里面,找到相应的代码块,然后进行改动

另外一个问题是:如果要添加新的功能,比如投票超过8次但不足10次的,给个机会,只是禁止登录和使用系统3天,如果再犯,才永久封掉账号,该怎么办呢?那就需要改动投票管理的源代码,在上面的if-else结构中再添加一个else if块进行处理

不管哪一种情况,都是在一大堆的控制代码里面找出需要的部分,然后进行修改,这从来都不是好方法,那么该如何实现才能做到,既能够很容易的给vote()方法添加新的功能,又能够很方便的修改已有的功能处理呢?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是状态模式。

状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看来起似乎修改了它的类。

# 2.1 解决思路

仔细分析上面的问题,会发现,那几种用户投票的类型,就相当于是描述了人员的几种投票状态,而各个状态和对应的功能处理具有很强的对应性,有点类似于 "一个萝卜一个坑",各个状态下的处理基本上都是不一样的,也不存在可以相互替换的可能

为了解决上面提出的问题,很自然的一个设计就是 首先把状态和状态对应的行为从原来的大杂烩代码中分离出来,把每个状态所对应的功能处理封装在一个独立的类里面,这样选择不同处理的时候,其实就是在选择不同的状态处理类。

然后为了统一操作这些不同的状态类,定义一个状态接口来约束它们,这样外部就可以面向这个统一的状态接口编程,而无需关心具体的状态类实现了

这样一来,要修改某种投票情况所对应的具体功能处理,那就是直接修改或者扩展某个状态处理类的功能就可以了。而要添加新的功能就更简单,直接添加新的状态处理类就可以了,当然在使用Context的时候,需要设置使用这个新的状态类的实例。

# 2.2 模式结构和说明

状态模式的结构如图所示:

classDiagram State <|.. ConcreteStateA State <|.. ConcreteStateB State <..o Context class State{ <<interface>> +handle(String)* void } class ConcreteStateA{ +handle(String) void } class ConcreteStateB{ +handle(String) void } class Context{ -State state +request(String) void }
  • Context:环境,也称上下文,通常用来定义客户感兴趣的接口,同时维护一个来具体处理当前状态的实例对象。
  • State:状态接口,用来封装与上下文的一个特定状态所对应的行为。
  • ConcreteState:具体实现状态处理的类,每个类实现一个跟上下文相关的状态的具体处理。

# 2.3 示例代码

状态接口:

/**
 * 封装与Context的一个特定状态相关的行为
 */
public interface State {

    /**
     * 状态对应的处理
     *
     * @param sampleParameter 示例参数,说明可以传入参数,具体传入
     *                        什么样的参数,传入几个参数,由具体应用来具体分析
     */
    void handle(String sampleParameter);
}
1
2
3
4
5
6
7
8
9
10
11
12

具体的状态实现:

/**
 * 实现一个与Context的一个特定状态相关的行为
 */
public class ConcreteStateA implements State {
    public void handle(String sampleParameter) {
        // 实现具体的处理
    }
}
1
2
3
4
5
6
7
8
/**
 * 实现一个与Context的一个特定状态相关的行为
 */
public class ConcreteStateB implements State {
    public void handle(String sampleParameter) {
        // 实现具体的处理
    }
}
1
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);
    }
}
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

# 2.4 重写案例

要使用状态模式,首先就需要把投票过程的各种状态定义出来,然后把这些状态对应的处理从原来大杂烩的实现中分离出来,形成独立的状态处理对象而原来的投票管理的对象就相当于Context了

把状态对应的行为分离出去过后,怎么调用呢?

按照状态模式的示例,是在Context中,处理客户请求的时候,转调相应的状态对应的具体的状态处理类来进行处理。

那就引出下一个问题:那么这些状态怎么变化呢?

看原来的实现,就是在投票方法里面,根据投票的次数进行判断,并维护投票类型的变化。那好,也依葫芦画瓢,就在投票方法里面来维护状态变化。

这个时候的程序结构如图所示:

classDiagram VoteState <|.. NormalVoteState VoteState <|.. RepeatVoteState VoteState <|.. SpliteVoteState VoteState <|.. BlackVoteState VoteManager o..> VoteState class VoteState{ <<interface>> +vote(String, String, VoteManager)* void } class NormalVoteState{ +vote(String, String, VoteManager) void } class RepeatVoteState{ +vote(String, String, VoteManager) void } class SpliteVoteState{ +vote(String, String, VoteManager) void } class BlackVoteState{ +vote(String, String, VoteManager) void } class VoteManager{ -State voteState -Map~String,String~ mapVote -Map~String,Integer~ mapVoteCount +vote(String, String) void +getMapVote() Map~String,String~ }

状态接口:

/**
 * 封装一个投票状态相关的行为
 */
public interface VoteState {
    /**
     * 处理状态对应的行为
     *
     * @param user        投票人
     * @param voteItem    投票项
     * @param voteManager 投票上下文,用来在实现状态对应的功能处理的时候,
     *                    可以回调上下文的数据
     */
    void vote(String user, String voteItem, VoteManager voteManager);
}
1
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("恭喜你投票成功");
    }
}
1
2
3
4
5
6
7
8
public class RepeatVoteState implements VoteState {
    public void vote(String user, String voteItem, VoteManager voteManager) {
        // 重复投票
        // 暂时不做处理
        System.out.println("请不要重复投票");
    }
}
1
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("你有恶意刷票行为,取消投票资格");
    }
}
1
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("进入黑名单,将禁止登录和使用本系统");
    }
}
1
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);
    }
}
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
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");
        }
    }
}
1
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. 状态模式和享元模式

这两个模式可以组合使用。

由于状态模式把状态对应的行为分散到多个状态对象中,会造成很多细粒度的状态对象,可以把这些状态处理对象通过享元模式来共享,从而节省资源。

# 四:参考文献

最后更新: 4/9/2022, 11:34:14 PM