设计模式 -- 状态模式(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(过长的方法)

由此,状态模式就出场了⬇。


状态模式

  • 新的设计:

    状态对象封装到各自类中,然后在动作发生时委托给当前状态。

  • 步骤:

    1. 定义一个State接口

      糖果机的每个动作都有一个对应的方法,然后把所有动作封装好。

    2. 为机器中每个状态实现状态类

      这些具体状态类负责在对应状态下进行机器的行为。

    3. 将动作委托到状态类。

类图

  • 代码

    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

    • 好处?

      1. 将与特定状态相关的行为局部化,并且将不同状态的行为分割开。

        局部化:将具有普适性的行为抽象出来,放到一个抽象对象中封装起来。

        这样,当有新的状态加入时,我们可以通过定义新的状态类来实现接口/继承父类,来消除庞大的条件分支语句来判断状态。

        原理:把逻辑分布到State子类之间,来减少相互间的依赖。

      2. 而且当从这个状态对象转化为另为一个状态对象时,封装起来的行为潜在的改变,但是用户对此毫不知情。

    • 缺点:

      子类太多,不好管理。

    • 适用情况:

      1. 当一个对象有很多种状态,而且他的行为依赖于他的状态,并且在运行时可能动态改变。

      2. 一个对象中含有庞大的条件分支语句,并且这些分支依赖于该对象的状态。


设计模式之外的部分

  1. 多范式编程语言: 支持超过一种编程范型语言

  2. 编程范型: 一类典型编程风格,如:

    并发编程,约束编程,数据流编程,声明性编程,分布式的编程,函数式编程,泛型编程,命令式(指令式)编程,逻辑编程,元编程,面向对象编程

    过程式编成: 主要采取程序调用(procedure call)或函数调用(function call)的方式来进行流程控制。

    编程范型提供并决定程序员对程序执行的看法

    Scala是一门多范式的编程语言,集成面向对象和函数式编程的特性。

    C++支持过程化,面向对象,范型编程

  3. 符合迪米特法则(得墨忒耳定律 Law of Demeter -> LoD、最小知识原则):

    迪米特法则可以简单说成:talk only to your immediate friends。

    一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

    1. 每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;

    2. 每个单元只能和它的朋友交谈:不能和陌生单元交谈;

    3. 只和自己直接的朋友交谈。

    得墨忒耳定律使得软件更好的可维护性与适应性。

    因为对象较少依赖其它对象的内部结构,可以改变对象容器(container)而不用改变它的调用者(caller)。

    一个简单例子是,人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。

  4. 卫语句

    如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。

    常用到的地方: if语句使用“卫语句 ”减少层级嵌套。

  1. 拥有[短方法] (short methods)的对象会活得比较好、比较长。不熟悉面向对象技术的人,常常觉得对象程序中只有无穷无尽的delegation(委托),根本没有进行任何计算。和此类程序共同生活数年之后,你才会知道,这些小小方法有多大价值。[间接层]所能带来的全部利益——解释能力、共享能力、选择能力——都是由小型方法支持的。