设计模式之 -- 状态模式(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),状态模式是对象的行为模式. 状态模式主要解决的是当控制一个对象状态的条件表达式过于 ...
随机推荐
- Kosaraju 算法
Kosaraju 算法 一.算法简介 在计算科学中,Kosaraju的算法(又称为–Sharir Kosaraju算法)是一个线性时间(linear time)算法找到的有向图的强连通分量.它利用了一 ...
- InterBase数据库迁移到MySQL(数据导出)
这篇我将记叙我的第二个脚本程序,这篇我使用InterBase数据库提供的“isql”命令来导出我所要的数据,但是由于“isql”命令没有直接导出数据的语句,说以我采用的是导入一个配置文件,在这个文件中 ...
- 深入浅出 - Android系统移植与平台开发(七)- 初识HAL
作者:唐老师,华清远见嵌入式学院讲师. 1. HAL的module与stub HAL(Hardware AbstractLayer)硬件抽象层是Google开发的Android系统里上层应用对底层硬件 ...
- 配置Nginx支持ThinkPHP的URL重写和PATHINFO
ThinkPHP支持通过PATHINFO和URL rewrite的方式来提供友好的URL,只需要在配置文件中设置 'URL_MODEL' => 2 即可.在Apache下只需要开启mod_rew ...
- 李洪强漫谈iOS开发[C语言-051]-判断整数位数
- spring security自定义拒绝访问页面
如果试图访问一个没有访问权限的页面,那么页面会出现403 访问错误. 如果我们希望自定义访问拒绝页面,只需要随便创建一个jsp页面,让后将这个页面的位置放到配置文件中. 下面创建一个accessDen ...
- C++复制对象时勿忘每一部分
现看这样一个程序: void logCall(const string& funcname) //标记记录 { cout <<funcname <<endl; } cl ...
- 计算alert弹出框的次数
(function() { var oldAlert = window.alert, count = 0; window.alert = function(a) { count++; oldAlert ...
- MyEclipse安装lombok
1. 将lombok.jar复制到myeclipse.ini所在的文件夹 2. 打开myeclipse.ini,插入以下两行: -Xbootclasspath/a:lombok.jar-javaage ...
- IOS第17天(2,Quartz2D图片剪裁变圆行图,和截屏图片)
**** #import "HMViewController.h" #import "UIImage+Tool.h" @interface HMViewCont ...