定义
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。属于行为模式。
使用场景
一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。
代码中包含大量与对象状态有关的条件语句,例如,一个操作中含有庞大的多分支语句(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!大概就是这样了
状态模式和策略模式的区别
最主要的不同是:状态模式和策略模式的结构是相似的,但它们的意图不同。
策略模式封装了一组相关算法,它允许Client在运行时使用可互换的行为;状态模式帮助一个类在不同的状态显示不同的行为。
在状态模式中,每个状态通过持有Context的引用,来实现状态转移;但是每个策略都不持有Context的引用,它们只是被Context使用。
另一个理论上的不同:策略模式定义了对象“怎么做”的部分。例如,排序对象怎么对数据排序。状态模式定义了对象“是什么”和“什么时候做”的部分。例如,对象处于什么状态,什么时候处在某个特定的状态。
最后但最重要的一个不同之处是,策略的改变由Client完成;而状态的改变,由Context或状态自己。
总结一下
好,终于撑到了结尾,这里做个总结,顺便填下前边挖的坑。
我们来说说用错的状态模式这个问题
这里有两个参考,一个是《HeadFirst设计模式》,一个是《Android源码设计模式解析与实战》。
在第二本书中状态模式部分的一个demo就是我们上边实现的那个场景。书里是怎么实现的呢?
public interface UserState{
public void comment(Context context);
}
//然后切换状态是在不同的Activity中点击登录按钮和注销按钮的时候做的操作
LoginContext.getLoginContext().setState(new LoginedState());
其实博主实现输入法场景的时候也是这样做的,我们来分析下这样做的问题。
首先,这种实现其实有些类似策略模式了,但是我们的意图仍然是对状态的封装。
其次,这样做把切换状态的操作暴露给了客户端,而不是Context角色,其实客户没有必要了解该怎么切换,只要简单粗暴的调用comment()方法就好了。