首页 > 代码库 > 用错的状态模式?
用错的状态模式?
突然有些明白了小说里世界上最牛逼的两个人为什么一定要在结尾干一架
因为他们真的都认为自己是正确的
并且深信不疑
而菜鸡(比如博主之流),有时候也是偏执狂
写在前面的话
这是一个两只猿类关于状态模式实现方式撕逼的故事。简要记录如下:
时间:2016年11月16日21:13:49
人物:博主和长脸先生同学。
起因:我们指定了一个这样的场景:用状态模式实现搜狗输入法输出中文、英文、大写三种状态的切换。然后我们分别写出了自己认为正确的代码,又为了证明自己的代码是正确的,开始撕逼。
经过:刀光剑影,刷刷刷刷。。。
结果:结果就是这篇博客的内容T^T。至于为什么把用错作为标题,文章结尾再分析。
定义
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。属于行为模式。
使用场景
一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。
代码中包含大量与对象状态有关的条件语句,例如,一个操作中含有庞大的多分支语句(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。
状态模式和策略模式的区别
最主要的不同是:状态模式和策略模式的结构是相似的,但它们的意图不同。
策略模式封装了一组相关算法,它允许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()方法就好了。
so,标题用了用错的状态模式。
其实设计模式这种东西,仁者见仁,智者见智,所以才会有那么多变种的实现,但目的是一样的:减少各个分析类之间的耦合和依赖。使软件更容易修改和维护,更善于应对变化。
独学而无友,则孤陋而寡闻。撕撕更健康。
用错的状态模式?