Post

设计模式:状态模式

场景再现

假设有一款游戏,在这个游戏中,玩家要通过操纵角色来完成任务,而角色有着各种各样的状态,在不同状态下,玩家的操作方式会有所不同,假设每个角色有下面这几个状态:

  • standing(静止)
  • walking(走)
  • running(跑)
  • jumping(跳跃)

在不同的状态下,角色的 UI 显示也会有所不同,角色在不同状态下都可以施展不同技能,另外,只有在静止的状态下才能进行交流。

于是我们可以得到下面这个这个类:

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
class Game {
    private final static int STANDING = 1;
    private final static int WALKING = 2;
    private final static int RUNNING = 3;
    private final static int JUMPING = 4;

    private int state = 1;

    public void display() {
        if (state == STANDING) {
            System.out.println("Standing...");
        } else if (state == WALKING) {
            System.out.println("Walking...");
        } else if (state == RUNNING) {
            System.out.println("Running...");
        } else if (state == JUMPING) {
            System.out.println("Jumping...");
        }
    }

    public void perform() {
        if (state == STANDING) {
            System.out.println("Perform the standing ability");
        } else if (state == WALKING) {
            System.out.println("Perform the walking ability");
        } else if (state == RUNNING) {
            System.out.println("Perform the running ability");
        } else if (state == JUMPING) {
            System.out.println("Perform the jumping ability");
        }
    }

    public void talking() {
        if (state == STANDING) {
            System.out.println("Talking...");
        } else if (state == WALKING) {
            System.out.println("Alert: fail to talking");
        } else if (state == RUNNING) {
            System.out.println("Alert: fail to talking");
        } else if (state == JUMPING) {
            System.out.println("Alert: fail to talking");
        }
    }

    public void setState(int state) {
        this.state = state;
    }
}

看到上面这个类,你可能很快就会发现问题。因为每个状态的表现形式都不一样,这个类里面的每个方法都需要对当前的状态进行判断。假设我们现在需要添加一个新的状态,那么势必我们需要对几乎所有的方法都进行修改,这显然 违背了我们之前强调的封闭修改,开放拓展原则(Close for modification,Open for extension)

另外,在方法中使用大量的 if-else 条件判断,将所有的逻辑放在一起也是不可取的。时间长了,方法会变得非常的臃肿,上面的例子比较简单,看上去好像没啥问题,可你想想,如果每个状态下我们需要做很多事情,使用很多的临时变量,进行很多的计算和业务逻辑,那么代码还会这么简单吗?即使你在此基础之上做封装,重构,但是这一个方法依然做了很多的事情,没法做到单一职责。

还有一点就是,上面的例子并没有很复杂的状态切换,如果把状态切换的逻辑再加到每个方法中,状态之间彼此依赖,代码的复杂度还会上升。想要彻底解决这个问题,我们必须从代码的架构入手。

状态模式

上面代码问题的根源在于状态的可变性,一是状态定义的可变,比如现在只有四个状态,我们后面可能会增加或对现有的状态进行更改,二是状态之间可以相互转化,相互切换。上面的代码直接操作变化的东西,导致程序过于耦合,因此,一个思路就是,将变化的部分封装起来,这也是重构代码的一个常用手段。

因为要把状态封装起来,我们需要定义一个统一的接口:

1
2
3
4
5
interface State {
    void display();
    void perform();
    void talking();
}

然后再将每个状态实现:

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
class StandingState implements State {
    public void display() {
        System.out.println("Standing...");
    }

    public void perform() {
        System.out.println("Perform the standing ability");
    }

    public void talking() {
        System.out.println("Talking...");
    }
}

class WalkingState implements State {
    public void display() {
        System.out.println("Walking...");
    }

    public void perform() {
        System.out.println("Perform the walking ability");
    }

    public void talking() {
        System.out.println("Alert: fail to talking");
    }
}

class RunningState implements State {
    public void display() {
        System.out.println("Running...");
    }

    public void perform() {
        System.out.println("Perform the running ability");
    }

    public void talking() {
        System.out.println("Alert: fail to talking");
    }
}

class JumpingState implements State {
    public void display() {
        System.out.println("Jumping...");
    }

    public void perform() {
        System.out.println("Perform the jumping ability");
    }

    public void talking() {
        System.out.println("Alert: fail to talking");
    }
}

状态是拆分出来了,但是更重要的是我们需要把这些状态用在游戏中,这样才能和前面的代码功能保持一致:

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
class Game {
    private State standingState;
    private State walkingState;
    private State runningState;
    private State JumpingState;

    private State state;

    public Game() {
        standingState = new StandingState();
        walkingState = new WalkingState();
        runningState = new RunningState();
        jumpingState = new JumpingState();

        state = standingState;
    }

    public void display() {
        state.display();
    }

    public void perform() {
        state.perform();
    }

    public void talking() {
        state.talking();
    }

    public void setState(State state) {
        this.state = state;
    }
}

可以看到,当我们对状态进行封装后,Game 类中的函数逻辑变得非常简单,这些方法直接调用当前状态中的方法,没有任何的条件判断。

另外,当我们需要引入新的状态时,我们也只需要新创建一个 State 接口的实现类,然后添加进 Game 即可。重点是我们不需要更改其他的状态,也不需要更改 Game 类中的方法。

下图可以很好地概括状态模式:

这里面的 Context 指的就是上面例子中的 Game,可以看到的是状态模式主要依赖于 组合委托。组合是指状态类 StateContext 类的成员,这样两个类不至于耦合过深。委托指的是,Context 类中的成员方法将请求委托于状态成员来执行,从而降低整体的复杂度。

状态模式 VS 策略模式

之前我们讲过 策略模式,如果你回过头去看,会发现这两个模式的结构怎么一摸一样?这不就是一个模式吗?

它们的结构一样,这没错,但是它们依然是不同的模式。它们的区别主要体现在使用上,以及所要达到的目的。

两个模式的名字其实就是很好的说明。首先,什么是策略?策略可以理解为解决方案,一般来说,对一个问题我们可以有多个解决方案,但是往往我们会从多个解决方案中选出一种来使用,这个选择是外部的,也就是说策略模式内部并不会更改外界传入的策略,它会按照外界传入的策略来执行。再来看看什么是状态,如果把状态也看作是解决方案,这显然不恰当,并且状态肯定不是单一的,如果只有单一一个状态那其实等同于没有状态,而且更重要的是状态之间可以来回切换,比如在某个行为下,我们从状态 A 变成了状态 B,这个改变也是发生在状态模式的内部。状态的更改、维护都应该发生在内部,不然在使用上会很乱。

知道了两个模式的定义,我们再来看看它们的各自的目的。策略模式主要是为了去重,如果每个策略都需要重新创建一套独立的模块,那这样就会存在重复的代码,后期维护会难上加难。策略模式提供了一套灵活的配置 Context 类的机制,并且允许运行时更改策略。

而状态模式更多的是偏重解耦,如果不加以封装,状态和状态之间的相互关联会让程序逻辑变得十分复杂,并且 Context 类中的方法也需要反复地去检查当前的逻辑。与前面策略模式配置 Context 不同,状态模式的目的并不在配置,而是允许 Context 可以根据当前状态来改变行为,这更多的发生在内部。

另外,上面的例子仅仅处于展示的目的,我们并没有引入状态的变化,而在实际中这并不常见。状态的切换可以放在 Context 类中,当然也可以放在状态的实现类中,这个需要根据实际情况来权衡。一般来说,如果状态的数目很多,变化的概率很大,最好还是放在 Context 类中,这样有一个地方统一管理,并且解除了状态相互之间的耦合,程序会比较好维护。

This post is licensed under CC BY 4.0 by the author.