状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。当控制一个对象的状态转换条件分支语句(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);
} }

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

设计模式之 -- 状态模式(State)的更多相关文章

  1. 【转】设计模式 ( 十七) 状态模式State(对象行为型)

    设计模式 ( 十七) 状态模式State(对象行为型) 1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ...

  2. 设计模式 ( 十七) 状态模式State(对象行为型)

    设计模式 ( 十七) 状态模式State(对象行为型) 1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ...

  3. 乐在其中设计模式(C#) - 状态模式(State Pattern)

    原文:乐在其中设计模式(C#) - 状态模式(State Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 状态模式(State Pattern) 作者:webabcd 介绍 允 ...

  4. 北风设计模式课程---状态模式State(对象行为型)

    北风设计模式课程---状态模式State(对象行为型) 一.总结 一句话总结: 状态模式 具体状态的行为在具体的状态类中就解决,不用交给外部做判断.实质是将多条件判断弄成了多个类,在不同的类中做判断 ...

  5. 二十四种设计模式:状态模式(State Pattern)

    状态模式(State Pattern) 介绍允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它所属的类. 示例有一个Message实体类,对它的操作有Insert()和Get()方法, ...

  6. 设计模式2——状态模式State

    参考链接: 设计模式之状态模式:https://www.cnblogs.com/haoerlv/p/7777789.html 设计模式系列之状态模式:https://www.jianshu.com/p ...

  7. [设计模式] 20 状态模式 State Pattern

    在GOF的<设计模式:可复用面向对象软件的基础>一书中对状态模式是这样说的:允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它的类.状态模式的重点在于状态转换,很多时候,对 ...

  8. 设计模式之状态模式(State)摘录

    23种GOF设计模式一般分为三大类:创建型模式.结构型模式.行为模式. 创建型模式抽象了实例化过程,它们帮助一个系统独立于怎样创建.组合和表示它的那些对象.一个类创建型模式使用继承改变被实例化的类,而 ...

  9. 大熊君说说JS与设计模式之------状态模式State

    一,总体概要 1,笔者浅谈 状态模式,又称状态对象模式(Pattern of Objects for States),状态模式是对象的行为模式. 状态模式主要解决的是当控制一个对象状态的条件表达式过于 ...

随机推荐

  1. topcoder SRM 623 DIV2 CatAndRat

    解决本题的一个关键点就是当Cat进入时,此时Rat在哪个位置? 注意移动方向可以随时改变,由于是圆环,故离入口最远点的距离是pi*R,即圆的一半, 当cat进入时(cat的速度大于rat的速度,否则不 ...

  2. sublime text 3 的在文件夹中查找的快捷键没有反应 的bug冲突

    11:19 2015/11/18 sublime text 3 的在文件夹中查找的快捷键没有反应 的bug冲突 在文件夹查找的快捷键:ctrl shift f没有反应,后来发现是百度输入法与它有问题, ...

  3. 【HDU】1536 S-Nim

    http://acm.hdu.edu.cn/showproblem.php?pid=1536 题意:同nim...多堆多询问...单堆n<=10000,每次取的是给定集合的数= = #inclu ...

  4. jquery 最简单的动画效果

    <p style="border: 1px solid red"> 我会慢慢变大 </p> <a>dianji</a> <sc ...

  5. AJAX-跨域解决之 JSONP

    (一)AJAX ajax 就是从某个文件中去找相关的数据,把数据拿过来以后,利用数据 分析数据 去做我们想做的事情    分两部分:拿数据                  用数据 oUsername ...

  6. SQLServer 客户端远程访问配置

    SQL2008报错“请验证实例名称是否正确并且SQL Server已配置为允许远程连接” 第一步: 连接远程服务器时SQL2008报错“请验证实例名称是否正确并且SQL Server已配置为允许远程连 ...

  7. TCP连接探测中的Keepalive和心跳包

    TCP连接探测中的Keepalive和心跳包 tcp keepalive 心跳 保活 Linuxtcp心跳keepalive保活1. TCP保活的必要性 1) 很多防火墙等对于空闲socket自动关闭 ...

  8. python成长之路——第一天

    一.python版本间的差异: 1.1:2.x与3.x版本对比 version 2.x 3.x print print " "或者print()打印都可以正常输出 只能print( ...

  9. URL编码:不同的操作系统、不同的浏览器、不同的网页字符集,将导致完全不同的编码结果。

    URL编码:不同的操作系统.不同的浏览器.不同的网页字符集,将导致完全不同的编码结果. 因此如果Url中有中文或特殊字符,一定要自己调用函数编码解码,不要让浏览器帮你编码,否则出现了问题会浪费你很多时 ...

  10. Windows Phone Data Protection

    To encrypt the PIN // Convert the PIN to a byte[]. byte[] PinByte = Encoding.UTF8.GetBytes(TBPin.Tex ...