设计模式之 -- 状态模式(State)
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。当控制一个对象的状态转换条件分支语句(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)的更多相关文章
- 【转】设计模式 ( 十七) 状态模式State(对象行为型)
设计模式 ( 十七) 状态模式State(对象行为型) 1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ...
- 设计模式 ( 十七) 状态模式State(对象行为型)
设计模式 ( 十七) 状态模式State(对象行为型) 1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ...
- 乐在其中设计模式(C#) - 状态模式(State Pattern)
原文:乐在其中设计模式(C#) - 状态模式(State Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 状态模式(State Pattern) 作者:webabcd 介绍 允 ...
- 北风设计模式课程---状态模式State(对象行为型)
北风设计模式课程---状态模式State(对象行为型) 一.总结 一句话总结: 状态模式 具体状态的行为在具体的状态类中就解决,不用交给外部做判断.实质是将多条件判断弄成了多个类,在不同的类中做判断 ...
- 二十四种设计模式:状态模式(State Pattern)
状态模式(State Pattern) 介绍允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它所属的类. 示例有一个Message实体类,对它的操作有Insert()和Get()方法, ...
- 设计模式2——状态模式State
参考链接: 设计模式之状态模式:https://www.cnblogs.com/haoerlv/p/7777789.html 设计模式系列之状态模式:https://www.jianshu.com/p ...
- [设计模式] 20 状态模式 State Pattern
在GOF的<设计模式:可复用面向对象软件的基础>一书中对状态模式是这样说的:允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它的类.状态模式的重点在于状态转换,很多时候,对 ...
- 设计模式之状态模式(State)摘录
23种GOF设计模式一般分为三大类:创建型模式.结构型模式.行为模式. 创建型模式抽象了实例化过程,它们帮助一个系统独立于怎样创建.组合和表示它的那些对象.一个类创建型模式使用继承改变被实例化的类,而 ...
- 大熊君说说JS与设计模式之------状态模式State
一,总体概要 1,笔者浅谈 状态模式,又称状态对象模式(Pattern of Objects for States),状态模式是对象的行为模式. 状态模式主要解决的是当控制一个对象状态的条件表达式过于 ...
随机推荐
- Codeforces Round #249 (Div. 2) A - Queue on Bus Stop
水题 #include <iostream> #include <vector> #include <algorithm> using namespace std; ...
- ACM: 限时训练题解-Rock-Paper-Scissors-前缀和
Rock-Paper-Scissors Rock-Paper-Scissors is a two-player game, where each player chooses one of Roc ...
- 彻底弄明白之java多线程中的volatile
一. volatite 简述 Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新. ...
- HDU 3652 B-number(数位DP)
题目链接 学习大神的数位DP模版. #include <iostream> #include <cstdio> #include <cstring> using n ...
- OSG-OSGEarth
OSG-OSGEarth 初次使用Cmake——以OsgEarth工程创建为例 转:http://www.cnblogs.com/Realh/archive/2012/02/08/2342507.ht ...
- Win7 系统下 Firefox hostadmin插件无法修改Host
问题: win 7系统,今天用杀毒软件杀了一下毒,firefox hostAdmin插件无法修改Host了,提示“ write hosts file failed check permissions ...
- Number Sequence
Number Sequence A number sequence is defined as follows: f(1) = 1, f(2) = 1, f(n) = (A * f(n - 1) ...
- [CareerCup] 17.9 Word Frequency in a Book 书中单词频率
17.9 Design a method to find the frequency of occurrences of any given word in a book. 这道题让我们找书中单词出现 ...
- 无法在提交表单前通过ajax验证解决办法
博主在一个小项目中,要实现注册表单无刷新验证用户名或密码,但是发现不管怎么样都无法在先通过ajax验证之前不提交表单. 例如:一个简单的验证函数 function check(){ $.post(&q ...
- hibernate 中如何用注解映射定长字符类型char(2)
如果是用xml的方式配置映射,可以在<column>标签的sql-type属性中设置char(2),比如: <property name="age" type=& ...