状态模式

90 阅读10分钟

引入案例

在讲状态模式之前,我们先来看看这个案例,大家可以想想如何实现会比较优雅(可扩展、可维护):你们公司准备开发一款纸牌游戏软件,在该游戏软件中用户角色具有入门级(Primary)、熟练级(Secondary)、高手级(Professional)和骨灰级(Final)4种等级,角色的等级与其积分相对应,游戏胜利将增加积分,失败则扣除积分。入门级具有最基本的游戏功能Play(),熟练级增加了游戏胜利积分加倍功能 doubleScore(),高手级在熟练级基础上增加了换牌功能 changeCards(),骨灰级在高手级基础上再增加偷看他人的牌的功能 peekCards().

大部分同学可能上来就会直接这么设计:

直接设计一个User 类,该类中有一个积分字段 curScore。同时具有 play,score,changeCards,peekCards 等功能。

/**
 * 这里我们假设 [0,1000),[1000,2000),[2000,3000),[2000,无穷) 分别对应四个等级的积分
 */
public class User {
    private String mName;
    private int mScore; //当前积分
    //定义用户等级
    private static final int PrimaryLever = 0;
    private static final int SecondaryLever = 1;
    private static final int ProfessionalLever = 2;
    private static final int FinalLever = 3;
    //默认是入门级
    private int mUserLever = PrimaryLever;
    
    public User(String name, int score) {
        this.mName = name;
        this.mScore = score;
        changeLever();
    }
    //计算得分,并在得分之后给用户等级赋值。
    public void win(int score) {
        if (mUserLever == PrimaryLever) {
            // 入门级只有单倍积分
            mScore += score;
        } else {
            //其他等级都是双倍积分
            mScore += score * 2;
        }
        changeLever();
        System.out.println(mName + " score : " + mScore);
    }

    public void loss(int score) {
        mScore += score;
        changeLever();
        System.out.println(mName + " score : " + mScore);
    }

    /**
     *  所有等级都有玩游戏的权限
     */
    public void play() {
        System.out.println(mName + " is playing");
    }

    /**
     *  只有高手级及其以上才有换牌权限
     */
    public void changeCards() {
        if (mUserLever >= ProfessionalLever) {
            System.out.println(mName + " change cards success");
        } else {
            System.out.println(mName + " lever is low, not allow change cards");
        }
    }

    //偷看别人牌的能力
    public void peekCards() {
        if (mUserLever >= FinalLever) {
            System.out.println(mName + " peek cards success");
        } else {
            System.out.println(mName + " lever is low, not allow peek cards");
        }
    }

    //得分之后需要查看状态等级是否发生改变
    public void changeLever() {
        if (mScore < 1000) {
            mUserLever = 0;
        } else if (mScore >= 1000 && mScore < 2000) {
            mUserLever = 1;
        } else if (mScore >= 2000 && mScore < 3000) {
            mUserLever = 2;
        } else if (mScore >= 3000) {
            mUserLever = 3;
        }
    }
}
}

/**
 * 状态模式
 */
public class StateMode {
    public static void main(String[] args) {
        User user1 = new User("唐僧", 3000);
        User user2 = new User("孙悟空", 2000);
        User user3 = new User("猪八戒", 1000);
        User user4 = new User("沙和尚", 100);

        System.out.println("---");

        user1.play();
        user2.play();
        user3.play();
        user4.play();

        System.out.println("---");

        user1.changeCards();
        user2.changeCards();
        user3.changeCards();
        user4.changeCards();

        System.out.println("---");

        user1.peekCards();
        user2.peekCards();
        user3.peekCards();
        user4.peekCards();

        System.out.println("---");

        user1.win(1);
        user2.win(1);
        user3.loss(-3);
        user4.win(+1);

    }
}

我们执行上述代码得到如下结果,功能上完全是符合预期的,但是我们不禁要问下自己,是否有更好的实现方式呢?

---
唐僧 is playing
孙悟空 is playing
猪八戒 is playing
沙和尚 is playing
---
唐僧 change cards success
孙悟空 change cards success
猪八戒 lever is low, not allow change cards
沙和尚 lever is low, not allow change cards
---
唐僧 peek cards success
孙悟空 lever is low, not allow peek cards
猪八戒 lever is low, not allow peek cards
沙和尚 lever is low, not allow peek cards
---
唐僧 score : 3002
孙悟空 score : 2002
猪八戒 score : 997
沙和尚 score : 101

分析上述代码,不难发现存在以下问题:

  1. 几乎每个方法中都包含状态判断语句,以判断在该状态下是否具有该方法以及在特定状态下该方法如何实现,导致代码冗长,可维护性较差。
  2. 拥有一个较为复杂的stateCheck()方法,包含大量的if...else if...else 语句用于进行状态转换,代码测试难度较大,且不易于维护。
  3. 系统扩展性较差,如果需要新增一种状态,比如超级玩家,需要对原有代码进行大量修改,扩展起来非常麻烦。

状态模式定义

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。

这么做的优势在于:上层业务无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,上层业务都可以一致性的处理(调用功能方法)

在状态模式中引入了抽象状态类和具体的状态类,它们时状态模式的核心,器结构如下图所示:

状态模式类图.jpg

  • Context:上下文类,它是拥有多种状态的对象。由于上下文类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在上下文类中维护了一个抽象状态类State实例,这个实例定义当前状态,在具体实现(赋值)时,它是一个State自类对象。
  • State:抽象状态类。它用于定义一个接口(定义上下文类中在某个状态下的行为),在抽象类中声明各种不同状态对应的方法,而其子类中实现这些方法。由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
  • ConcreteState:它是抽象类的子类,每一个子类实现上下文类的一个状态相关的行为。所以不同的状态类其行为有所不同。

完整解决方案

上述纸牌游戏如果通过状态模式来实现,需要怎么设计编码呢?

状态模式解决类图

首先我们需要明确用户的行为动作,主要有 play,changeCards,peekCards,win,loss 这5个行为,那么每种状态下都需要有这几个行为的具体实现才行。

public abstract class BaseLever {

    protected User mUser;
    public BaseLever(User user) {
        mUser = user;
    }

    public void  play() {
        System.out.println(mUser.getmName() + " is playing");
    }

    public void loss(int score) {
        mUser.setScore(mUser.getScore() + score);
        mUser.changeLever();
        System.out.println(mUser.getmName() + " loss score: " + mUser.getScore());
    }

    public abstract void changeCards();
    public abstract void peekCards();
    public abstract void win(int score);
}


public class Primary extends BaseLever {

    public Primary(User user) {
        super(user);
    }

    public void changeCards() {
        System.out.println(mUser.getmName() + "普通级用户不允许换牌");
    }

    public void peekCards() {
        System.out.println(mUser.getmName() + "用户级别不够,不允许偷看别人的牌");
    }

    public void win(int score) {
        mUser.setScore(mUser.getScore() + score);
        System.out.println(mUser.getmName() + " score: " + mUser.getScore());
    }
}

public class Secondary extends Primary {

    public Secondary(User user) {
        super(user);
    }

    @Override
    public void win(int score) {
        mUser.setScore(mUser.getScore() + score * 2);
        mUser.changeLever();
        System.out.println(mUser.getmName() + " win score: " + mUser.getScore());
    }
}

public class Professional extends Secondary {
    public Professional(User user) {
        super(user);
    }

    public void changeCards() {
        System.out.println(mUser.getmName() + "高手级及其以上用户换牌成功");
    }
}

public class Final extends Professional {
    public Final(User user) {
        super(user);
    }

    public void peekCards() {
        System.out.println(mUser.getmName() + "骨灰级用户偷看别人的牌成功");
    }
}

public class User {

    private String mName;

    private int mScore; //当前积分

    //定义用户等级
    private final BaseLever PrimaryLever =  new Primary(this);
    private final BaseLever SecondaryLever = new Secondary(this);
    private final BaseLever ProfessionalLever = new Professional(this);
    private final BaseLever FinalLever = new Final(this);

    //默认是入门级
    private BaseLever mUserLever = PrimaryLever;


    public User(String name, int score) {
        this.mName = name;
        this.mScore = score;
        changeLever();
    }

    public void setScore(int score) {
        this.mScore = score;
    }

    public int getScore() {
        return mScore;
    }

    public String getmName() {
        return mName;
    }

    //计算得分,并在得分之后给用户等级赋值。
    public void win(int score) {
        mUserLever.win(score);
    }

    public void loss(int score) {
        mUserLever.loss(score);
    }

    /**
     *  所有等级都有玩游戏的权限
     */
    public void play() {
        mUserLever.play();
    }

    /**
     *  只有高手级及其以上才有换牌权限
     */
    public void changeCards() {
        mUserLever.changeCards();
    }

    //偷看别人牌的能力
    public void peekCards() {
        mUserLever.peekCards();
    }


    //得分之后需要查看状态等级是否发生改变
    public void changeLever() {
        if (mScore < 1000) {
            mUserLever = PrimaryLever;
        } else if (mScore >= 1000 && mScore < 2000) {
            mUserLever = SecondaryLever;
        } else if (mScore >= 2000 && mScore < 3000) {
            mUserLever = ProfessionalLever;
        } else if (mScore >= 3000) {
            mUserLever = FinalLever;
        }
    }
}

public class StateMode {
    public static void main(String[] args) {
        User user1 = new User("唐僧", 3000);
        User user2 = new User("孙悟空", 2000);
        User user3 = new User("猪八戒", 1000);
        User user4 = new User("沙和尚", 100);

        System.out.println("---");

        user1.play();
        user2.play();
        user3.play();
        user4.play();

        System.out.println("---");

        user1.changeCards();
        user2.changeCards();
        user3.changeCards();
        user4.changeCards();

        System.out.println("---");

        user1.peekCards();
        user2.peekCards();
        user3.peekCards();
        user4.peekCards();

        System.out.println("---");

        user1.win(1);
        user2.win(1);
        user3.loss(-3);
        user4.win(+1);

    }
}

从User类中的各个行为实现来看变得非常简单了,并且每个状态类中的行为也互相解耦了。 最终执行结果如下:

---
唐僧 is playing
孙悟空 is playing
猪八戒 is playing
沙和尚 is playing
---
唐僧高手级及其以上用户换牌成功
孙悟空高手级及其以上用户换牌成功
猪八戒普通级用户不允许换牌
沙和尚普通级用户不允许换牌
---
唐僧骨灰级用户偷看别人的牌成功
孙悟空用户级别不够,不允许偷看别人的牌
猪八戒用户级别不够,不允许偷看别人的牌
沙和尚用户级别不够,不允许偷看别人的牌
---
唐僧 win score: 3002
孙悟空 win score: 2002
猪八戒 loss score: 997
沙和尚 score: 101

共享状态

如果多个上下文类要复用某个状态对象,那么最简单的方法就是将该状态对象设置为static 的,这样可以做到对象共享。这里大家知道有这么一回事即可

上下文中进行状态切换

对象状态的切换一般有2种方式来实现切换:在上下文类(即对象本身)中实现切换在状态类中实现切换。

前面举的例子是在状态类中进行的状态切换,那什么时候可以在上下文类中进行状态切换呢? 这里我总结出一条经验:当状态变化因素直接由外界引起的时候就可以直接在上下文中进行切换;反之如果状态切换因素是受上下文类自身相关操作后导致状态变化,则需要通过状态类来切换相关状态。

比如下面这个例子,当用户第一次点击一次屏幕上的图片时图片放大2倍,第二次点击时图片放大4倍,第三次点击时恢复原来大小。那么图片就有3种状态,NormalState、LargerState、LargestState。在每种状态下需要展示不同大小的图片。 那这里状态影响因素是什么呢?当然是用户的点击行为,当有点击行为发生时就需要切换图片状态,可以我们可以在用户点击行为回调中直接改变状态.

这里相关为代码如下:

...
public void onClick() {
    if (curState == NormalState) {
        setState(LargerState);
    } else if (curState == LargerState) {
        setState(LargestState);
    } else if (curState == LargestState) {
        setState(NormalState);
    }
    curState.display();
}
...

最后只要将状态设置正确了,就可以保证display的效果是符合预期的。

状态模式总结

优点

状态模式主要优点如下:

  1. 封装了状态的转换规则,可以封装在上下文类或者状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
  2. 将所有与某个状态相关的行为放在一个类中,只需要注入一个不同的状态对象即可使上下文对象拥有不同的行为。
  3. 状态模式可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
  4. 可以让多个上下文对象共享一个状态对象,从而减少系统中对象个数。

缺点

状态模式的主要缺点如下:

  1. 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
  2. 状态模式的程序结构与实现都比较复杂,如果使用不当将导致程序结构和代码的混乱。
  3. 状态模式对开闭原则的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需要修改对应类的源代码。

适用场景

在以下情况下可以考虑使用状态模式:

  1. 对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化。
  2. 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便的增加和删除状态,并且导致业务类与模块之间的耦合增强。