设计之禅——状态模式

2020-09-07 10:45:38 浏览数 (2)

前言

之前我写过一篇策略模式的文章,讲的是如何灵活地改变对象的行为,今天要讲的模式和策略模式非常像,它也是让你设计出如何灵活改变对象行为的一个模式,与策略模式不同的是它是根据自身状态而自行地改变行为,它就是状态模式。

详解

普通实现

首先我们来分析一个实例:现在的游戏基本都有自动打怪做任务的功能,如果让你实现这个功能你会怎么做呢?

本篇讲解的是状态模式,当然首先应该分析其应有状态和行为,下面是我画的一个简单的状态图:

椭圆代表的是所处状态,指引线代表执行的行为。一开始角色处于初始状态,什么也不做,当玩家开启自动任务功能时,角色就自动的接受任务,当接到杀怪的任务后,发现周围没有怪,就把“初始状态”改为“未发现怪物”状态并开始四处游走寻找怪物,走啊走,走啊走,发现了目标怪物就将状态修改为“发现怪物”,然后开始攻击打怪,直到杀怪数量达到任务指定数量后,就停止打怪并将状态修改为“任务达成”状态,最后回到接任务那里提交任务,角色状态又重置为初始状态(这里只是为了方便理解该模式,不要太纠结功能细节)。不难发现,在该实例中,我们包含了四个状态和四个行为,任何一个行为是随时都有可能进行的,但是其表现结果却会因为状态的不同而有不一样的结果,按照我们面向过程的编程方式也是非常容易实现的:

代码语言: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方法名么?

总结

状态模式允许对象在内部状态改变时改变它的行为,如果需要在多个对象间共享状态,那么只需要定义静态域即可。

状态模式与策略模式具有相同的类图,但它们本质的意图是不同的。前者是封装基于状态的行为,并将行为委托到当前的状态,用户不需要知道有哪些状态;而后者是将可以互换的行为封装起来,然后使用委托,由客户决定需要使用哪种行为,客户需要知道所有的行为类。

0 人点赞