项目组在使用观察者模式完成气象站项目之后,名声在外,这次又接到了来自于无人售货行业的万能糖果公司的订单。
该公司要设计一款自动糖果售货机,开始时,售货机的状态是"没钱",投入钱后状态变为"有钱",然后转动曲柄,糖果机会吐出糖果。
用一张状态图来表现如下:
从这张图中,我们可以识别出售货机的状态:没钱、有钱、出售中、售罄。导致状态发生迁移的动作有:投钱、退钱、转动曲柄、发放糖果。
按照直观的方式,我们来实现V1版本的糖果机。
/**
* 自助售货机
*/
public class GumballMachineV1 {
final static int SALE_OUT = 0; //售罄
final static int NO_QUARTER = 1; //没有硬币
final static int HAS_QUARTER = 2; //有硬币
final static int SOLD = 3; //出售中
private int state = SALE_OUT;
private int count;
public GumballMachineV1(int count) {
this.count = count;
if(this.count > 0){
state = NO_QUARTER;
}
}
//投入硬币的动作
public void insertQuarter(){
if(state == NO_QUARTER){
//状态变化
state = HAS_QUARTER;
System.out.println("你投入了硬币");
}else if(state == SALE_OUT){
System.out.println("售罄状态下,不能投入硬币");
}else if(state == HAS_QUARTER){
System.out.println("你已经投入硬币,不需要继续投入");
}else if (state == SOLD){
System.out.println("请稍等,正在出糖果");
}else{
System.out.println("未知状态,怕是出错比较好。");
}
}
//退钱的动作
public void ejectQuarter(){
if(state == NO_QUARTER){
System.out.println("没有投入硬币,退不了");
}else if(state == SALE_OUT){
System.out.println("售罄状态下,不能投入硬币,当然也不能退钱了");
}else if(state == HAS_QUARTER){
//状态变化
state = NO_QUARTER;
System.out.println("退钱给你了");
}else if (state == SOLD){
System.out.println("正在出糖果中,不能退钱");
}else{
System.out.println("未知状态,怕是出错比较好。");
}
}
//按下购买按钮(转动曲柄)
public void turnCrank(){
if(state == NO_QUARTER){
System.out.println("还没有投入钱,转动也没用");
}else if(state == SALE_OUT){
System.out.println("售罄状态下,不能投入硬币,转动没用");
}else if(state == HAS_QUARTER){
//状态变化
state = SOLD;
System.out.println("接受转动,出糖中...");
dispense();
}else if (state == SOLD){
System.out.println("正在出糖果中,等着吧你。");
}else{
System.out.println("未知状态,怕是出错比较好。");
}
}
//发放糖果(机器内部自动触发)
public void dispense(){
if(state == NO_QUARTER){
System.out.println("先付钱,才能给糖果");
}else if(state == SALE_OUT){
System.out.println("都没糖果了,给不了..");
}else if(state == HAS_QUARTER){
System.out.println("先转动下曲柄,才能给你糖果哦");
}else if (state == SOLD){
System.out.println("来,给你糖果。。");
//状态变化
count--;
if(count > 0){
state = NO_QUARTER;
}else{
state = SALE_OUT;
}
}else{
System.out.println("未知状态,怕是出错比较好。");
}
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("GumballMachineV1{");
sb.append("state=").append(state);
sb.append(", count=").append(count);
sb.append('}');
return sb.toString();
}
}
该版本中,机器的每个动作,我们使用if-else判断机器当前状态,从而做出正确的行为。
v1版本的测试
/**
* 自助售货机测试
*/
public class GumballMachineMainV1 {
public static void main(String[] args) {
GumballMachineV1 machine = new GumballMachineV1(3);
System.out.println(machine);
machine.insertQuarter();
machine.turnCrank();
System.out.println(machine);
machine.insertQuarter();
machine.ejectQuarter();
machine.turnCrank();
System.out.println(machine);
machine.insertQuarter();
machine.turnCrank();
machine.insertQuarter();
machine.turnCrank();
System.out.println(machine);
machine.insertQuarter();
}
}
V1版实现对应对变化的能力有限,比如现在万能糖果公司老板觉得可以将购买糖果当做一个游戏,每次购买有10%的几率中奖,可以获得两颗糖果。
如果要在V1上对此进行扩展,除了要添加状态外(这个还好说),每个动作中的if-else都要新增加对新状态的判断,这回让代码一团糟的,不满足开闭原则。
既然状态,甚至导致状态变化的动作是变化的,那么我们能不能将变化抽取出来,封装到单独的类中,然后通过组合使用?
沿着这个思路,我们使用V2来重构
首先抽象出来状态接口,其提供了状态机的所有行为。
/**
* 状态接口
* 定义自助售货机的所有方法,每种状态都有一种实现,分别实现在该种状态下的行为。
*/
public interface StateV2 {
void insertQuarter();
void ejectQuarter();
void turnCrank();
void dispense();
}
接下来,实现4中具体的状态。具体状态的实现依据是我们的状态图。
/**
* 没币状态
*/
public class NoQuarterStateV2 implements StateV2 {
GumballMachineV2 machine;
public NoQuarterStateV2(GumballMachineV2 machine) {
this.machine = machine;
}
@Override
public void insertQuarter() {
System.out.println("你投入了硬币");
//状态变更
machine.changeState(machine.getHasQuarter());
}
@Override
public void ejectQuarter() {
System.out.println("没有投入硬币,退不了");
}
@Override
public void turnCrank() {
System.out.println("还没有投入钱,转动也没用");
}
@Override
public void dispense() {
System.out.println("先付钱,才能给糖果");
}
}
/**
* 有币状态
*/
public class HasQuarterStateV2 implements StateV2 {
GumballMachineV2 machine;
public HasQuarterStateV2(GumballMachineV2 machine) {
this.machine = machine;
}
@Override
public void insertQuarter() {
System.out.println("你已经投入硬币,不需要继续投入");
}
@Override
public void ejectQuarter() {
//状态变更
System.out.println("退钱给你了");
machine.changeState(machine.getNoQuarter());
}
@Override
public void turnCrank() {
//状态变化
System.out.println("接受转动,出糖中...");
machine.changeState(machine.getSold());
}
@Override
public void dispense() {
System.out.println("先转动下曲柄,才能给你糖果哦");
}
}
/**
* 出货状态
*/
public class SoldStateV2 implements StateV2 {
GumballMachineV2 machine;
public SoldStateV2(GumballMachineV2 machine) {
this.machine = machine;
}
@Override
public void insertQuarter() {
System.out.println("请稍等,正在出糖果。。。。");
}
@Override
public void ejectQuarter() {
System.out.println("正在出糖果中,不能退钱");
}
@Override
public void turnCrank() {
System.out.println("正在出糖果中,等着吧你。");
}
@Override
public void dispense() {
System.out.println("来,给你糖果。。");
//状态变化
machine.countDEC();
if(machine.getCount() > 0){
machine.changeState(machine.getNoQuarter());
}else{
machine.changeState(machine.getSaleOut());
}
}
}
/**
* 售罄状态
*/
public class SoldOutStateV2 implements StateV2 {
GumballMachineV2 machine;
public SoldOutStateV2(GumballMachineV2 machine) {
this.machine = machine;
}
@Override
public void insertQuarter() {
System.out.println("售罄状态下,不能投入硬币");
}
@Override
public void ejectQuarter() {
System.out.println("售罄状态下,不能投入硬币,当然也不能退钱了");
}
@Override
public void turnCrank() {
System.out.println("售罄状态下,不能投入硬币,转动没用");
}
@Override
public void dispense() {
System.out.println("都没糖果了,给不了..");
}
}
此时,我们的糖果售货机就简单了,它将行为委托它的当前状态来实现。
/**
* 自助售货机
*/
public class GumballMachineV2 {
private StateV2 saleOut ; //售罄
private StateV2 noQuarter ; //没有硬币
private StateV2 hasQuarter ; //有硬币
private StateV2 sold ; //出售中
private StateV2 state;
private int count;
public GumballMachineV2(int count) {
saleOut = new SoldOutStateV2(this);
noQuarter = new NoQuarterStateV2(this);
hasQuarter = new HasQuarterStateV2(this);
sold = new SoldStateV2(this);
this.count = count;
if(this.count > 0){
state = noQuarter;
}
}
//投入硬币的动作
public void insertQuarter(){
state.insertQuarter();
}
//退钱的动作
public void ejectQuarter(){
state.ejectQuarter();
}
//按下购买按钮(转动曲柄)
public void turnCrank(){
state.turnCrank();
dispense();
}
//发放糖果(机器内部自动触发)
public void dispense(){
state.dispense();
}
//状态变更。
public void changeState(StateV2 newState){
state = newState;
}
//减库存
public void countDEC(){
this.count--;
}
public StateV2 getSaleOut() {
return saleOut;
}
public void setSaleOut(StateV2 saleOut) {
this.saleOut = saleOut;
}
public StateV2 getNoQuarter() {
return noQuarter;
}
public void setNoQuarter(StateV2 noQuarter) {
this.noQuarter = noQuarter;
}
public StateV2 getHasQuarter() {
return hasQuarter;
}
public void setHasQuarter(StateV2 hasQuarter) {
this.hasQuarter = hasQuarter;
}
public StateV2 getSold() {
return sold;
}
public void setSold(StateV2 sold) {
this.sold = sold;
}
public StateV2 getState() {
return state;
}
public void setState(StateV2 state) {
this.state = state;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("GumballMachineV2{");
sb.append(", state=").append(state);
sb.append(", count=").append(count);
sb.append('}');
return sb.toString();
}
}
最后,测试下v2代码
/**
* 自助售货机测试
*/
public class GumballMachineMainV2 {
public static void main(String[] args) {
GumballMachineV2 machine = new GumballMachineV2(3);
System.out.println(machine);
machine.insertQuarter();
machine.turnCrank();
System.out.println(machine);
machine.insertQuarter();
machine.ejectQuarter();
machine.turnCrank();
System.out.println(machine);
machine.insertQuarter();
machine.turnCrank();
machine.insertQuarter();
machine.turnCrank();
System.out.println(machine);
machine.insertQuarter();
}
}
V2代码中,我们为每个状态单独封装起来,并且在各自状态下对行为做了自己的实现。
糖果机通过组合方式使用状态,其当前状态在不同的状态之间转换,对外表现就像,糖果机的同一方法在不停的变化一样。
现在,我们在V2的基础上来加入前面提到的变化,即新加入一个新的中奖状态的V3版实现。
首先,我们要提供中奖状态的实现
/**
* 中奖状态
*/
public class WinStateV3 implements StateV3 {
GumballMachineV3 machine;
public WinStateV3(GumballMachineV3 machine) {
this.machine = machine;
}
@Override
public void insertQuarter() {
System.out.println("请稍等,正在出糖果。。。。");
}
@Override
public void ejectQuarter() {
System.out.println("正在出糖果中,不能退钱");
}
@Override
public void turnCrank() {
System.out.println("正在出糖果中,等着吧你。");
}
@Override
public void dispense() {
System.out.println("恭喜你,你中奖了买一送一。。");
oneGumball();
if (machine.getState() != machine.getSaleOut()){
oneGumball();
}
}
private void oneGumball() {
System.out.println("来,给你糖果。。");
//状态变化
machine.countDEC();
if(machine.getCount() > 0){
machine.changeState(machine.getNoQuarter());
}else{
machine.changeState(machine.getSaleOut());
}
}
}
然后,需要修改 有币状态 中的状态流转,让10%的几率流转到 中奖状态。
/**
* 有币状态
*/
public class HasQuarterStateV3 implements StateV3 {
GumballMachineV3 machine;
Random random;
public HasQuarterStateV3(GumballMachineV3 machine) {
this.machine = machine;
this.random = new Random();
}
@Override
public void insertQuarter() {
System.out.println("你已经投入硬币,不需要继续投入");
}
@Override
public void ejectQuarter() {
//状态变更
System.out.println("退钱给你了");
machine.changeState(machine.getNoQuarter());
}
@Override
public void turnCrank() {
//状态变化
System.out.println("接受转动,出糖中...");
int winRand = random.nextInt(10);
System.out.println("---"+winRand);
if(winRand == 0){
machine.changeState(machine.getWin());
}else{
machine.changeState(machine.getSold());
}
}
@Override
public void dispense() {
System.out.println("先转动下曲柄,才能给你糖果哦");
}
}
当然,售货机中新加入中奖状态的实例
/**
* 自助售货机
*/
public class GumballMachineV3 {
private StateV3 saleOut ; //售罄
private StateV3 noQuarter ; //没有硬币
private StateV3 hasQuarter ; //有硬币
private StateV3 sold ; //出售中
private StateV3 win ; //中奖
private StateV3 state;
private int count;
public GumballMachineV3(int count) {
saleOut = new SoldOutStateV3(this);
noQuarter = new NoQuarterStateV3(this);
hasQuarter = new HasQuarterStateV3(this);
sold = new SoldStateV3(this);
win = new WinStateV3(this);
this.count = count;
if(this.count > 0){
state = noQuarter;
}
}
//投入硬币的动作
public void insertQuarter(){
state.insertQuarter();
}
//退钱的动作
public void ejectQuarter(){
state.ejectQuarter();
}
//按下购买按钮(转动曲柄)
public void turnCrank(){
state.turnCrank();
dispense();
}
//发放糖果(机器内部自动触发)
public void dispense(){
state.dispense();
}
//状态变更。
public void changeState(StateV3 newState){
state = newState;
}
//减库存
public void countDEC(){
this.count--;
}
public StateV3 getSaleOut() {
return saleOut;
}
public void setSaleOut(StateV3 saleOut) {
this.saleOut = saleOut;
}
public StateV3 getNoQuarter() {
return noQuarter;
}
public void setNoQuarter(StateV3 noQuarter) {
this.noQuarter = noQuarter;
}
public StateV3 getHasQuarter() {
return hasQuarter;
}
public void setHasQuarter(StateV3 hasQuarter) {
this.hasQuarter = hasQuarter;
}
public StateV3 getSold() {
return sold;
}
public void setSold(StateV3 sold) {
this.sold = sold;
}
public StateV3 getState() {
return state;
}
public void setState(StateV3 state) {
this.state = state;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public StateV3 getWin() {
return win;
}
public void setWin(StateV3 win) {
this.win = win;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("GumballMachineV3{");
sb.append("state=").append(state);
sb.append(", count=").append(count);
sb.append('}');
return sb.toString();
}
}
测试代码和V2一样。
可以看出,通过封装变化,多用组合的原则之后,我们的代码扩展性明显提高了。
上面的V2和V3,就是使用了状态模式来解决问题。
状态模式,允许对象内部的状态发生变化时,改变它的行为。从外面看起来好像修改了它的类。
这个模式将对象的状态封装为独立的类,调用对象的行为委托给当前状态的行为,这就导致了当状态发生变化时,其行为也会跟着发生变化。
用代码来实现一波(context中的状态变更) 状态A–(action1)–>状态B–(action2)–>状态C–(action3)–>状态A
/**
* 状态接口
*/
public interface State {
void action1();
void action2();
void action3();
}
具体状态实现(内含状态转换)
/**
* 状态A
*/
public class ConcreteStateA implements State {
private Context context;
public ConcreteStateA(Context context) {
this.context = context;
}
@Override
public void action1() {
System.out.println("状态A时,action1动作,状态切换到B...");
context.changeState(context.getStateB());
}
@Override
public void action2() {
}
@Override
public void action3() {
}
}
/**
* 状态B
*/
public class ConcreteStateB implements State {
private Context context;
public ConcreteStateB(Context context) {
this.context = context;
}
@Override
public void action1() {
}
@Override
public void action2() {
System.out.println("状态B时,action2动作...状态变更到C");
context.changeState(context.getStateC());
}
@Override
public void action3() {
}
}
/**
* 状态C
*/
public class ConcreteStateC implements State {
private Context context;
public ConcreteStateC(Context context) {
this.context = context;
}
@Override
public void action1() {
}
@Override
public void action2() {
}
@Override
public void action3() {
System.out.println("状态C时,action3动作...状态变更到A");
context.changeState(context.getStateA());
}
}
状态上下文,组合状态的状态机。
/**
* 状态上下文
*/
public class Context {
private State stateA ;
private State stateB ;
private State stateC ;
private State state;
public Context() {
stateA = new ConcreteStateA(this);
stateB = new ConcreteStateB(this);
stateC = new ConcreteStateC(this);
this.state = stateA;
}
public void request1(){
state.action1();
}
public void request2(){
state.action2();
}
public void request3(){
state.action3();
}
public void changeState(State state){
this.state = state;
}
public State getStateA() {
return stateA;
}
public State getStateB() {
return stateB;
}
public State getStateC() {
return stateC;
}
public State getState() {
return state;
}
}
测试
/**
* 测试
*/
public class StateMain {
public static void main(String[] args) {
Context context = new Context();
context.request1();
context.request2();
context.request3();
}
}
如果看过策略模式的UML的话,你会发现状态模式的类结构和策略模式一模一样,同样的类组织方式,解决问题的思路是一样的,只不过关注点(目的)不一样而已。
策略模式抽象了算法接口,提供不同的算法实现并封装起来,在策略上下文中组合某一种算法实现。
状态模式抽象了状态接口,提供不同的状态实现并封装起来,在状态上下文中组合状态类,状态类的具体类型会在运行过程中迁移,从而让状态上下文表现出不同的行为。
示例参考 https://www.journaldev.com/1751/state-design-pattern-java。
实现电视遥控板的电源按钮功能,当电视状态是关机时,按下电源按钮的动作会打开电视,状态迁移到开机状态;当电视状态是开机状态时,按下电源按钮的动作会关闭电视,状态迁移到关机状态。
首先还是看下没有使用状态模式下的常规实现
/**
* 通过IF-ELSE来实现基于状态的不同行为。
*/
public class TVRemoteBasic {
private String state="OFF";
public void setState(String state){
this.state=state;
}
public void pressPowerButton(){
if(state.equalsIgnoreCase("ON")){
System.out.println("TV is turned OFF");
setState("OFF");
}else if(state.equalsIgnoreCase("OFF")){
System.out.println("TV is turned ON");
setState("ON");
}
}
public static void main(String args[]){
TVRemoteBasic remote = new TVRemoteBasic();
remote.pressPowerButton();
remote.pressPowerButton();
}
}
这个不用多说,和糖果机的例子一样。
接下来,我们使用状态模式来改造一波。
首先,定义状态抽象(接口或者抽象来都可以)。
/**
* 电视机状态
*/
public interface TvState {
void pressPowerButton();
}
开机和关机状态实现:
/**
* 开机状态
*/
public class TVStartState implements TvState {
private TVContext context;
public TVStartState(TVContext context) {
this.context = context;
}
@Override
public void pressPowerButton() {
System.out.println("TV is turned OFF");
context.setCurrentState(context.getOffState());
}
}
/**
* 关机状态
*/
public class TVStopState implements TvState {
private TVContext context;
public TVStopState(TVContext context) {
this.context = context;
}
@Override
public void pressPowerButton() {
System.out.println("TV is turned ON");
context.setCurrentState(context.getOnState());
}
}
最后来实现状态上下文,也就是来组合我们状态的状态机,它会将收到的请求委托给当前状态类去实现。
/**
* 电视机
*/
public class TVContext {
private TvState onState ;
private TvState offState;
private TvState currentState;
public TVContext() {
onState = new TVStartState(this);
offState = new TVStopState(this);
currentState = offState;
}
public void pressPowerButton(){
currentState.pressPowerButton();
}
public TvState getOnState() {
return onState;
}
public void setOnState(TvState onState) {
this.onState = onState;
}
public TvState getOffState() {
return offState;
}
public void setOffState(TvState offState) {
this.offState = offState;
}
public TvState getCurrentState() {
return currentState;
}
public void setCurrentState(TvState currentState) {
this.currentState = currentState;
}
}
测试
/**
* 测试
*/
public class TVRemote {
public static void main(String[] args) {
TVContext context = new TVContext();
context.pressPowerButton();
context.pressPowerButton();
context.pressPowerButton();
}
}
输出结果
TV is turned ON
TV is turned OFF
TV is turned ON
https://gitee.com/cq-laozhou/design-pattern