前言
之前我写过一篇策略模式的文章,讲的是如何灵活地改变对象的行为,今天要讲的模式和策略模式非常像,它也是让你设计出如何灵活改变对象行为的一个模式,与策略模式不同的是它是根据自身状态而自行地改变行为,它就是状态模式。
详解
普通实现
首先我们来分析一个实例:现在的游戏基本都有自动打怪做任务的功能,如果让你实现这个功能你会怎么做呢?
本篇讲解的是状态模式,当然首先应该分析其应有状态和行为,下面是我画的一个简单的状态图:
椭圆代表的是所处状态,指引线代表执行的行为。一开始角色处于初始状态,什么也不做,当玩家开启自动任务功能时,角色就自动的接受任务,当接到杀怪的任务后,发现周围没有怪,就把“初始状态”改为“未发现怪物”状态并开始四处游走寻找怪物,走啊走,走啊走,发现了目标怪物就将状态修改为“发现怪物”,然后开始攻击打怪,直到杀怪数量达到任务指定数量后,就停止打怪并将状态修改为“任务达成”状态,最后回到接任务那里提交任务,角色状态又重置为初始状态(这里只是为了方便理解该模式,不要太纠结功能细节)。不难发现,在该实例中,我们包含了四个状态和四个行为,任何一个行为是随时都有可能进行的,但是其表现结果却会因为状态的不同而有不一样的结果,按照我们面向过程的编程方式也是非常容易实现的:
代码语言:javascript复制public class Character {
// 停止
private final static int STOP = 0;
// 附近有怪
private final static int HASMONSTER = 1;
// 附近没有怪
private final static int NOMONSTER = 2;
// 任务条件达成
private final static int MISSIONCLEAR = 4;
// 当前状态
private int state = STOP;
// 还需杀怪数量
private int count = 0;
public void accept(int count) {
if (state == STOP) {
this.count = count;
state = NOMONSTER;
// move to find the monster
move();
} else if (state == HASMONSTER) {
System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
} else if (state == NOMONSTER) {
System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
} else if (state == MISSIONCLEAR) {
System.out.println("Sorry!You must submit the current task!");
}
}
private void move() {
if (state == STOP) {
System.out.println("Moving....");
state = HASMONSTER;
attack();
} else if (state == HASMONSTER) {
System.out.println("Moving to find new monster");
attack();
} else if (state == NOMONSTER) {
System.out.println("Moving to find monster");
state = HASMONSTER;
attack();
} else if (state == MISSIONCLEAR) {
System.out.println("Moving to submit");
submit();
}
}
private void attack() {
}
private void submit() {
}
}
最后两个方法我没有给出具体实现,相信难不倒你,当全部实现后角色就能自动接任务打怪了:
代码语言:javascript复制Accept the task.Need to kill monster:10
Moving to find monster
need to kill:9
need to kill:8
need to kill:7
need to kill:6
need to kill:5
need to kill:4
need to kill:3
need to kill:2
need to kill:1
need to kill:0
Moving to submit
Congratulations on completing the task!
不过,功能虽然实现了,但是这样写代码冗长不说,还非常难于理解维护,想象一下这里只假设了4种状态,当如果有非常多的状态,那就是满篇的if else了,而且如果未来需要增加新的状态,那么当前的实现无疑是违反了open-close原则的,我们没有封装变化的那部分。那应该如何做呢?这就需要我们的状态模式了。
使用状态模式重构代码
往下看之前,不妨先仔细思考一下,既然该功能中状态是会随时改变的,而行为又会受到状态的影响,那何不将状态抽离出来成为一个体系呢?比如定义一个状态接口(为什么这里需要定义所有的行为方法呢?):
代码语言:javascript复制public interface State {
void accept(int count);
void move();
void attack();
void submit();
}
那么角色类中就可以如下定义了:
代码语言:javascript复制public class Character {
// 当前状态
private State current = new StopState(this);
// 所需杀怪数量
private int count = 0;
public void accept(int count) {
// 注意这里不能直接将值赋给成员变量
current.accept(count);
}
public void move() {
current.move();
}
public void attack() {
current.attack();
}
public void submit() {
current.submit();
}
public void killOne() {
this.count--;
}
public void setCurrent(State current) {
this.current = current;
}
public void setCount(int count) {
this.count = count;
}
public State getCurrent() {
return current;
}
public int getCount() {
return count;
}
}
相比较之前,新的类只保留了当前状态,并增加了getter和setter方法,而角色的行为则全都委托给了具体的状态类来实现,那具体的状态类应该如何实现呢?
代码语言:javascript复制// 初始状态
public class StopState implements State {
private Character c;
public StopState(Character c) {
this.c = c;
}
@Override
public void accept(int count) {
c.setCount(count);
c.setCurrent(new NoMonsterState(c));
c.move();
}
@Override
public void move() {
System.out.println("Moving....");
c.setCurrent(new HasMonsterState(c));
c.attack();
}
@Override
public void attack() {
System.out.println("Sorry!You must accept the task!");
}
@Override
public void submit() {
System.out.println("You don't have task to submit!");
}
}
// 附近没有怪物
public class NoMonsterState implements State {
private Character c;
public NoMonsterState(Character c) {
this.c = c;
}
@Override
public void accept(int count) {
System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
}
@Override
public void move() {
System.out.println("Moving to find monster!");
c.setCurrent(new HasMonsterState(c));
c.attack();
}
@Override
public void attack() {
c.move();
}
@Override
public void submit() {
System.out.println("Please complete the task!");
}
}
这里我也只给出了两个实现类,其它的相信你能很容实现它们。通过状态模式重构后,代码清晰了很多,没有满屏的if else,角色也能够根据当前所处的状态表现出相应的行为,同时如果需要增加新的状态时,只需要实现State接口就行了,看起来相当完美。但是,没有什么模式是完美的,使用状态模式的缺点我们很容易发现,原来一个类就能解决的,现在裂变为了四个类,系统结构复杂了很多,但这样的牺牲是非常有必要和值得的。
思考
刚刚我们已经实现了状态模式,但是还有个细节问题不知你注意到了没有?比如:
代码语言:javascript复制 public void move() {
System.out.println("Moving....");
c.setCurrent(new HasMonsterState(c));
c.attack();
}
在我的实现中,都是由状态来控制下一个状态是什么,这样状态之间就形成了强依赖,当然你可以将状态转换放到context(Character)类中,不过这种更适合状态转换是固定的,而在我们这个例子中,状态的变更是动态的。还需要注意的是我这里调用 c.setCurrent(new HasMonsterState©)时,状态是硬编码传入的,这样当系统进化时可能就需要更改此处的代码,如何解决这种情况呢?在《Head First设计模式》书中有提到,在Context类中定义所有的状态并提供getter方法,这里则调用getter获取后再传入,但区别只在于是context类还是状态类对修改封闭:
代码语言:javascript复制c.setCurrent(c.getHasMonsterState());
对此我有点疑问,即使使用getter获取,那未来系统进化导致状态的改变后难道不需要修改getter方法名么?
总结
状态模式允许对象在内部状态改变时改变它的行为,如果需要在多个对象间共享状态,那么只需要定义静态域即可。
状态模式与策略模式具有相同的类图,但它们本质的意图是不同的。前者是封装基于状态的行为,并将行为委托到当前的状态,用户不需要知道有哪些状态;而后者是将可以互换的行为封装起来,然后使用委托,由客户决定需要使用哪种行为,客户需要知道所有的行为类。