定义

把请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作



图纸

设计模式——2_1 命令(Command)-LMLPHP



一个例子:空调和他的遥控器

假定你入职了一家空调公司,接手了和立式空调有关的模块。不久之后,公司决定给这个立式空调增加一个遥控器(原有的立式空调全部都是通过控制面板上的按钮进行操作的)。正当你摩拳擦掌准备大干一番的时候,前辈留给你的紧耦合代码却让你差点当场骂娘。困难面前,你是要说服上司放弃遥控器的企划,还是连夜删库跑路?又或者重构已有的代码……

铺垫有点太长了,总之这次的例子开始了:


只有控制面板的空调

言归正传,前辈留下的代码是这样的:

设计模式——2_1 命令(Command)-LMLPHP

AirConditioner(空调)

/**
 * 空调
 */
public class AirConditioner {

    public static final int MAX_TEMPERATURE = 32;//最高温度
    public static final int MIN_TEMPERATURE = 18;//最低温度
    private static final String[] MODE_ARRAY = {"制冷模式", "睡眠模式", "送风模式"};

    /**
     * 温度
     */
    private Float temperature;

    /**
     * 当前模式
     */
    private Integer modePoint;

    /**
     * 是否是开启的
     */
    private boolean isOn;

    /**
     * 控制面板
     */
    private ControlPanel controlPanel;

    public AirConditioner() {
        controlPanel = ControlPanel.createControlPanel(this);
    }

    public static String[] getModeArray() {
        return MODE_ARRAY.clone();
    }

    public Float getTemperature() {
        return temperature;
    }

    public void setTemperature(Float temperature) {
        if (temperature == null || (temperature >= MIN_TEMPERATURE && temperature <= MAX_TEMPERATURE)) {
            this.temperature = temperature;
        }
    }

    public String getMode() {
        return MODE_ARRAY[modePoint];
    }

    public Integer getModePoint() {
        return modePoint;
    }

    public void setModePoint(Integer modePoint) {
        this.modePoint = modePoint;
    }

    public boolean isOn() {
        return isOn;
    }

    public void setOn(boolean on) {
        isOn = on;
    }
}

ControlPanel

/**
 * 控制面板
 */
public class ControlPanel {

    private final AirConditioner airConditioner;

    private ControlPanel(AirConditioner airConditioner) {
        this.airConditioner = airConditioner;
    }

    public static ControlPanel createControlPanel(AirConditioner airConditioner) {
        ControlPanel controlPanel = new ControlPanel(airConditioner);
        controlPanel.off();
        return controlPanel;
    }

    /**
     * 开机
     */
    public void on() {
        if (!airConditioner.isOn()) {
            //关机模式才可以执行这个动作
            airConditioner.setOn(true);//设定开机
            airConditioner.setTemperature(26f);//默认26度
            airConditioner.setModePoint(0);//默认第一个模式
        }
    }

    /**
     * 关机
     */
    public void off() {
        if (airConditioner.isOn()) {
            //开机状态才可以执行这个动作
            airConditioner.setOn(false);
            airConditioner.setTemperature(null);
            airConditioner.setModePoint(null);
        }
    }

    /**
     * 温度上升1
     */
    public void addTemperature() {
        if (airConditioner.isOn()) {
            airConditioner.setTemperature(airConditioner.getTemperature() + 1);
        }
    }

    /**
     * 温度下降1
     */
    public void lessenTemperature() {
        if (airConditioner.isOn()) {
            airConditioner.setTemperature(airConditioner.getTemperature() - 1);
        }
    }

    /**
     * 下一模式
     */
    public void nextMode() {
        Integer modePoint = airConditioner.getModePoint();
        if (airConditioner.isOn()) {
            if (modePoint + 1 >= AirConditioner.getModeArray().length) {
                airConditioner.setModePoint(modePoint + 1);
            } else {
                airConditioner.setModePoint(0);
            }
        }
    }
}

在这个只有控制面板的立式空调里面,前辈通过 AirConditioner(空调) 来表示一部空调的底层函数,然后通过 ControlPanel(控制面板)Client 提供操作空调内部属性的接口

虽然它可以如预期一般完成任务,但这种设计绝算不上优雅,他的问题主要体现在 控制面板 和空调底层方法之间的耦合过于紧密,现在只有一种类型的空调,如果有底层方法不相同的第二种类型的空调出现,那么这个 控制面板 是无法与其兼容的


遥控器

遥控器 的引入改变了现态,很显然,遥控器 至少需要拥有和 控制面板 一样的效果,而且一个 遥控器 必须可以同时对应多个 空调 对象。也就是说,你只有在最终调用遥控器上面的任务的时候才会知道到底要调用哪个空调上的方法,做出来的效果应该是这样的:

设计模式——2_1 命令(Command)-LMLPHP

我们新增了 RemoteController(遥控器) 作为遥控器的实现,而 RemoteControllerControlPanel 的实现唯一的区别就在于 ControlPanelAirConditioner 从他诞生的时候就被定义且无法修改,而 RemoteControllerAirConditioner 在调用命令的时候才会被指定

除此之外,程序中出现了大量的重复代码,这些重复代码分布在 控制面板遥控器 的每一个对应方法中

有没有办法把他们解耦?我的意思是把调用者和他的实现解耦,也就是说做成这样的效果:

设计模式——2_1 命令(Command)-LMLPHP


放在本例中,他长这样:

设计模式——2_1 命令(Command)-LMLPHP

Executor & Pool

/**
 * 空调命令执行器
 */
public abstract class ACExecutor implements Cloneable {

	/**
	 * 执行命令
	 */
	public abstract void execute(AirConditioner airConditioner);

	/**
	 * 克隆方法,覆盖Object中的clone
	 */
	public ACExecutor clone() {
		try {
			return (ACExecutor) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			throw new RuntimeException(e);//异常原样抛出
		}
	}
}

public class OnExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (!airConditioner.isOn()) {
			//关机模式才可以执行这个动作
			airConditioner.setOn(true);//设定开机
			airConditioner.setTemperature(26f);//默认26度
			airConditioner.setModePoint(0);//默认第一个模式
		}
	}
}

public class OffExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (airConditioner.isOn()) {
			//开机状态才可以执行这个动作
			airConditioner.setOn(false);
			airConditioner.setTemperature(null);
			airConditioner.setModePoint(null);
		}
	}
}

public class AddTemperatureExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (airConditioner.isOn()) {
			airConditioner.setTemperature(airConditioner.getTemperature() + 1);
		}
	}
}

public class LessenTemperatureExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (airConditioner.isOn()) {
			airConditioner.setTemperature(airConditioner.getTemperature() - 1);
		}
	}
}

public class NextModeExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		Integer modePoint = airConditioner.getModePoint();
		if (airConditioner.isOn()) {
			if (modePoint + 1 >= AirConditioner.getModeArray().length) {
				airConditioner.setModePoint(modePoint + 1);
			} else {
				airConditioner.setModePoint(0);
			}
		}
	}
}

/**
 * 空调命令执行器的对象池
 */
public class ACExecutorPool {

	//单例相关
	private static final ACExecutorPool INSTANCE = new ACExecutorPool();

	public static ACExecutorPool getInstance() {
		return INSTANCE;
	}

	//原型池
	private final Map<Class<? extends ACExecutor>, ACExecutor> prototypePool = new HashMap<>();

	public ACExecutor createOnExecutor() {
		return getNewObjectByPool(OnExecutor.class);
	}

	public ACExecutor createOffExecutor() {
		return getNewObjectByPool(OffExecutor.class);
	}

	public ACExecutor createAddTemperatureExecutor() {
		return getNewObjectByPool(AddTemperatureExecutor.class);
	}

	public ACExecutor createLessenTemperatureExecutor() {
		return getNewObjectByPool(LessenTemperatureExecutor.class);
	}

	public ACExecutor createNextModelExecutor() {
		return getNewObjectByPool(NextModeExecutor.class);
	}

	/**
	 * 从原型池中获取对应的新命令对象
	 */
	private ACExecutor getNewObjectByPool(Class<? extends ACExecutor> c) {
		if (prototypePool.containsKey(c)) {
			return prototypePool.get(c).clone();
		} else {
			try {
				ACExecutor prototype = c.newInstance();
				prototypePool.put(c, prototype);
				return prototype.clone();
			} catch (InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
				throw new RuntimeException("初始化命令对象原型池的时候出现了异常,具体异常为:" + e.getCause(), e);
			}
		}
	}
}

ControlPanel & RemoteController

/**
 * 控制面板
 */
public class ControlPanel {

	private final AirConditioner airConditioner;

	private ControlPanel(AirConditioner airConditioner) {
		this.airConditioner = airConditioner;
	}

	public static ControlPanel createControlPanel(AirConditioner airConditioner) {
		ControlPanel controlPanel = new ControlPanel(airConditioner);
		controlPanel.off();
		return controlPanel;
	}

	/**
	 * 开机
	 */
	public void on() {
		ACExecutorPool.getInstance().createOnExecutor().execute(airConditioner);
	}

	/**
	 * 关机
	 */
	public void off() {
		ACExecutorPool.getInstance().createOffExecutor().execute(airConditioner);
	}

	/**
	 * 温度上升1
	 */
	public void addTemperature() {
		ACExecutorPool.getInstance().createAddTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 温度下降1
	 */
	public void lessenTemperature() {
		ACExecutorPool.getInstance().createLessenTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 下一模式
	 */
	public void nextMode() {
		ACExecutorPool.getInstance().createNextModelExecutor().execute(airConditioner);
	}
}

/**
 * 遥控器
 */
public class RemoteController {

	/**
	 * 开机
	 */
	public void on(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createOnExecutor().execute(airConditioner);
	}

	/**
	 * 关机
	 */
	public void off(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createOffExecutor().execute(airConditioner);
	}

	/**
	 * 温度上升1
	 */
	public void addTemperature(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createAddTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 温度下降1
	 */
	public void lessenTemperature(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createLessenTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 下一模式
	 */
	public void nextMode(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createNextModelExecutor().execute(airConditioner);
	}
}

在这种实现方式里,我们把具体执行的操作封装到 ACExecutor(空调执行器) 类簇中,为 所有的操作定义了自己的执行类;然后在 client 点击 控制面板遥控器 上的对应按钮的时候,把让他们获取对应的 执行器对象,并执行对应的操作。而 控制面板遥控器 根本不关心 执行器 的底层做了什么操作,这让他们从命令的执行者变成了命令的调度者


听起来很复杂,但是这种复杂是 颗粒度 细致程度导致的(五个命令导致我们需要五个对应的执行者子类),流程上其实是很简单的。以开机为例,我们的程序其实是这样做的:

设计模式——2_1 命令(Command)-LMLPHP

这种让程序变得复杂的写法是有价值的,至少可以让你在以下两种情况可以少写很多代码:

  1. 当出现新的操作面板,比如手机APP控制空调的时候,我就可以通过创建新的控制器类并让他调用已有的执行者来实现,而不需要再重复执行者里面的代码
  2. 如果出现了接口一致,但行为不一致的空调,那我依然可以继续使用控制面板和遥控器,只需要建立对应的执行器类簇即可

而这正是一个标准的命令实现


可以撤销的操作

一般我讲到 ”这正是xxx的实现“ 的时候,例子就结束了,但这次是例外

请留意一下上例的一个配角类—— ACExecutorPool,这个类的作用是为我们的程序产出可靠的执行类对象。在上例中,这一个类里,用到了两种模式,分别是 单例原型

单例很好理解,为了让全局都用一个对象池

为什么要用原型呢?每个执行器都用单例不是更节省吗?


这种情况下会用原型只有一种可能,那就是执行器应该是带状态的,而且他的状态是有意义的

那你会说了,不对啊,上例的执行器哪有状态。别急,业务来了


某日,接到通知,我们需要在 控制面板 上添加一个 返回(back) 按钮,用于撤销我们刚刚执行的命令,要怎么实现呢?

如果你没有使用命令模式实现这种功能费死劲,但是在命令模式的框架下,你可以这样做:

设计模式——2_1 命令(Command)-LMLPHP

我们在 ACExecutor 中提供了用于回滚的方法 back,而在 控制面板 中我们通过添加 history 列表的方式存储已经执行过的执行器,以便我们回滚

这时候 ACExecutor 的对象就绝不能用单例了,因为他的属性是一种凭证,用于证明这个执行器对象有没有执行成功。此时原型模式就是你比较合适的选择了,因为执行器对象是会被大量创建的,原型可以有效的降低创建执行器的开销(复制初始属性

而这也是命令模式所能实现的功能之一



碎碎念

命令和Runnable

Java的多线程模块中用到了很标准的命令模式

我们通过 Thread 来管控线程,但是线程具体如何执行是由 Runnable 来决定的

也就是说 Thread 本质上其实就是 ReceiverRunnable 才是掌握具体内容的执行器


命令和事务

命令模式中的执行器的颗粒度你是可以自己掌握的,你可以只让他执行单体命令,也可以让他执行多个指令捆绑在一起的复合指令

这就是SQL中的事务一样,他是一个具有 原子性 的整体,一荣俱荣,一损俱损




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

02-03 11:11