浅谈设计模式 - 状态模式(十三)
前言
状态模式其实也是一个十分常见的模式,最常见的应用场景是线程的状态切换,最常使用的方式就是对于If/else进行解耦,另外这个模式可以配合 责任链模式组合搭配出各种不同的状态切换效果,可以用设计模式模拟一个简单的“工作流”。
优缺点:
状态模式非常明显的是用于解耦大量If/else的模式,所以他的优点十分突出,就是可以简化大量的if/else判断,但是缺点页十分明显,那就是程序的执行受限于状态,假如状态的常量非常多的情况下,依然会出现大量的if/else的现象,状态模式和策略模式一样一旦情况十分复杂的时候很容易造成类的膨胀,当然多数情况下这种缺点几乎可以忽略,总比太多。
对于状态模式在jdk1.8之后的lambada表达式中可以有体现,lambada实现了java的参数“方法化”,这样极大地简化了类的膨胀,但是可能比较难以理解,并且一旦复杂依然建议使用状态来实现切换,这样更方便维护。
状态模式的结构图:
下面是状态模式的结构图,本身比较简单,似乎并没有什么特别的地方,但是当我们和“策略模式”对比之后,似乎很容易混淆,下面我们来看下这两个结构图的对比:
状态模式结构图:
策略模式结构图:
通过对比可以看到,状态和策略这两个模式是十分相似的,那么我们应该如何区分这两个模式呢?在平时的开发工作中,如果一个对象有很多种状态,并且这个对象在不同状态下的行为也不一样,那么我们就可以使用状态模式来解决这个问题,但是如果你让同一事物在不同的时刻有不同的行为,可以使用策略模式触发不同的行为。打个比方,如果你想让开关出现不同的行为,你需要设计两个状态开关,然后在事件处理的时候将逻辑分发到不同的状态完成触发,而如果你想要实现类似商品的折扣或者优惠促销,满减等等“模式”的时候更加适合使用策略,当然如果无法区分也没有关系,哪个使用更为熟练即可。
案例:糖果机
这是《head first设计模式》中关于状态模式案例当中的糖果机,我们可以从下面的图中看到如果使用单纯的代码来完成下面的工作,就会出现非常多尴尬的情况,比如大量繁杂的if/else代码充斥,下面我们来看下关于这个糖果机按照普通的方式要如何设计?
不使用设计模式
不使用设计模式的情况下,我们通常的做法是定义状态常量,比如设置枚举或者直接设置final的标示位等等,我们
首先我们需要划分对象,糖果机和糖果,糖果机包含硬币的总钱数,糖果的数量等等。
- 定义四个状态:售罄,售出中,存在硬币,没有硬币
为了实现状态的实现,我们需要设计类似枚举的常量来表示糖果机的状态。
- 状态设置为常量,而糖果机需要内置机器的状态
- 最后用逻辑代码和判断让糖果机内部进行工作,当然这会出现巨多的if/else判断。
最后我们的代码表现形式如下,用传统的模式我们很可能写出类似的代码:
MechanicaState:定义了糖果机的状态,当然可以作为糖果机的私有内部类定义使用,也可以设计为枚举,这里偷懒设计为一个常量类的形式。
/**
* 机器状态
*/
public final class MechanicaState {
/**
* 售罄
*/
public static final int SOLD_OUT = 0;
/**
* 存在硬币
*/
public static final int HAS = 1;
/**
* 没有硬币
*/
public static final int NOT = 2;
/**
* 售出糖果中
*/
public static final int SOLD = 4;
}
CandyMechaica:糖果器,包含了糖果内部的工作方法,可以看到有非常多冗余的If/else判断:
/**
* 糖果机
*/
public class CandyMechanica {
/**
* 默认是售罄的状态
*/
private int sold_state = SOLD_OUT;
/**
* 糖果数量
*/
private int count = 0;
public CandyMechanica(int count) throws InterruptedException {
if(count <= 0){
throw new InterruptedException("初始化失败");
}else {
sold_state = NOT;
}
System.out.println("欢迎光临糖果机,当前糖果机的糖果数量为:"+ count);
this.count = count;
}
/**
* 启动糖果机
*/
public void startUp(){
switch (sold_state){
case NOT:
System.out.println("当前糖果机没有硬币,请先投入硬币");
break;
case SOLD:
System.out.println("糖果售出中,请稍后");
break;
case HAS:
sold_state = SOLD;
candySold();
break;
case SOLD_OUT:
System.out.println("糖果已售罄");
break;
}
}
/**
* 投入硬币的操作
*/
public void putCoin(){
switch (sold_state){
case NOT:
sold_state = HAS;
System.out.println("投入硬币成功,请开启糖果机");
break;
case SOLD:
System.out.println("糖果售出中,请勿重复投放");
break;
case HAS:
System.out.println("当前已经存在硬币,请勿重复投放");
break;
case SOLD_OUT:
System.out.println("糖果已售罄,您投入的硬币将会在稍后退回");
break;
}
}
/**
* 售出糖果
*/
public void candySold(){
switch (sold_state){
case NOT:
System.out.println("当前机器内没有硬币,请先投入硬币");
break;
case SOLD:
System.out.println("糖果已售出,请取走您的糖果");
count--;
if(count == 0){
System.out.println("当前糖果已经售罄");
sold_state = SOLD_OUT;
}
sold_state = NOT;
break;
case HAS:
sold_state = NOT;
System.out.println("当前已经存在硬币,请勿重复投放");
break;
}
}
}
最后就是对于上面的糖果机器进行简单的单元测试:
/**
* 单元测试
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
CandyMechanica candyMechanica = new CandyMechanica(5);
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
}
}/*运行结果:
欢迎光临糖果机,当前糖果机的糖果数量为:5
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
当前糖果机没有硬币,请先投入硬币
投入硬币成功,请开启糖果机
当前已经存在硬币,请勿重复投放
糖果已售出,请取走您的糖果
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
当前糖果已经售罄
Process finished with exit code 0
*/
使用状态模式重构
接着我们使用状态模式来重构一下上面的代码,我们重点关注CandyMechanica
这个类,他的三个方法耦合了大量的if/else判断,在编写这种代码的时候不仅会使得代码十分的死板,而且很容易出错,我想没有人会喜欢写上面这样的代码,所以下面我们使用状态模式看下如何重构:
糖果机分为四个状态,但是他们有着类似的行为:推入硬币,启动机器,推出糖果这三个方法
- 使用接口的形式定时状态的公用行为
- 我们把上面三个状态抽取为状态的公用方法,但是context在哪里?
- context在这里的表现形式为糖果机,我们使用在状态内部组合糖果机的形式实现糖果机的状态“切换”。
- 注意:由于使用了内部类的内置形式,所以有时候很多判断可以简化,更多的时候建议抽出来作为单独的类。
最后他的表现形式如下:
CandyState:糖果机分为四个状态,但是他们有着类似的行为:推入硬币,启动机器,推出糖果这三个方法
/**
* 糖果状态
*/
public interface CandyState {
/**
* 启动糖果机
*/
void startUp();
/**
* 投入硬币
*/
void putCoin();
/**
* 推出糖果
*/
void candySold();
}
糖果机还是很复杂,当然并不像是上面的形式:
CandyMechanica:重写之后的糖果机,此糖果机把所有的状态解耦并且抽取为对象的形式。
/**
* 状态模式重写糖果机
*/
public class CandyMechanica implements CandyState {
private int count;
/**
* 当前状态
*/
private CandyState nowState;
// 有硬币
private CandyState hasState;
// 无硬币
private CandyState notState;
// 售罄
private CandyState solidOutState;
// 售出中
private CandyState solidState;
public CandyMechanica(int count) throws InterruptedException {
notState = new NotState(this);
solidOutState = new SoldOutState(this);
hasState = new HasState(this);
solidState = new SoldOutState(this);
if (count <= 0) {
throw new InterruptedException("初始化失败");
} else {
nowState = notState;
}
this.count = count;
}
@Override
public void startUp() {
nowState.startUp();
}
@Override
public void putCoin() {
nowState.putCoin();
}
@Override
public void candySold() {
nowState.candySold();
}
/**
*
*/
public static class HasState implements CandyState {
private CandyMechanica candyMechanica;
public HasState(CandyMechanica candyMechanica) {
this.candyMechanica = candyMechanica;
}
@Override
public void startUp() {
candyMechanica.nowState = candyMechanica.solidState;
candyMechanica.candySold();
System.out.println("糖果售出中,请稍后");
}
@Override
public void putCoin() {
System.out.println("当前已有糖果,请勿重复投入");
}
@Override
public void candySold() {
System.out.println("糖果已售罄");
}
}
/**
* 售罄状态
*/
public static class SoldOutState implements CandyState {
private CandyMechanica candyMechanica;
public SoldOutState(CandyMechanica candyMechanica) {
this.candyMechanica = candyMechanica;
}
@Override
public void startUp() {
System.out.println("糖果已售罄");
}
@Override
public void putCoin() {
System.out.println("糖果已售罄,您投入的硬币将会在稍后退回");
}
@Override
public void candySold() {
System.out.println("糖果已售罄");
}
}
/**
* 售出状态
*/
public static class SoldState implements CandyState {
private CandyMechanica candyMechanica;
public SoldState(CandyMechanica candyMechanica) {
this.candyMechanica = candyMechanica;
}
@Override
public void startUp() {
System.out.println("糖果售出中,请稍后");
}
@Override
public void putCoin() {
System.out.println("糖果售出中,请勿重复投放");
}
@Override
public void candySold() {
System.out.println("糖果已售出,请取走您的糖果");
candyMechanica.count--;
if (candyMechanica.count == 0) {
System.out.println("当前糖果已经售罄");
candyMechanica.nowState = candyMechanica.solidOutState;
}
candyMechanica.nowState = candyMechanica.notState;
}
}
/**
* 无硬币状态
*/
public static class NotState implements CandyState {
private CandyMechanica candyMechanica;
public NotState(CandyMechanica candyMechanica) {
this.candyMechanica = candyMechanica;
}
@Override
public void startUp() {
System.out.println("当前糖果机没有硬币,请先投入硬币");
}
@Override
public void putCoin() {
candyMechanica.nowState = candyMechanica.hasState;
System.out.println("投入硬币成功,请开启糖果机");
}
@Override
public void candySold() {
System.out.println("当前机器内没有硬币,请先投入硬币");
}
}
}
最后是单元测试的部分:
/**
* 单元测试
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
CandyMechanica candyMechanica = new CandyMechanica(5);
candyMechanica.putCoin();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.candySold();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
candyMechanica.putCoin();
candyMechanica.startUp();
}
}/*运行结果:
欢迎光临糖果机,当前糖果机的糖果数量为:5
投入硬币成功,请开启糖果机
当前已经存在硬币,请勿重复投放
糖果已售出,请取走您的糖果
当前机器内没有硬币,请先投入硬币
当前糖果机没有硬币,请先投入硬币
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
投入硬币成功,请开启糖果机
糖果已售出,请取走您的糖果
当前糖果已经售罄
*/
总结
本文的代码比较多,状态模式也是和策略一样,只要看一眼样例代码即可。
下面我们来具体总结一下状态模式的特点,使用状态模式的优势有以下几个方面:
- 将应用的代码解耦,利于阅读和维护。我们可以看到,在第一种方案中,我们使用了大量的
if/else
来进行逻辑的判断,将各种状态和逻辑放在一起进行处理。在我们应用相关对象的状态比较少的情况下可能不会有太大的问题,但是一旦对象的状态变得多了起来,这种耦合比较深的代码维护起来简直就是噩梦。 - 将变化封装进具体的状态对象中,相当于将变化局部化,并且进行了封装。利于以后的维护与拓展。使用状态模式之后,我们把相关的操作都封装进对应的状态中,如果想修改或者添加新的状态,也是很方便的。对代码的修改也比较少,扩展性比较好。
- 通过组合和委托,让对象在运行的时候可以通过改变状态来改变自己的行为。我们只需要将对象的状态图画出来,专注于对象的状态改变,以及每个状态有哪些行为。这让我们的开发变得简单一些,也不容易出错,能够保证我们写出来的代码质量是不错的。
写在最后
状态模式使用频率和策略模式差不多,使用的地方还是比较多的,也是可以快速的简化代码的一种设计模式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。