设计模式之 -- 状态模式(State)

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

概述

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

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

2、解决原理

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

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

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

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

4、结构

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

设计模式之 -- 状态模式(State)

图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设计模式》有关状态模式的一节中提到一个经典的糖果机设计问题,其状态图如下图所示:设计模式之 -- 状态模式(State)

图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);
} }

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

上一篇:CP="CAO PSA OUR" 用P3P header解决iframe跨域访问cookie


下一篇:mysql中将查询结果进行拼接处理及concat、group_concat的使用