状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。当控制一个对象的状态转换条件分支语句(if...else或switch...case)过于复杂时,可以此模式将状态的判断逻辑转移到不同状态的一系列类中,将复杂的逻辑简单化,便于阅读与维护。

概述

1、为什么要使用状态模式?

   在软件开发过程中,应用程序可能会根据不同的条件作出不同的行为。常见的解决办法是先分析所有条件,通过大量的条件分支语句(if...else或switch...case)指定应用程序在不同条件下作出的不同行为。但是,每当增加一个条件时,就可能修改大量的条件分支语句,使得分支代码越来越复杂,代码的可读性、扩展性、可维护性也会越来越差,这时候就该状态模式粉墨登场了。

2、解决原理

   状态模式将大量的判断逻辑转移到表示不同状态的一系列中,从而消除了原先复杂的条件分支语句,降低了判断逻辑的复杂度。

3、状态模式适用的两种情况

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

   ② 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示;

4、结构

   状态模式的UML类图如图1所示:

图1 状态模式的UML类图

   由图可知:

   ① State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为;

   ② ConcreteState类,具体状态,每一个子类实现一个与Context的一个特定状态相关的行为;

   ③ Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态;

   实现代码如下:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace State
{
/*
* State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为
*/
abstract class State
{
public abstract void Handle(Context context);
} class ConcreteStateA : State
{
public override void Handle(Context context)
{
//这里写状态A的处理代码
//... //假设ConcreteStateA的下一个状态是ConcreteStateB
//此处状态定义可以在状态子类中指定,也可以在外部指定
context.setState(new ConcreteStateB());
}
} class ConcreteStateB : State
{
public override void Handle(Context context)
{
//这里写状态B的处理代码
//... //假假设ConcreteStateB的下一个状态是ConcreteStateA
//此处状态定义可以在状态子类中指定,也可以在外部指定
context.setState(new ConcreteStateA());
}
}
/*
* Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态
*/
class Context
{
private State state; public Context(State state)
{
this.state = state;
} public State getState()
{
return this.state;
} public void setState(State state)
{
this.state = state;
Console.WriteLine("当前状态:"+this.state.GetType().Name);
} //调用子类的对应方法
public void Request()
{
this.state.Handle(this);
}
}
}

5、状态模式带来的优点与效果

   ① 使得程序逻辑更加清晰、易维护。使用状态模式消除了大量的条件分支语句,将特定的状态相关的行为都放入一个State子类对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换;

   ② 它使得状态转换显示化。通过State对象表示不同的程序状态,比通过内部数据值来表示更加明确。而且数据转换状态有可能操作多个变量,而State对象转换只需更改状态实例,是一个原子操作,更加方便;

   ③ State对象可以被共享。不同Context对象可以共享一个State对象,这是使用内部数据变量表示状态很难实现的;

   此外,状态模式实现较为复杂,同时也会大大增加系统类和对象个数,建议在合适条件下引用。

从糖果机实例理解状态模式

   在著名的《Head First设计模式》有关状态模式的一节中提到一个经典的糖果机设计问题,其状态图如下图所示:

图2 糖果机设计状态图

   在此糖果机状态图,我们可以看出存在有四种状态和四种动作,这四种动作分别为:“投入25分钱”、“退回25分钱”、“转动曲柄”和“发放糖果”。如果糖果工程师让你来设计这个程序,那么作为一个聪明的程序员,你会怎么设计呢?

   首先,我们会用一个枚举来表示不同的状态,分别代表:售罄状态、售出状态、没有25分钱状态、有25分钱状态。

 private enum State {
SOLD_OUT, SOLD, NO_QUARTER, HAS_QUARTER
}

   然后在糖果机类体内定义一个内部状态变量state,用于记录糖果机当前所处的不同状态。然后在上述四种不同的动作方法内,根据此内部状态state的当前值来做出不同的处理。很快,糖果机很快就设计好了,代码如下:

 package state.candymachine;

 public class CandyMachine{

     //四种状态,分别代表:售罄状态、售出状态、没有25分钱状态、有25分钱状态
private enum State {
SOLD_OUT, SOLD, NO_QUARTER, HAS_QUARTER
} private State state = State.NO_QUARTER;
private int candyNums = 0; public CandyMachine(int candyNums) {
this.candyNums = candyNums;
if (candyNums > 0) {
this.state = State.NO_QUARTER;
} else {
this.state = State.SOLD_OUT;
}
} public State getState() {
return state;
} public void setState(State state) {
this.state = state;
switch(this.state){
case SOLD:
System.out.println("糖果已经为您准备好,请点击售出糖果按钮..");
break;
case SOLD_OUT:
System.out.println("本糖果机所有糖果已经售罄,尽情下次光临哦~~~");
break;
case NO_QUARTER:
System.out.println("机器已经准备完毕,请您投入25分钱购买糖果~~~");
break;
case HAS_QUARTER:
System.out.println("请您选择操作:退回25分钱 or 转动曲柄....");
break;
}
} public int getCandyNums() {
return candyNums;
} public void setCandyNums(int candyNums) {
this.candyNums = candyNums;
} public void trunCrank() {
if (state == State.HAS_QUARTER) {
System.out.println("曲柄已经开始转动,您的糖果即将出炉,尽请稍候~~~");
setState(State.SOLD);
} else {
System.out.println("无法转动曲柄,您还未投入25分钱呢");
}
} public void dispenseCandy() {
if (state == State.SOLD) {
System.out.println("发放糖果1颗,尽情享受吧...");
this.candyNums = this.candyNums - 1;
if (this.candyNums > 0) {
setState(State.NO_QUARTER);
} else {
setState(State.SOLD_OUT);
}
}else{
System.out.println("无法发放糖果,请先转动曲柄");
}
} public void insertQuarter() {
if(state == State.NO_QUARTER){
System.out.println("成功投入25分钱,您的糖果已经在等您了哦~~");
setState(State.HAS_QUARTER);
}else{
System.out.println("无法投入25分钱,机器中已经有25分钱了");
}
} public void ejectQuarter(){
if(state == State.HAS_QUARTER){
System.out.println("您的25分钱已经退回,欢迎下次光临~~~");
setState(State.NO_QUARTER);
}else{
System.out.println("无法退回25分钱,您还未投入钱呢");
}
} }

   现在我们来测试它是否能正常工作:

 package state.candymachine;

 import java.util.Scanner;

 public class MachineTest {

     public static void main(String[] args) {
CandyMachine machine = new CandyMachine(3);
while (machine.getCandyNums() > 0) {
System.out.println("当前糖果机还剩" + machine.getCandyNums() + "颗糖果");
System.out.println("请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果");
Scanner sc = new Scanner(System.in);
int op = sc.nextInt();
if (op == 1)
machine.insertQuarter();
else if (op == 2)
machine.ejectQuarter();
else if (op == 3)
machine.trunCrank();
else if (op == 4)
machine.dispenseCandy();
else
System.out.println("输入有误,请重新输入...");
} }
}

   经过一番简单的测试,糖果机能正常工作,测试明细如下:

机器已经准备完毕,请您投入25分钱购买糖果~~~
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
1
【操作成功】成功投入25分钱,您的糖果已经在等您了哦~~
请您选择操作:退回25分钱 or 转动曲柄....
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
2
【操作成功】您的25分钱已经退回,欢迎下次光临~~~
机器已经准备完毕,请您投入25分钱购买糖果~~~
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
1
【操作成功】成功投入25分钱,您的糖果已经在等您了哦~~
请您选择操作:退回25分钱 or 转动曲柄....
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
1
无法投入25分钱,机器中已经有25分钱了
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
3
【操作成功】曲柄已经开始转动,您的糖果即将出炉,尽请稍候~~~
糖果已经为您准备好,请点击售出糖果按钮..
当前糖果机还剩1颗糖果
请您选择您要执行的操作:1-投入 25分钱 2-退回25分钱 3-转动曲柄 4-发放糖果
4
【操作成功】发放糖果1颗,尽情享受吧...
本糖果机所有糖果已经售罄,尽情下次光临哦~~~

   看到代码中大量的if...else了吗,有没有觉得它们很不雅观?如果在此基础上,糖果机增加几个状态与动作,那么将会出现更大一大拨if...else,极大地降低了代码的可读性,提高了维护成本。

   那么,如何使用State模式来重构此程序呢?

   首先要定义一个State基类,包含上述四种动作。然后再分别定义四种不同状态的State子类,分别是:SoldState、SoldOutState、NoQuarterState和HasQuarterState,分别在对应的状态子类中实现不同的动作。

   重构后的State以及其不同子类如下所示:

 package state.candymachine;

 public class State {

     // 转动曲柄
public void trunCrank(CandyMachine machine) {
System.out.println("无法转动曲柄,请先投入25分钱");
} // 发放糖果
public void dispenseCandy(CandyMachine machine) {
System.out.println("无法发放糖果,请先转动曲柄");
} // 投入25分钱
public void insertQuarter(CandyMachine machine) {
System.out.println("无法投入25分钱,机器中已经有25分钱了");
} // 退回25分钱
public void ejectQuarter(CandyMachine machine) {
System.out.println("无法退回25分钱,您还未投入钱呢");
} } /**
* 售出糖果状态
* 本次售出后 糖果=0则转入“糖果售罄”状态 糖果>0则转入“无25分钱”状态
*/
class SoldState extends State { public SoldState() {
System.out.println("糖果已经为您准备好,请点击售出糖果按钮..");
} @Override
public void dispenseCandy(CandyMachine machine) {
System.out.println("【操作成功】发放糖果1颗,尽情享受吧...");
int currCandyNums = machine.getCandyNums() - 1;
machine.setCandyNums(currCandyNums);
if (currCandyNums > 0) {
machine.setState(new NoQuarterState());
} else {
machine.setState(new SoldOutState());
}
}
} // 售罄状态
class SoldOutState extends State {
public SoldOutState(){
System.out.println("本糖果机所有糖果已经售罄,尽情下次光临哦~~~");
}
} // 无25分钱状态
class NoQuarterState extends State { public NoQuarterState() {
System.out.println("机器已经准备完毕,请您投入25分钱购买糖果~~~");
} @Override
public void insertQuarter(CandyMachine machine) {
System.out.println("【操作成功】成功投入25分钱,您的糖果已经在等您了哦~~");
machine.setState(new HasQuarterState());
}
} // 有25分钱状态
class HasQuarterState extends State { public HasQuarterState() {
System.out.println("请您选择操作:退回25分钱 or 转动曲柄....");
} @Override
public void trunCrank(CandyMachine machine) {
System.out.println("【操作成功】曲柄已经开始转动,您的糖果即将出炉,尽请稍候~~~");
machine.setState(new SoldState());
}
@Override
public void ejectQuarter(CandyMachine machine) {
System.out.println("【操作成功】您的25分钱已经退回,欢迎下次光临~~~");
machine.setState(new NoQuarterState());
}
}

   然后,在糖果机类中使用State的一个实例对象来记录当前的状态,对于四种动作则分别交给当前State实例对象来处理。

   重构后的糖果机类CandyMachine如下所示:

 package state.candymachine;

 public class CandyMachine {

     private State state;
private int candyNums = 0; public CandyMachine(int candyNums) {
this.candyNums = candyNums;
if (candyNums > 0) {
setState(new NoQuarterState());
} else {
setState(new SoldOutState());
}
} public State getState() {
return state;
} public void setState(State state) {
this.state = state;
} public int getCandyNums() {
return candyNums;
} public void setCandyNums(int candyNums) {
this.candyNums = candyNums;
} public void trunCrank(){
this.state.trunCrank(this);
} public void dispenseCandy(){
this.state.dispenseCandy(this);
} public void insertQuarter(){
this.state.insertQuarter(this);
} public void ejectQuarter(){
this.state.ejectQuarter(this);
} }

   在重构后的代码中,不存在任何的条件分支语句,代码有了很好的可读性,也漂亮了许多,是么....

最新文章

  1. 浅谈ARP协议以及应用
  2. SSAS动态添加分区 (转载)
  3. Freemaker 自定义函数
  4. [deviceone开发]-echart的简单报表示例
  5. C#错误之 System.Threading.ThreadAbortException:正在中止线程
  6. 深度优先搜索 codevs 1031 质数环
  7. View (四)视图状态及重绘流程分析
  8. Mysql 解决left join 数据重复的问题
  9. JBPM4 常用表结构
  10. Java标准输入输出流的重定向及恢复
  11. js模板引擎实现原理
  12. DateTime格式大全
  13. php和表单(1)
  14. Elasticsearch索引自动删除
  15. SharePoint 2010 之寻找页面布局
  16. vue路由详解
  17. ionic3使用echarts
  18. centos7如何添加开机启动服务/脚本
  19. Principal components analysis(PCA):主元分析
  20. CSS学习笔记:盒子模型

热门文章

  1. HDU 1251 Trie树模板题
  2. 20145330《Java程序设计》课程总结
  3. Hibernate学习笔记2
  4. 转载:C#中的Invoke理解一
  5. java分享第三天(异常)
  6. spring security 管理会话 多个用户不可以使用同一个账号登录系统
  7. 矩形的个数-nyoj206
  8. java中hashCode方法与equals方法的用法总结
  9. LaTex 数学公式
  10. wordpress 导航相关的函数