[整理] 设计模式之状态模式
定义
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
特征
-
意图:
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。 -
主要解决:
对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。 -
何时使用:
代码中包含大量与对象状态有关的条件语句。 -
如何解决:
将各种具体的状态类抽象出来。 -
关键代码:
通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。 -
应用实例:
- 打篮球的时候运动员可以有正常状态、不正常状态和超常状态。
- 曾侯乙编钟中,'钟是抽象接口','钟A'等是具体状态,'曾侯乙编钟'是具体环境(Context)。
- 优点:
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
- 缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
- 使用场景:
- 行为随状态改变而改变的场景。
- 条件、分支语句的代替者。
- 注意事项:
在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
案例
下面使用个例子来说明状态模式的用法,现在有个自动售货机的代码需要我们来写,状态图如下:
分析一个这个状态图:
- 包含4个状态(我们使用4个int型常量来表示)
- 包含3个暴露在外的方法(投币、退币、转动手柄)
- 我们需要处理每个状态下,用户都可以触发这三个动作。
下面我们根据分析的结果,写出代码:
// 自动售货机
public class VendingMachine
{
/**
* 已投币
*/
private final static int HAS_MONEY = 0;
/**
* 未投币
*/
private final static int NO_MONEY = 1;
/**
* 售出商品
*/
private final static int SOLD = 2;
/**
* 商品售罄
*/
private final static int SOLD_OUT = 3;
private int currentStatus = NO_MONEY;
/**
* 商品数量
*/
private int count = 0;
public VendingMachine(int count)
{
this.count = count;
if (count > 0)
{
currentStatus = NO_MONEY;
}
}
/**
* 投入硬币,任何状态用户都可能投币
*/
public void insertMoney()
{
switch (currentStatus)
{
case NO_MONEY:
currentStatus = HAS_MONEY;
System.out.println("成功投入硬币");
break;
case HAS_MONEY:
System.out.println("已经有硬币,无需投币");
break;
case SOLD:
System.out.println("请稍等...");
break;
case SOLD_OUT:
System.out.println("商品已经售罄,请勿投币");
break;
}
}
/**
* 退币,任何状态用户都可能退币
*/
public void backMoney()
{
switch (currentStatus)
{
case NO_MONEY:
System.out.println("您未投入硬币");
break;
case HAS_MONEY:
currentStatus = NO_MONEY;
System.out.println("退币成功");
break;
case SOLD:
System.out.println("您已经买了糖果...");
break;
case SOLD_OUT:
System.out.println("您未投币...");
break;
}
}
/**
* 转动手柄购买,任何状态用户都可能转动手柄
*/
public void turnCrank()
{
switch (currentStatus)
{
case NO_MONEY:
System.out.println("请先投入硬币");
break;
case HAS_MONEY:
System.out.println("正在出商品....");
currentStatus = SOLD;
dispense();
break;
case SOLD:
System.out.println("连续转动也没用...");
break;
case SOLD_OUT:
System.out.println("商品已经售罄");
break;
}
}
/**
* 发放商品
*/
private void dispense()
{
switch (currentStatus)
{
case NO_MONEY:
case HAS_MONEY:
case SOLD_OUT:
throw new IllegalStateException("非法的状态...");
case SOLD:
count--;
System.out.println("发出商品...");
if (count == 0)
{
System.out.println("商品售罄");
currentStatus = SOLD_OUT;
} else
{
currentStatus = NO_MONEY;
}
break;
}
}
}
针对用户的每个动作,我们考虑了在任何状态下发生,并做了一定处理。下面进行一些测试:
public class TestTra
{
public static void main(String[] args)
{
VendingMachine machine = new VendingMachine(10);
machine.insertMoney();
machine.backMoney();
System.out.println("-----------");
machine.insertMoney();
machine.turnCrank();
System.out.println("----------压力测试-----");
machine.insertMoney();
machine.insertMoney();
machine.turnCrank();
machine.turnCrank();
machine.backMoney();
machine.turnCrank();
}
}
输出结果:
成功投入硬币
退币成功
-----------
成功投入硬币
正在出商品....
发出商品...
----------压力测试-----
成功投入硬币
已经有硬币,无需投币
正在出商品....
发出商品...
请先投入硬币
您未投入硬币
请先投入硬币
需求变化
感觉还是不错的,基本实现了功能,但是有些事情是不可避免的,那就是需求的变化,现在为了提升销量,当用户每次转动手柄买商品的时候,有10%的几率赠送一瓶。
现在的状态图发生了变化,当用户转动手柄时,可能会达到一个中奖的状态,图如下:
如果在我们刚写的代码上直接添加,则需要在每个动作的switch中添加判断条件,且非常容易出错。所以现在我们要考虑重新设计我们的代码,我们考虑把每个状态写状态类,负责实现在对应动作下的行为,然后自动售货机在不能的状态间切换:
下面开始重构,我们现在有5种状态,对应4个动作(投币、退币、转动手柄、发出商品),下面首先定义一个状态的超类型:
// 状态的接口
public interface State
{
/**
* 放钱
*/
public void insertMoney();
/**
* 退钱
*/
public void backMoney();
/**
* 转动曲柄
*/
public void turnCrank();
/**
* 出商品
*/
public void dispense();
}
然后分别是每个状态的实现:
// 没钱的状态
public class NoMoneyState implements State
{
private VendingMachine machine;
public NoMoneyState(VendingMachine machine)
{
this.machine = machine;
}
@Override
public void insertMoney()
{
System.out.println("投币成功");
machine.setState(machine.getHasMoneyState());
}
@Override
public void backMoney()
{
System.out.println("您未投币,想退钱?...");
}
@Override
public void turnCrank()
{
System.out.println("您未投币,想拿东西么?...");
}
@Override
public void dispense()
{
throw new IllegalStateException("非法状态!");
}
}
import java.util.Random;
// 已投入钱的状态
public class HasMoneyState implements State
{
private VendingMachine machine;
private Random random = new Random();
public HasMoneyState(VendingMachine machine)
{
this.machine = machine;
}
@Override
public void insertMoney()
{
System.out.println("您已经投过币了,无需再投....");
}
@Override
public void backMoney()
{
System.out.println("退币成功");
machine.setState(machine.getNoMoneyState());
}
@Override
public void turnCrank()
{
System.out.println("你转动了手柄");
int winner = random.nextInt(10);
if (winner == 0 && machine.getCount() > 1)
{
machine.setState(machine.getWinnerState());
} else
{
machine.setState(machine.getSoldState());
}
}
@Override
public void dispense()
{
throw new IllegalStateException("非法状态!");
}
}
// 售罄的状态
public class SoldOutState implements State
{
private VendingMachine machine;
public SoldOutState(VendingMachine machine)
{
this.machine = machine;
}
@Override
public void insertMoney()
{
System.out.println("投币失败,商品已售罄");
}
@Override
public void backMoney()
{
System.out.println("您未投币,想退钱么?...");
}
@Override
public void turnCrank()
{
System.out.println("商品售罄,转动手柄也木有用");
}
@Override
public void dispense()
{
throw new IllegalStateException("非法状态!");
}
}
// 准备出商品的状态,该状态下,不会有任何用户的操作
public class SoldState implements State
{
private VendingMachine machine;
public SoldState(VendingMachine machine)
{
this.machine = machine;
}
@Override
public void insertMoney()
{
System.out.println("正在出货,请勿投币");
}
@Override
public void backMoney()
{
System.out.println("正在出货,没有可退的钱");
}
@Override
public void turnCrank()
{
System.out.println("正在出货,请勿重复转动手柄");
}
@Override
public void dispense()
{
machine.dispense();
if (machine.getCount() > 0)
{
machine.setState(machine.getNoMoneyState());
} else
{
System.out.println("商品已经售罄");
machine.setState(machine.getSoldOutState());
}
}
}
// 中奖的状态,该状态下不会有任何用户的操作
public class WinnerState implements State
{
private VendingMachine machine;
public WinnerState(VendingMachine machine)
{
this.machine = machine;
}
@Override
public void insertMoney()
{
throw new IllegalStateException("非法状态");
}
@Override
public void backMoney()
{
throw new IllegalStateException("非法状态");
}
@Override
public void turnCrank()
{
throw new IllegalStateException("非法状态");
}
@Override
public void dispense()
{
System.out.println("你中奖了,恭喜你,将得到2件商品");
machine.dispense();
if (machine.getCount() == 0)
{
System.out.println("商品已经售罄");
machine.setState(machine.getSoldOutState());
} else
{
machine.dispense();
if (machine.getCount() > 0)
{
machine.setState(machine.getNoMoneyState());
} else
{
System.out.println("商品已经售罄");
machine.setState(machine.getSoldOutState());
}
}
}
}
最后是自动售货机的代码:
// 自动售货机
public class VendingMachine
{
private State noMoneyState;
private State hasMoneyState;
private State soldState;
private State soldOutState;
private State winnerState ;
private int count = 0;
private State currentState = noMoneyState;
public VendingMachine(int count)
{
noMoneyState = new NoMoneyState(this);
hasMoneyState = new HasMoneyState(this);
soldState = new SoldState(this);
soldOutState = new SoldOutState(this);
winnerState = new WinnerState(this);
if (count > 0)
{
this.count = count;
currentState = noMoneyState;
}
}
public void insertMoney()
{
currentState.insertMoney();
}
public void backMoney()
{
currentState.backMoney();
}
public void turnCrank()
{
currentState.turnCrank();
if (currentState == soldState || currentState == winnerState)
currentState.dispense();
}
public void dispense()
{
System.out.println("发出一件商品...");
if (count != 0)
{
count -= 1;
}
}
public void setState(State state)
{
this.currentState = state;
}
//getter setter omitted ...
}
可以看到,我们现在把每个状态对应于动作的行为局部化到了状态自己的类中实现,不仅增加了扩展性而且使代码的阅读性大幅度的提高。以后再添加状态,只需要针对新添加的状态的实现类,并在自动售货机中添加此状态即可。
下面进行一些测试:
public class Test
{
public static void main(String[] args)
{
VendingMachine machine = new VendingMachine(10);
machine.insertMoney();
machine.backMoney();
System.out.println("----我要中奖----");
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
machine.insertMoney();
machine.turnCrank();
System.out.println("-------压力测试------");
machine.insertMoney();
machine.backMoney();
machine.backMoney();
machine.turnCrank();// 无效操作
machine.turnCrank();// 无效操作
machine.backMoney();
}
}
输出结果:
投币成功
退币成功
----我要中奖----
投币成功
你转动了手柄
发出一件商品...
投币成功
你转动了手柄
发出一件商品...
投币成功
你转动了手柄
发出一件商品...
投币成功
你转动了手柄
发出一件商品...
投币成功
你转动了手柄
发出一件商品...
投币成功
你转动了手柄
发出一件商品...
投币成功
你转动了手柄
你中奖了,恭喜你,将得到2件商品
发出一件商品...
发出一件商品...
-------压力测试------
投币成功
退币成功
您未投币,想退钱?...
您未投币,想拿东西么?...
您未投币,想拿东西么?...
您未投币,想退钱?...