设计模式 -- 状态模式(state)
本文最后更新于:4 个月前
⚡一句话概括
状态模式允许对象在内部状态时改变它的行为,对象看起来好像修改了它所属的类。
用途:
主要是解决当控制一个对象装换的条件表达式过于复杂的时候,把状态判断逻辑转移到表示不同状态的一系列类当中。可以把复杂的判断逻辑简化。
⚡例子
现在有这样一个需求:
有一个糖果机,当有人把25美分的硬币放到机器里,转动曲柄,这个机器就会弹出糖果。
但是,这个机器有好几种状态,在某些状态下可以弹出糖果,而在某些状态下不能。
如下图所示,用圆圈表示状态,箭头表示行为。
⚡if-else / switch case 大法
public class GumBall {
/**
* 糖果机状态
*/
private State state;
/**
* 糖果数目
*/
private int number;
GumBall(int number) {
if (number >= 0) {
this.number = number;
state = State.NO_QUARTER_STATE;
} else {
this.number = 0;
state = State.NO_CANDY_STATE;
}
}
/**
* 投入一个25美分硬币买糖果
*/
public void insertQuarter() {
System.out.print("投入硬币");
if (state == State.HAS_QUARTER_STATE) {
System.out.println(",已经放有硬币,请稍候");
return;
}
if (state == State.NO_CANDY_STATE) {
System.out.println(",售罄");
return;
}
if (state == State.GOING_TO_PRODUCE_STATE) {
System.out.println(",正在出货,请等待");
return;
}
/**
* 允许投入硬币的情况
*/
if (state == State.NO_QUARTER_STATE) {
System.out.println(",成功投入一枚硬币");
state = State.HAS_QUARTER_STATE;
}
}
/**
* 拿回硬币
*/
public void ejectQuarter() {
System.out.print("退款");
if (state == State.NO_CANDY_STATE) {
System.out.println(",已售罄");
}
if (state == State.NO_QUARTER_STATE) {
System.out.println(",没有硬币");
} else if (state == State.GOING_TO_PRODUCE_STATE) {
System.out.println(",硬币已存入");
} else if (state == State.HAS_QUARTER_STATE) {
System.out.println(",退还硬币成功");
state = State.NO_QUARTER_STATE;
}
}
/**
* 转动曲柄
*/
public void turnCrank() {
System.out.print("转动曲柄");
if (state == State.NO_QUARTER_STATE) {
System.out.println(",未付款,请先投入硬币");
return;
}
if (state == State.NO_CANDY_STATE) {
System.out.println(",已售罄");
return;
}
if (state == State.GOING_TO_PRODUCE_STATE) {
System.out.println(",请稍候正在出货");
return;
}
if (state == State.HAS_QUARTER_STATE) {
System.out.println("");
state = State.GOING_TO_PRODUCE_STATE;
dispense();
}
}
/**
* 分发糖果
*/
private void dispense() {
System.out.print("分发糖果");
if (state == State.NO_CANDY_STATE) {
System.out.println(",没有糖果,无法出货");
} else if (state == State.NO_QUARTER_STATE) {
System.out.println(",没有硬币,请先付款");
} else if (state == State.HAS_QUARTER_STATE) {
System.out.println(",请先转动曲柄");
} else if (state == State.GOING_TO_PRODUCE_STATE) {
System.out.println(",出货成功");
number--;
if (number == 0) {
System.out.println("糖果卖完了");
state = State.NO_CANDY_STATE;
} else {
state = State.NO_QUARTER_STATE;
}
}
}
public String toString() {
return "state:" + String.valueOf(state) + "\tinventory:" + number;
}
/**
* 装填糖果
*
* @param number 新加入糖果数量
*/
public void refill(int number) {
System.out.println("before refill:" + this.number);
this.number += number;
if (number > 0 || state == State.NO_CANDY_STATE) {
state = State.NO_QUARTER_STATE;
}
System.out.println("after refill:" + this.number);
}
public enum State {
/**
* 没有25美分
*/
NO_QUARTER_STATE,
/**
* 有25美分
*/
HAS_QUARTER_STATE,
/**
* 即将产生糖果
*/
GOING_TO_PRODUCE_STATE,
/**
* 售罄
*/
NO_CANDY_STATE
}
}
主函数测试
public class Main {
public static void main(String[] args) {
GumBall gumBall = new GumBall(2);
System.out.println(gumBall.toString());
// 正常流程
gumBall.insertQuarter();
gumBall.turnCrank();
System.out.println(gumBall.toString());
// 直接退款无效
gumBall.ejectQuarter();
System.out.println(gumBall.toString());
// 投钱再取出,再转动曲柄,无效
gumBall.insertQuarter();
gumBall.ejectQuarter();
gumBall.turnCrank();
System.out.println(gumBall.toString());
// 投钱再转动曲柄,再取出,有效
gumBall.insertQuarter();
gumBall.turnCrank();
gumBall.ejectQuarter();
System.out.println(gumBall.toString());
// 没货了
gumBall.insertQuarter();
gumBall.ejectQuarter();
gumBall.turnCrank();
System.out.println(gumBall.toString());
}
}
输出:
state:NO_QUARTER_STATE inventory:2
投入硬币,成功投入一枚硬币
转动曲柄
分发糖果,出货成功
state:NO_QUARTER_STATE inventory:1
退款,没有硬币
state:NO_QUARTER_STATE inventory:1
投入硬币,成功投入一枚硬币
退款,退还硬币成功
转动曲柄,未付款,请先投入硬币
state:NO_QUARTER_STATE inventory:1
投入硬币,成功投入一枚硬币
转动曲柄
分发糖果,出货成功
糖果卖完了
退款,已售罄
state:NO_CANDY_STATE inventory:0
投入硬币,售罄
退款,已售罄
转动曲柄,已售罄
state:NO_CANDY_STATE inventory:0
到目前为止好像都很顺利?
然后,最讨厌的部分还是来了,改需求!?
现在,糖果机为了增加销量增加,有10%的几率会在转动曲柄的时候出来两颗糖果。
这样糖果机会多出一个 '中奖’ 状态
那么,此时就要修改原来写好的代码逻辑:
在insetQuarter、ejectQuarter、turnCrank()、dispense()等方法里,每个方法再加上一个if条件判断->是否为中奖状态。
而且turnCrank方法会变得特别复杂,因为这里就需要考虑是转变为中奖状态(winner_state)还是即将销售状态(Going_to_produce_state)。
存在的问题:
这样的程序就更像是一般我们写C++的顺序的,或者说是过程化的编程范式,没有用到面向对象的设计思想,而且扩展性很差。
无独有偶,在《重构:改善既有代码的设计》一书中,提到一个概念:
代码的坏味道(bad smell):
其中一种就是Long Method(过长的方法)
由此,状态模式就出场了⬇。
⚡状态模式
-
新的设计:
将状态对象封装到各自类中,然后在动作发生时委托给当前状态。
-
步骤:
-
定义一个State接口。
糖果机的每个动作都有一个对应的方法,然后把所有动作封装好。
-
为机器中每个状态实现状态类。
这些具体状态类负责在对应状态下进行机器的行为。
-
将动作委托到状态类。
-
-
代码
public interface State{ void insertQuarter(); void ejectQuarter(); void turnGrank(); void dispense(); }
/** * 没有硬币状态 */ class NoQuarterState implements State(){ NewGumball newGumBall; public NoQuarterState(NewGumBall newGumBall){ this.newGumBall = newGumBall; } @Override public void insertQuarter(){ System.out.println("成功投入一枚硬币"); newGumBall.setState(newGumBall.getHasQuarterState()); } @Override public void ejectQuarter(){ System.out.println("没有硬币,退款失败"); } @Override public void turnCrank(){ System.out.println("请先投入硬币再转动曲柄"); } @Override public void dispense(){ System.out.println("请先投入硬币"); } @Override public String toString(){ System.out.println("NoQuarterState"); } }
/** * 没有糖果状态 */ public class NoCandyState implements State { private NewGumBall newgumBall; public NoCandyState(NewGumBall newGumBall) { this.newgumBall = newGumBall; } @Override public void insertQuarter() { System.out.println("售罄"); } @Override public void dispense() { System.out.println("售罄"); } @Override public void ejectQuarter() { System.out.println("售罄"); } @Override public void turnCrank() { System.out.println("售罄"); } @Override public String toString() { return "NoCandyState"; } }
/** * 即将出货状态 */ public class GoingToBeProduceState implements State { private NewGumBall newGumBall; public GoingToBeProduceState(NewGumBall newGumBall) { this.newGumBall = newGumBall; } @Override public void insertQuarter() { System.out.println("正在出货,请勿重复投币"); } @Override public void dispense() { newGumBall.releaseCandy(); if (newGumBall.getCandyNumber() > 0) { newGumBall.setState(newGumBall.getNoQuarterState()); } else { System.out.println("最后一个糖果已卖出"); newGumBall.setState(newGumBall.getNoCandyState()); } System.out.println("出货成功"); } @Override public void ejectQuarter() { System.out.println("退款失败,硬币已投入"); } @Override public void turnCrank() { System.out.println("正在出货,请勿转动曲柄"); } @Override public String toString(){ return "GoingToBeProduceState"; } }
/** * 有硬币状态 */ public class HasQuarterState implements State { private NewGumBall newGumBall; public HasQuarterState(NewGumBall newGumBall) { this.newGumBall = newGumBall; } @Override public void insertQuarter() { System.out.println("已投入硬币,请勿重复投入"); } @Override public void dispense() { System.out.println("请转动曲柄"); } @Override public void ejectQuarter() { System.out.println("退款成功"); newGumBall.setState(newGumBall.getNoQuarterState()); } @Override public void turnCrank() { System.out.println("转动曲柄,等待出货"); newGumBall.setState(newGumBall.getGoingToBeProduceState()); } @Override public String toString(){ return "HasQuarterState"; } }
/** * 全新的糖果机 */ public class NewGumBall { private State state; private State goingToBeProduceState; private State HasQuarterState; private State NoCandyState; private State NoQuarterState; private int candyNumber = 0; public NewGumBall(int candyNumber) { goingToBeProduceState = new GoingToBeProduceState(this); HasQuarterState = new HasQuarterState(this); NoCandyState = new NoCandyState(this); NoQuarterState = new NoQuarterState(this); if (candyNumber >= 0) { this.candyNumber = candyNumber; state = NoQuarterState; } else { state = NoCandyState; } } public void releaseCandy() { System.out.println("糖果已出货"); if (candyNumber > 0) { candyNumber--; } } public void setState(State state) { this.state = state; } public int getCandyNumber() { return candyNumber; } public State getGoingToBeProduceState() { return goingToBeProduceState; } public State getHasQuarterState() { return HasQuarterState; } public State getNoCandyState() { return NoCandyState; } public State getNoQuarterState() { return NoQuarterState; } public void insertQuarter() { state.insertQuarter(); } public void turnCrank() { state.turnCrank(); state.dispense(); } public void ejectQuarter() { state.ejectQuarter(); } @Override public String toString() { return "state:" + state + "\t inventory:" + candyNumber; } }
public class Main { public static void main(String[] args) { NewGumBall newGumBall = new NewGumBall(2); System.out.println(newGumBall.toString()); // 正常流程 newGumBall.insertQuarter(); newGumBall.turnCrank(); System.out.println(newGumBall.toString()); // 直接退款无效 newGumBall.ejectQuarter(); System.out.println(newGumBall.toString()); // 投钱再取出,再转动曲柄,有效 newGumBall.insertQuarter(); newGumBall.ejectQuarter(); newGumBall.turnCrank(); System.out.println(newGumBall.toString()); // 投钱再转动曲柄,再取出,无效 newGumBall.insertQuarter(); newGumBall.turnCrank(); newGumBall.ejectQuarter(); System.out.println(newGumBall.toString()); // 没货了 newGumBall.insertQuarter(); newGumBall.ejectQuarter(); System.out.println(newGumBall.toString()); } }
输出:
state:NoQuarterSate inventory:2
成功投入一枚硬币
转动曲柄,等待出货
糖果已出货
出货成功
state:NoQuarterSate inventory:1没有硬币,退款失败
state:NoQuarterSate inventory:1成功投入一枚硬币
退款成功
请先投入硬币再转动曲柄
请先投入硬币
state:NoQuarterSate inventory:1成功投入一枚硬币
转动曲柄,等待出货
糖果已出货
最后一个糖果已卖出
出货成功
售罄
state:NoCandyState inventory:0售罄
售罄
售罄
售罄
state:NoCandyState inventory:0-
好处?
-
将与特定状态相关的行为局部化,并且将不同状态的行为分割开。
局部化:将具有普适性的行为抽象出来,放到一个抽象对象中封装起来。
这样,当有新的状态加入时,我们可以通过定义新的状态类来实现接口/继承父类,来消除庞大的条件分支语句来判断状态。
原理:把逻辑分布到State子类之间,来减少相互间的依赖。
-
而且当从这个状态对象转化为另为一个状态对象时,封装起来的行为潜在的改变,但是用户对此毫不知情。
-
-
缺点:
子类太多,不好管理。
-
适用情况:
-
当一个对象有很多种状态,而且他的行为依赖于他的状态,并且在运行时可能动态改变。
-
一个对象中含有庞大的条件分支语句,并且这些分支依赖于该对象的状态。
-
-
⚡设计模式之外的部分
-
多范式编程语言: 支持超过一种编程范型语言
-
编程范型: 一类典型编程风格,如:
并发编程,约束编程,数据流编程,声明性编程,分布式的编程,函数式编程,泛型编程,命令式(指令式)编程,逻辑编程,元编程,面向对象编程
过程式编成: 主要采取程序调用(procedure call)或函数调用(function call)的方式来进行流程控制。
编程范型提供并决定程序员对程序执行的看法
Scala是一门多范式的编程语言,集成面向对象和函数式编程的特性。
C++支持过程化,面向对象,范型编程
-
符合迪米特法则(得墨忒耳定律 Law of Demeter -> LoD、最小知识原则):
迪米特法则可以简单说成:talk only to your immediate friends。
一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
-
每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;
-
每个单元只能和它的朋友交谈:不能和陌生单元交谈;
-
只和自己直接的朋友交谈。
得墨忒耳定律使得软件更好的可维护性与适应性。
因为对象较少依赖其它对象的内部结构,可以改变对象容器(container)而不用改变它的调用者(caller)。
一个简单例子是,人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。
-
-
卫语句
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。
常用到的地方: if语句使用“卫语句 ”减少层级嵌套。
本博客所有文章除特别声明外,均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。转载请注明出处!