一、介绍
状态模式中的行为是由状态来决定的,不同的状态下有不同的行为。状态模式和策略模式的结构几乎完全一样,但它们的目的、本质却完全不一样。状态模式的行为是平行的、不可替换的,策略模式的行为是彼此独立、可相互替换的。用一句话来表述,状态模式把对象的行为包装在不同的状态对象里,每一个状态对象都有一个共同的抽象状态基类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。
二、定义
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
三、使用场景
(1)一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。
(2)代码中包含大量与对象状态有关的条件语句,例如,一个操作中含有庞大的多分支语句(if-else或switch-case),且这些分支依赖于该对象的状态。
状态模式将每一个条件分支放入一个独立的类中,这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖与其他对象而独立变化,这样通过多态去除过多的、重复的if-else等分支语句。
四、状态模式的UML类图
UML类图:
角色介绍:
Context:环境类,定义客户感兴趣的接口,维护一个State子类的实例,这个实例定义了对象的当前状态。
State:抽象状态类或状态接口,定义一个或者一组接口,表示该状态下的行为。
ConcreteStateA、ConcreteStateB:具体状态类,每一个具体的状态类实现抽象State中定义的接口,从而达到不同状态下的不同行为。
五、简单示例
下面我们就以电视遥控器为例来演示一下状态模式的实现。我们首先将电视的状态简单分为开机状态和关机状态,在开机状态下可以通过遥控器进行频道切换、调整音量等操作,但是,此时重复按开机键是无效的;而在关机状态下,频道切换、调整音量、关机都是无效的操作,只有按开机按钮时才会生效。也就是说电视的内部状态决定了遥控器的行为。
首先是普通的实现方法:
public class TVController {//开机状态private final static int POWER_ON = 1;//关机状态private final static int POWER_OFF = 2; //默认状态private int mState = POWER_OFF;public void powerOn(){if(mState ==POWER_OFF){System.out.println("电视开机了");}mState = POWER_ON;}public void powerOff(){if(mState ==POWER_ON){System.out.println("电视关机了");}mState = POWER_OFF;}public void nextChannel(){if(mState ==POWER_ON){System.out.println("下一频道");}else{System.out.println("没有开机");}}public void prevChannel(){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("没有开机");}}
}
可以看到,每次执行通过判断当前状态来进行操作,部分的代码重复,假设状态和功能增加,就会越来越难以维护。这时可以使用状态模式,如下:
电视的状态接口:
/*** 电视状态接口,定义了电视的操作函数* **/
public interface TVState {public void nextChannel();public void prevChannel();public void turnUp();public void turnDown();}
关机状态:
/*** * 关机状态,操作无结果* * */
public class PowerOffState implements TVState{@Overridepublic void nextChannel() {}@Overridepublic void prevChannel() {}@Overridepublic void turnUp() {}@Overridepublic void turnDown() {}
}
开机状态:
/*** * 开机状态,操作有效* * */
public class PowerOnState implements TVState{@Overridepublic void nextChannel() {System.out.println("下一频道");}@Overridepublic void prevChannel() {System.out.println("上一频道");}@Overridepublic void turnUp() {System.out.println("调高音量");}@Overridepublic void turnDown() {System.out.println("调低音量");}
}
电源操作接口:
/*** 电源操作接口* * */
public interface PowerController {public void powerOn();public void powerOff();
}
电视遥控器:
/*** 电视遥控器* * */
public class TVController implements PowerController{TVState mTVState;public void setTVState(TVState mTVState){this.mTVState = mTVState;}@Overridepublic void powerOn() {setTVState(new PowerOnState());System.out.println("开机了");}@Overridepublic 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();}
}
调用:
public class Client {public static void main(String[] args) {TVController tvController = new TVController();//设置开机状态tvController.powerOn();//下一频道tvController.nextChannel();//调高音量tvController.turnUp();//关机tvController.powerOff();//调低音量,此时不会生效tvController.turnDown();}
}
输出结果如下:
开机了
下一频道
调高音量
关机了
上述实现中,我们抽象了一个TVState接口,该接口中有操作电视的所有函数,该接口有两个实现类,即开机状态(PowerOnState)和关机状态(PowerOffState)。开机状态下只有开机功能是无效的,也就是说在已经开机的时候用户在按开机键不会产生任何反应;而在关机状态下,只有开机功能是可用的,其他功能都不会生效。同一个操作,如调高音量的turnUp函数,在关机状态下无效,在开机状态下就会将电视的音量调高,也就是说电视内部状态影响了电视遥控器的行为。状态模式将这些行为封装到状态类中,在进行操作时将这些功能转发给状态对象,不同的状态有不同的实现,这样就通过多态的形式去除了重复、杂乱的if-else语句,这也正是状态模式的精髓所在。
六、Android实战中的使用
1、登录系统,根据用户是否登录,判断事件的处理方式。
2、Wi-Fi管理,在不同的状态下,WiFi的扫描请求处理不一。
下面以登录系统为例讲解下状态模式在实战中的使用:
在android开发中,我们遇到登录界面是十分常见的,而状态设计模式在登录界面的应用十分广泛,用户在登录状态下和未登录状态下,对逻辑的操作是不一样的。例如最常见的情况就是在玩新浪微博的时候,用户在登录的情况下才能完成评论和转发微博的操作;而当用户处于未登录的情况下要执行转发和评论微博的操作需要进入登录界面登录以后才能执行,所以面对这两者不同的状况,利用状态设计模式来设计这个例子最好不过。
1、状态基类
前面我们讲过状态设计模式的原理实则是多态,在这里我们用UserState接口表示此基类,包换转发操作和评论这两种状态,代码如下:
public interface UserState {/*** 转发操作* @param context*/public void forword(Context context);/*** 评论操作* @param context*/public void commit(Context context);
}
2、用户在登录和未登录两种状况下的实现类LoginState和LogoutState;代码如下:
在LoginState.java中,用户是可以执行转发和评论操作。
public class LoginState implements UserState{@Overridepublic void forword(Context context) {Toast.makeText(context, "转发成功", Toast.LENGTH_SHORT).show();}@Overridepublic void commit(Context context) {Toast.makeText(context, "评论成功", Toast.LENGTH_SHORT).show();}
}
在LogoutState.java中,用户在未登录的情况下不允许执行操作,而是应该跳转到登录界面执行登录以后才可以执行。
public class LogoutState implements UserState{/*** 跳转到登录界面登录以后才能转发*/@Overridepublic void forword(Context context) {gotoLohinActivity(context);}/*** 跳转到登录界面登录以后才能评论*/@Overridepublic void commit(Context context) {gotoLohinActivity(context);}/*** 界面跳转操作* @param context*/private void gotoLohinActivity(Context context){context.startActivity(new Intent(context,LoginActivity.class));}
}
3、操作角色LoginContext
这里的LoginContext就是在状态模式的Context角色,是用户操作对象和管理对象,LoginContext委托相关的操作给状态对象,在其中状态的发生改变,LoginContext的行为也发生改变。LoginContext的代码如*下:
温馨提示:
这里我们用到单例就是为了全局只有一个LoginContext去控制用户状态;
public class LoginContext {//用户状态默认为未登录状态UserState state = new LogoutState();private LoginContext(){};//私有构造函数,避免外界可以通过new 获取对象//单例模式public static LoginContext getInstance(){return SingletonHolder.instance;}/***静态代码块*/private static class SingletonHolder{private static final LoginContext instance = new LoginContext();}public void setState(UserState state){this.state = state;}//转发public void forward(Context context){state.forword(context);}//评论public void commit(Context context){state.commit(context);}
}
4、界面展示
LoginActivity.java,此界面执行登录操作,登录成后把 LoginContext.getInstance().setState(new LoginState());设置为登录状态,在MainActivity中就执行的是登录状态下的操作,即可以转发可评论;
public class LoginActivity extends Activity implements OnClickListener{private static final String LOGIN_URL = "http://10.10.200.193:8080/Day01/servlet/LoginServlet";private EditText et_username;private EditText et_password;private Button btn_login;private String username;private String password;private KJHttp http;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_login);initView();initData();}private void initView() {et_username = (EditText) findViewById(R.id.et_username);et_password = (EditText) findViewById(R.id.et_password);btn_login = (Button) findViewById(R.id.btn_login);btn_login.setOnClickListener(LoginActivity.this);}private void initData() {http = new KJHttp();}/*** 执行登录操作* * @param username2* @param password2*/protected void sendLogin(String username2, String password2) {HttpParams params = new HttpParams();params.put("username", "user1");params.put("password", "123456");http.post(LOGIN_URL, params, new HttpCallBack() {@Overridepublic void onSuccess(String t) {if ("200".equals(t)) {//设置为登录状态LoginContext.getInstance().setState(new LoginState());startActivity(new Intent(LoginActivity.this,MainActivity.class));finish();Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();}}});}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btn_login:username = et_username.getEditableText().toString().trim();password = et_password.getEditableText().toString().trim();if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {Toast.makeText(LoginActivity.this, "用户名密码不能为空", Toast.LENGTH_SHORT).show();return;}sendLogin(username, password);break;}}}
MainActivity.java,在用户登录成功后,点击转发和评论执行的是登录状态下的操作,而当用户注销时,我们把LoginContext的状态设置为未登录状态;LoginContext.getInstance().setState(new LogoutState());此时在点击转发和评论操作时就会跳到用户登录界面。
public class MainActivity extends Activity {private Button btn_forward;private Button btn_commit;private Button btn_logout;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();initListener();}private void initView() {btn_forward = (Button) findViewById(R.id.btn_forward);btn_commit = (Button) findViewById(R.id.btn_commit);btn_logout = (Button) findViewById(R.id.btn_logout);}private void initListener() {//转发操作btn_forward.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {//调用LoginContext里面的转发函数LoginContext.getInstance().forward(MainActivity.this);}});//评论操作btn_commit.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {//调用LoginContext里面的转发函数LoginContext.getInstance().commit(MainActivity.this);}});//注销操作btn_logout.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {//设置为注销状态LoginContext.getInstance().setState(new LogoutState());}});}
}
七、总结
状态模式的关键点在于不同的状态下对于同一行为有不同的响应,这其实就是一个将if-else用多态来实现的一个具体示例。在if-else或者switch-case形式下根据不同的状态进行判断,如果是状态A那么执行方法A,状态B执行方法B,但这种实现使得逻辑耦合在一起,易于出错,通过状态模式能够很好的消除这类”丑陋“的逻辑处理,当然并不是任何出现if-else的地方都应该通过状态模式重构,模式的运用一定要考虑所处的情景以及你要解决的问题,只有符合特定的场景才建议使用对应的模式。
优点:
- 将所有与一个特定的状态相关的行为都放入一个状态对象中,它提供了一个更好的方法来组织与特定状态相关的代码,将繁琐的状态判断转换成结构清晰的状态类族,在避免代码膨胀的同时也保证了可扩展性与可维护性。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。