用错的状态模式?

1,334 阅读5分钟

定义

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。属于行为模式。

使用场景

  1. 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为

  2. 代码中包含大量与对象状态有关的条件语句,例如,一个操作中含有庞大的多分支语句(if-else或switch-case),且这些分支依赖于该对象的状态

结构

这里写图片描述

模式所涉及的角色有:

  • 环境(Context)角色,也称上下文:定义客户端所感兴趣的接口,并且保留一个具体状态类的实例。这个具体状态类的实例给出此环境对象的现有状态。

  • 抽象状态(State)角色:定义一个接口,用以封装环境(Context)对象的一个特定的状态所对应的行为。

  • 具体状态(ConcreteState)角色:每一个具体状态类都实现了环境(Context)的一个状态所对应的行为。

实现

这次的实现我们以开发中的一个场景来展开分析。场景是这样的:我们的App有登录和登出两种状态,默认是没有登录的。登陆状态下我们可以发表评论,如果是在登出状态,跳转到登陆界面。

接下来我们开始把我们的抽象状态角色抽象出来。分析状态模式可以画一个简单的状态图来帮助我们分析,草图如下:

这里写图片描述

通过状态图我们可以很轻松的看出,我们的具体状态角色:登陆状态和登出状态。我们的抽象状态角色中的动作有:登陆、登出、评论

接下来我们看具体实现:

环境(Context)角色

public class SignManager {

    private SignState mState;
    private SignState mSignInState;
    private SignState mSignOutState;

    private static SignManager ourInstance = new SignManager();

    public static SignManager getInstance() {
        return ourInstance;
    }

    private SignManager() {
        mSignInState = new SignInState();
        mSignOutState = new SignOutState();
        mState = mSignOutState;
    }

    public void setState(SignState state) {
        mState = state;
    }

    public SignState getSignInState() {
        return mSignInState;
    }

    public void setSignInState(SignState signInState) {
        mSignInState = signInState;
    }

    public SignState getSignOutState() {
        return mSignOutState;
    }

    public void setSignOutState(SignState signOutState) {
        mSignOutState = signOutState;
    }

    //将需要外部调用的方法委托给SignState
    public void signIn() {
        mState.signIn();
    }

    public void signOut() {
        mState.signOut();
    }

    public void comment() {
        mState.comment();
    }
}

这里我们用了单例模式来实现,统一一个访问点,这里就先不要纠结单例模式的实现了,这不是重点。

抽象状态(State)角色

/**
 * 登录状态的接口(抽象类或interface),封装改变状态的动作
 */

public abstract class SignState {

    /**
     * 登陆
     */
    public void signIn() {
        throw new UnsupportedOperationException();
    }

    /**
     * 登出
     */
    public void signOut() {
        throw new UnsupportedOperationException();
    }

    /**
     * 发表评论
     */
    public abstract void comment();
}

这里需要注意的是我们用的是抽象类而不是接口,目的是我们可以在抽象类里做一些默认的实现。比如我们在登录状态下调用登陆,会抛出UnsupportedOperationException()异常。

具体状态(ConcreteState)角色

/**
 * 登陆状态
 */

public class SignInState extends SignState {

    @Override
    public void signOut() {
        SignManager.getInstance().setState(SignManager.getInstance().getSignOutState());
    }

    @Override
    public void comment() {
        Log.e("state", "您已登录,可以发表评论哦~");
    }
}

/**
 * 登出状态
 */

public class SignOutState extends SignState {

    @Override
    public void signIn() {
        SignManager.getInstance().setState(SignManager.getInstance().getSignInState());
    }

    @Override
    public void comment() {
        Log.e("state", "您还没有登录,需要先去登陆哦~");
    }

}

客户端调用

private void testState() {
    //当我们登陆的时候只要告诉SignManager我们已经登录了
    SignManager.getInstance().signIn();
    //当我们评论的时候只需要调用评论就好了,不用再去判断是否登陆
    SignManager.getInstance().comment();

    //因为我们已经登录了,这个时候再去调用登录我们会抛出一个异常,同理登出之后在调用登出方法也会抛出异常
//  SignManager.getInstance().signIn();

    //登出
    SignManager.getInstance().signOut();
    SignManager.getInstance().comment();
}

bingo!大概就是这样了

测试代码已上传到github

状态模式和策略模式的区别

最主要的不同是:状态模式和策略模式的结构是相似的,但它们的意图不同

  1. 策略模式封装了一组相关算法,它允许Client在运行时使用可互换的行为;状态模式帮助一个类在不同的状态显示不同的行为。

  2. 在状态模式中,每个状态通过持有Context的引用,来实现状态转移;但是每个策略都不持有Context的引用,它们只是被Context使用。

  3. 另一个理论上的不同:策略模式定义了对象“怎么做”的部分。例如,排序对象怎么对数据排序。状态模式定义了对象“是什么”和“什么时候做”的部分。例如,对象处于什么状态,什么时候处在某个特定的状态。

  4. 最后但最重要的一个不同之处是,策略的改变由Client完成;而状态的改变,由Context或状态自己。

总结一下

好,终于撑到了结尾,这里做个总结,顺便填下前边挖的坑。

我们来说说用错的状态模式这个问题

这里有两个参考,一个是《HeadFirst设计模式》,一个是《Android源码设计模式解析与实战》。

在第二本书中状态模式部分的一个demo就是我们上边实现的那个场景。书里是怎么实现的呢?

public interface UserState{

    public void comment(Context context);
}

//然后切换状态是在不同的Activity中点击登录按钮和注销按钮的时候做的操作
LoginContext.getLoginContext().setState(new LoginedState());

其实博主实现输入法场景的时候也是这样做的,我们来分析下这样做的问题。

首先,这种实现其实有些类似策略模式了,但是我们的意图仍然是对状态的封装。

其次,这样做把切换状态的操作暴露给了客户端,而不是Context角色,其实客户没有必要了解该怎么切换,只要简单粗暴的调用comment()方法就好了。