# 状态模式
1.简介
状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
比如说你有一个文档(Document)类,文档可能会处在草稿(Draft)、**审阅中(Moderation)和已发布(Published)三种状态中的一种。同时文档的publish(发布)**方法在不同的状态下的行为并不相同。
- 处于 草稿状态时,他会将文档转移到 审阅中状态
- 处于 审阅中状态时,如果当前用户是管理员,他会公开发布文档。
- 处于 已发布状态时,它不会进行任何操作。
这种逻辑通常是通过众多的条件运算符(if
或 switch
)实现的,可根据对象的当前状态选择相应的行为。因为 状态本身通常只是对象中的一组成员变量值。
当我们逐步在 文档类中添加更多状态和依赖于状态的行为后,为了能根据当前状态选择完成相应行为的方法,绝大部份方法中会包含复杂的条件语句,修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句,导致代码维护困难,越来越臃肿,这种问题可以使用状态模式来解决。
2.UML图
1.上下文(Context): 保存了对于一个具体状态对象的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会提供一个设置器用于传递新的状态对象。
2.状态(State): 接口会声明特定于状态的方法。这些方法应能被其他所有具体状态所理解。
3.具体状态(Concrete States): 会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部份通用行为的中间抽象类。
状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息,并且可以触发状态转移。
4.上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。
3.代码示例
下面我们来编写自动售货机代码的示例:
首先分析一下上面的状态图;
我们发现有四种状态
有三个方法
再每种状态下用户都可以触发相对应的方法
我们按照我们通常的思路编写代码:
package com.gs.designmodel.state.test;
/**
* @author: Gaos
* @Date: 2023-07-14 17:58
**/
/**
* 自动售货机
*/
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;
}
}
}
针对于用户的每个动作,我们判断当前的每一个状态,去做处理。代码里面也充满了 swithc、if、else
。
测试一下
package com.gs.designmodel.state.test;
/**
* @author: Gaos
* @Date: 2023-07-17 11:11
**/
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();
System.out.println("------------");
machine.insertMoney();
machine.insertMoney();
machine.turnCrank();
machine.turnCrank();
machine.backMoney();
machine.turnCrank();
}
}
结果:
成功投入硬币
退币成功
-----------
成功投入硬币
正在出商品....
发出商品...
------------
成功投入硬币
已经有硬币,无需投币
正在出商品....
发出商品...
请先投入硬币
您未投入硬币
请先投入硬币
现在需求发生了变化,为了提升销量,用户在转动手柄购买商品的时候,都会有百分之10的概率得到买一送一的奖励,相应的状态图也会发生改变:
如果按照我们之前的思路在原本代码上增加逻辑的话,也就意味着我们需要在每个方法里面的 switch、if、else
里增加或修改代码,覆盖面太大。所以我们应该重新设计我们的代码,将不同的状态对应不同的类,负责实现各个动作下的行为。自动售货机在不同的状态之间进行切换。我们现在有五种状态、四种动作。
首先定义一个状态的接口:
package com.gs.designmodel.state.real;
/**
* @author: Gaos
* @Date: 2023-07-17 10:04
* 状态接口
**/
public interface State {
/**
* 放钱
*/
public void insertMoney();
/**
* 退钱
*/
public void backMoney();
/**
* 转动曲柄
*/
void turnCrank();
/**
* 售出商品
*/
void dispense();
}
然后分别是每种状态的具体实现:
没钱的状态:
package com.gs.designmodel.state.real;
/**
* @author: Gaos
* @Date: 2023-07-17 10:07
*
* 没有钱的状态
**/
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("非法状态");
}
}
投入币的状态:
package com.gs.designmodel.state.real;
import java.util.Random;
/**
* @author: Gaos
* @Date: 2023-07-17 10:24
* 投入币的状态
**/
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("非法状态,请检查");
}
}
售罄的状态:
package com.gs.designmodel.state.real;
/**
* @author: Gaos
* @Date: 2023-07-17 10:29
*
* 售罄的状态
**/
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("非法状态!");
}
}
准备出商品的状态:
package com.gs.designmodel.state.real;
/**
* @author: Gaos
* @Date: 2023-07-17 10:33
*
* 准备出商品的状态
**/
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());
}
}
}
中奖的状态:
package com.gs.designmodel.state.real;
/**
* @author: Gaos
* @Date: 2023-07-17 10:37
*
* 中奖的状态
**/
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("恭喜你,中奖了,将获得两件商品");
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());
}
}
}
}
最后是自动售货机的代码:
package com.gs.designmodel.state.real;
import lombok.Data;
/**
* @author: Gaos
* @Date: 2023-07-17 10:08
* 自动售货机
**/
@Data
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;
}
}
可以看到,我们现在把每个状态对应于动作的行为局部化到了状态自己的类中实现,不仅增加了扩展性而且使代码的阅读性大幅度的提高。以后再添加状态,只需要针对新添加的状态的实现类,并在自动售货机中添加此状态即可。
测试一下:
package com.gs.designmodel.state.real;
/**
* @author: Gaos
* @Date: 2023-07-17 10:47
**/
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();
}
}
结果:
投币成功
退币成功
----我要中奖----
投币成功
您转动了手柄
发出一件商品...
投币成功
您转动了手柄
发出一件商品...
投币成功
您转动了手柄
发出一件商品...
投币成功
您转动了手柄
发出一件商品...
投币成功
您转动了手柄
发出一件商品...
投币成功
您转动了手柄
发出一件商品...
投币成功
您转动了手柄
发出一件商品...
-------压力测试------
投币成功
退币成功
未投币,无法退钱
未投币,无法转动
未投币,无法转动
未投币,无法退钱
4.总结
如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可以使用状态模式。
如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可以使用该模式。
当相似状态和状态转换中存在许多重复代码的时候,可以使用状态模式,将公用代码抽取到抽象基类中来减少。
桥接、状态、策略模式的接口都比较相似,实际上他们都基于组合模式----将工作委派给其他对象。状态模式和策略模式的不同点在于:策略使得这些对象相互之间完全独立,它们不需要知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,并允许它们自行改变在不同情景下的状态。