####状态模式介绍
状态模式中的行为是由状态来决定的,不同的状态下有不同的行为。状态模式和策略模式的结构几乎完全一样,但它们的目的、本质却完全不一样。状态模式的行为是平行的、不可替换的,策略模式的行为是彼此独立的、可相互替换的。用一句话来表述,状态模式把对象的行为包装在不同的状态对象里,每一个状态对象都有一个共同的抽象状态基类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之变化。
####状态模式的定义
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
####状态模式的使用场景
(1)一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。
(2)代码中包含大量与对象状态有关的条件语句,例如,一个操作中含有庞大的多分支语句(if-else或switch-case),且这些分支依赖于该对象的状态。
状态模式将每一个条件分支放入一个独立的类中,这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化,这样通过多态来去除过多的、重复的if-else等分支语句。
####状态模式的UML类图
package com.guifa.statedemo;
/**
* 电视遥控器,含有开机、关机、下一频道、上一频道、调高音量、调低音量这几个功能
*/
public class TvController {
/**
* 开机状态
*/
private static final int POWER_ON = 1;
/**
* 关机状态
*/
private static final int POWER_OFF = 2;
private int mState = POWER_OFF;
public void powerOn() {
mState = POWER_ON;
if (mState == POWER_OFF) {
System.out.println("开机啦");
}
}
public void powerOff() {
mState = POWER_OFF;
if (mState == POWER_ON) {
System.out.println("关机啦");
}
}
public void nextChannel() {
if (mState == POWER_ON) {
System.out.println("下一频道");
} else {
System.out.println("两个红灯提示没有开机");
}
}
public void prevVhannel() {
if (mState == POWER_ON) {
System.out.println("上一频道");
} else {
System.out.println("两个红灯提示没有开机");
}
}
public void turnUp() {
if (mState == POWER_ON) {
System.out.println("调高音量");
} else {
System.out.println("两个红灯提示没有开机");
}
}
public void turnDown() {
if (mState == POWER_ON) {
System.out.println("调低音量");
} else {
System.out.println("两个红灯提示没有开机");
}
}
}
可以看到,在TvController类中,通过mState字段存储了电视机的状态,并在各个操作中根据状态来判断是否应该执行。这就导致了在每个功能中都需要用到if-else,代码重复、相对较为混乱,如果当状态变成5个、功能函数变为10个,每个函数中都需要用到if-else进行判断,而这些代码都充斥在一个类中,使得这个类变得越来越难以维护。
状态模式就是为解决这类问题而出现的,我们将这些状态用对象来代替,将这些行为封装到对象中,使得在不同的状态下有不同的实现,这样就将这些if-else从TVController类中去掉,整个结构也变得清晰起来。我们看看代码实现:
package com.guifa.statedemo;
/**
* 电视状态接口,定义了电视操作函数
*/
public interface TvState {
public void nextChannel();
public void prevChannel();
public void turnUp();
public void turnDown();
}
package com.guifa.statedemo;
/**
* 关机状态,此时只有开机功能是有效的
*/
public class PowerOffState implements TvState {
@Override
public void nextChannel() {
}
@Override
public void prevChannel() {
}
@Override
public void turnUp() {
}
@Override
public void turnDown() {
}
}
package com.guifa.statedemo;
/**
* 开机状态,此时再触发开机功能不做任何操作
*/
public class PowerOnState implements TvState {
@Override
public void nextChannel() {
System.out.println("下一频道");
}
@Override
public void prevChannel() {
System.out.println("上一频道");
}
@Override
public void turnUp() {
System.out.println("调高音量");
}
@Override
public void turnDown() {
System.out.println("调低音量");
}
}
package com.guifa.statedemo;
/**
* 电源操作接口
*/
public interface PowerController {
public void powerOn();
public void powerOff();
}
package com.guifa.statedemo;
/**
* 电视遥控器,类似于经典状态模式种的Context
*/
public class TvController implements PowerController {
TvState mTvState;
public void setTvState(TvState mTvState) {
this.mTvState = mTvState;
}
@Override
public void powerOn() {
setTvState(new PowerOnState());
System.out.println("开机啦");
}
@Override
public void powerOff() {
setTvState(new PowerOffState());
System.out.println("关机啦");
}
public void nextChannel() {
mTvState.nextChannel();
}
public void prevChannel() {
mTvState.prevChannel();
}
public void turnUp() {
mTvState.turnUp();
}
public void turnDown() {
mTvState.turnDown();
}
}
下面是客户端的调用代码:
package com.guifa.statedemo;
/**
* 客户端调用代码
*/
public class Client {
public static void main(String[] args) {
TvController tvController = new TvController();
// 设置开机状态
tvController.powerOn();
// 下一频道
tvController.nextChannel();
// 调高音量
tvController.turnUp();
// 设置关机状态
tvController.powerOff();
// 调高音量,此时不会生效
tvController.turnUp();
}
}
输出结果如下:
上述实现中,我们抽象了一个TvState接口,该接口中有操作电视的所有函数,该接口有两个实现类,即开机状态(PowerOnState)和关机状态(PowerOffState)。开机状态下只有开机功能是无效的,也就是说在已经开机的时候用户再按开机键不会产生任何反应;而在关机状态下,只有开机功能是可用的,其他功能都不会生效。同一个操作,如调高音量的turnUp函数,在关机状态下无效,在开机状态下就会将电视的音量调高,也就是说电视的内部状态影响了电视遥控器的行为。状态模式将这些行为封装到状态类中,在进行操作时将这些功能转发给状态对象,不同的状态有不同的实现,这样就通过多态的形式去除了重复、杂乱的if-else语句,这也正是状态模式的精髓所在。
####状态模式实战
在开发过程中,我们用到状态模式最常见的地方应该是用户登录系统。在用户已登录和未登录的情况下,对于同一事件的处理行为是不一样的,例如,在新浪微博中要转发一条内容,用户在未登录的情况下点击转发按钮,此时会先让用户登录,然后再执行转发操作;如果是已登录的情况下,那么用户输入转发的内容后就可以直接进行操作。可见,在这两种状态下,对于转发这个操作的处理动画完全不一样,当状态改变时对于转发操作的行为发生了改变。
下面我们用状态模式来简单实现这个过程,首先创建一个Android项目,里面含有两个Activity,分别为MainActivity、LoginActivity,MainActivity是应用第一个Activity,有转发和注销用户功能,LoginActivity则为用户登录界面。
用户默认状态为未登录状态,此时用户再MainActivity界面点击转发时会先跳转到登录界面,然后在登录界面登录成功后再回到MainActivity页面,此时,用户再进行转发操作就可以实现真正的转发功能。
先定义UserState接口,并定义了两个方法,即转发和评论。
package com.guifa.statedemo;
import android.content.Context;
/**
* 用户状态
*/
public interface UserState {
/**
* 转发
*
* @param context context
*/
public void forward(Context context);
/**
* 评论
*
* @param context context
*/
public void comment(Context context);
}
新建两个类并实现UserState接口,即用户已登录和未登录状态。
package com.guifa.statedemo;
import android.content.Context;
import android.widget.Toast;
/**
* 已登录状态
*/
public class LoginedState implements UserState {
@Override
public void forward(Context context) {
Toast.makeText(context, "转发微博", Toast.LENGTH_SHORT).show();
}
@Override
public void comment(Context context) {
Toast.makeText(context, "评论微博", Toast.LENGTH_SHORT).show();
}
}
package com.guifa.statedemo;
import android.content.Context;
import android.content.Intent;
/**
* 注销状态,即未登录状态
*/
public class logoutState implements UserState {
@Override
public void forward(Context context) {
gotoLoginActivity(context);
}
@Override
public void comment(Context context) {
gotoLoginActivity(context);
}
private void gotoLoginActivity(Context context) {
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
}
这里的LoginContext就是状态模式中的Context角色,是用户的操作对象和状态管理对象,LoginContext将相关操作委托给状态对象,这样状态发生改变时,LoginContext的行为就发生了改变。LoginContext通过setState来对状态进行修改,相关代码如下:
package com.guifa.statedemo;
import android.content.Context;
/**
* LoginContext,用户接口和状态管理类
*/
public class LoginContext {
/**
* 用户状态,默认为未登录状态
*/
UserState mState = new logoutState();
/**
* 单例
*/
static LoginContext sLoginContext = new LoginContext();
public LoginContext() {
}
public static LoginContext getLoginContext() {
return sLoginContext;
}
public void setState(UserState mState) {
this.mState = mState;
}
/**
* 转发
*
* @param context context
*/
public void forward(Context context) {
mState.forward(context);
}
/**
* 评论
*
* @param context context
*/
public void comment(Context context) {
mState.comment(context);
}
}
MainActivity的代码如下:
package com.guifa.statedemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 转发按钮
findViewById(R.id.forward_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 调用LoginContext的转发函数
LoginContext.getLoginContext().forward(MainActivity.this);
}
});
// 注销函数
findViewById(R.id.logout_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 设置为注销状态
LoginContext.getLoginContext().setState(new logoutState());
Toast.makeText(MainActivity.this, "注销成功", Toast.LENGTH_SHORT).show();
}
});
}
}
LoginActivity则是在用户输入用户名和密码之后执行登录,成功之后将LoginActivity的状态设置为已登录状态,并且返回MainActivity页面,具体代码如下:
package com.guifa.statedemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends AppCompatActivity {
private EditText userNameEditText;
private EditText pwdEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
userNameEditText = findViewById(R.id.username_edittext);
pwdEditText = findViewById(R.id.pwd_edittext);
findViewById(R.id.login_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
login();
finish();
}
});
}
private void login() {
String userName = userNameEditText.getText().toString().trim();
String pwd = pwdEditText.getText().toString().trim();
// 执行网络请求,进行登录...
// 登录成功后修改为已登录状态
LoginContext.getLoginContext().setState(new LoginedState());
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
}
}
Activity的XML文件很简单就不写了。最终的效果如下:
####总结
状态模式的关键点在于不同的状态下对于同一行为有不同的响应,这其实就是一个将if-else用多态来实现的一个具体示例。在if-else或者switch-case形式下根据不同的状态进行判断,如果是状态A那么执行方法A、状态B执行方法B,但这种实现使得逻辑耦合在一起,易于出错,通过状态模式能够很好地消除这类“丑陋”的逻辑处理,当然并不是任何出现if-else的地方都应该通过状态模式重构,模式的运用一定要考虑所处的情景以及你要解决的问题,只有符合特定的场景才建议使用对应的模式。
#####优点
State模式将所有与一个特定的状态相关的行为都放入一个状态对象中,它提供了一个更好的方法来组织与特定状态相关的代码,将烦琐的状态判断转换成结构清晰的状态类族,在避免代码膨胀的同时也保证了可扩展性与可维护性。
#####缺点
状态模式的使用必然会增加系统类和对象的个数。