命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

一个家电公司想邀请你设计一个家电自动化遥控器的API。这个遥控器有7个可编程的插槽,每个都可以指定到一个不同的家电装置。每个插槽都有对应的“打开”和“关闭”按钮。这个遥控器还具备一个整体的撤销按钮。

现在已经有一组Java类,这些类是多家产商开发出来的,用来控制家电自动化装置,例如电灯、风扇、热水器、音响设备和其他类似的可控制装置。

你要做的是创建一组控制遥控器的API,让每个插槽都能够控制一个或一组装置。请注意,要能够控制目前的装置和任何未来可能出现的装置,这一点很重要。

先看下产商的类

以下是部分产商的类,这些可以使你对即将设计的对象的接口有一些想法:

public class TV {
public void on(){}
public void off(){}
public void setInputChannel(){}
public void setVolume(){}
} public class CeilingFan {
public void high(){}
public void medium(){}
public void low(){}
public void off(){}
public void getSpeed(){}
} public class OutDoorLight {
public void on(){}
public void off(){}
}

如何设计遥控器的API?

可以看到,现在我们有一套五花八门的产商类。许多类都具备on()和off()方法,除此之外,还有一些方法像是high()、setInputChannel()、setVolume()等。不只这样,将来可能还会有更多的产商类。而且每个类还会有各式各样的方法。

设计API的关键点在于,遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需要知道这些家电自动化的细节,或者如何打开热水器。不能在遥控器内包含一大堆if语句,例如“if slot1==Light, then light.on()”,否则只要有新的产商类进来,就必须修改代码,这会造成潜在的错误,而且工作没完没了。

要怎么将遥控器和产商类的实例解耦?可以考虑使用命令模式。命令模式可将“动作的请求者”从“动作的执行者”对象中解耦。在这个例子里,请求者是遥控器,而执行者对象就是产商类其中之一的实例。

我们可以把请求(例如打开电灯)封装成一个特定对象(例如客厅电灯对象)。所以,如果对每个按钮都存储一个命令对象,那么当按钮被按下的时候,就可以请命令对象做相关的工作。遥控器并不需要知道工作内容是什么,只要有个命令对象能和正确的对象沟通,把事情做好就可以了。所以,遥控器和电灯对象解耦了。

进一步理解命令模式

我们都知道餐厅是怎么工作的:

  1. 顾客把订单交给服务员;
  2. 服务员拿到订单后,放在订单柜台,然后告诉厨师:“订单来了!”;
  3. 厨师根据订单准备餐点。

    让我们更详细地研究下这个交互过程:

一张订单封装了准备餐点的请求

把订单想象成一个用来请求准备餐点的对象,和一般的对象一样,订单对象可以被传递:从服务员传递到订单柜台,或者从一个服务员传递给下一班的服务员。订单的接口只包含一个方法,这个方法封装了准备餐点所需的动作。订单内有一个到“需要进行准备工作的对象”(也就是厨师)的引用。这一切都被封装起来,所以服务员不需要知道订单上有什么,也不需要知道是谁来准备餐点;她只需要将订单放到订单窗口,然后喊一声“订单来了!”就可以了。

服务员的工作是接受订单,然后调用订单对象的方法

服务员的工作很简单:接下顾客的订单,继续帮助下一个顾客,然后将一定数量的订单放到订单柜台,并调用订单对象的方法,让厨师来准备餐点。服务员不需要担心订单的内容是什么,或者由谁来准备餐点。她只需要知道,订单有个准备餐点的方法可以调用,这就够了。

厨师具备准备餐点的知识

厨师是一种对象,他真正知道如何准备餐点。一旦服务员调用准备餐点的方法,厨师就接手,实现创建餐点的所有方法。请注意,服务员和厨师之间是彻底的解耦:服务员的订单封装了餐点的细节,她只要调用每个订单的方法即可,而厨师看了订单就知道该做些什么餐点;厨师和服务员之间从来不需要直接沟通。

把餐厅想成是OO设计模式的一种模型,而这个模型允许将“发出请求的对象”和“接受与执行这些请求的对象”分割开来。比方说,对于遥控器API,我们需要分割开“发出请求的按钮代码”和“执行请求的产商特定对象”。假如遥控器的每个插槽都持有一个像餐厅订单那样的对象,会怎么样?那么,当一个按钮被按下,只要调用该对象的方法,电灯就开了,而遥控器不需要知道事情是怎么发生的,也不需要知道涉及哪些对象。

实现一个命令对象

现在让我们开始建立第一个命令对象。

实现命令接口

首先,让所有的命令对象实现相同的包含一个execute()方法的接口:

public interface Command {
public void execute();
}

实现一个打开电灯的命令

现在,假设想实现一个打开电灯的命令。根据产商提供的类,Light类有两个方法:on()和off()。下面是如何将它实现成一个命令:

// 这是一个命令,所以需要实现Command接口
public class LightOnCommand implements Command {
Light light; // 构造器被传入了某个电灯(比方说:客厅的电灯),然后记录在实例变量中。
// 一旦调用execute(),就由这个电灯对象成为接收者,负责接收请求。
public LightOnCommand(Light light){
this.light = light;
} // 这个execute()方法调用接收对象(我们正在控制的电灯)的on()方法
@Override
public void execute() {
light.on();
}
}

使用命令对象

让我们把这一切简化,假设我们的遥控器只有一个按钮和对应的插槽,可以控制一个装置:

public class SimpleRemoteControl {
// 有一个插槽持有命令,而这个命令控制着一个装置
Command slot; public SimpleRemoteControl(){} // 这个方法用来设置插槽控制的命令。如果这段代码的客户想要改变遥控器按钮的行为,
// 可以多次调用这个方法。
public void setCommand(Command command){
slot = command;
} // 当按下按钮时,这个方法就会被调用,使得当前命令衔接插槽,并调用它的execute()方法
public void buttonWasPressed(){
slot.execute();
}
}

遥控器使用的简单测试

下面这段代码用来测试上面的简单遥控器:

// 控制器就是调用者,会传入一个命令对象,可以用来发出请求
SimpleRemoteControl remote = new SimpleRemoteControl();
// 现在创建了一个电灯对象,此对象也就是请求的接收者
Light light = new Light();
// 在这里创建一个命令,然后将接收者传给它
LightOnCommand lightOn = new LightOnCommand(light); // 把命令传给调用者
remote.setCommand(lightOn);
// 模拟按下按钮
remote.buttonWasPressed();

定义命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

我们知道一个命令对象通过在特定接收者上绑定一组动作来封装一个请求,要达到这一点,命令对象将动作和接收者包进对象中。这个对象只暴露出一个execute()方法,当此方法被调用的时候,接收者就会进行这些动作。从外面来看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execute()方法,请求的目的就能达到。

我们也看到了利用命令来参数化对象的一些例子。再回到餐厅,一整天下来,服务员参数化了许多订单。在简单遥控器中,我们用一个“打开电灯”命令加载按钮插槽。就和服务员一样,遥控器插槽根本不在乎所拥有的是什么命令对象,只要该对象实现了Command接口就可以了。

我们还未说到使用命令模式来实现“队列、日志和支持撤销操作”。这是基本命令模式相当直接的扩展,稍后就会看到这些内容。

实现遥控器

现在让我们去实现有7个插槽的遥控器。

public class RemoteControl {
// 这个时候,遥控器要处理7个开与关的命令,使用相应数组记录这些命令
Command[] onCommands;
Command[] offCommands; // 在构造器中,只需实例化并初始化这两个开与关的数组
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7]; Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
} // setCommand()方法有3个参数,分别是插槽的位置、开的命令和关的命令。
// 这些命令将记录在开关数组中对应的插槽位置,以供稍后使用
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
} // 当按下开或关的按钮,硬件就会负责调用对应的方法
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
} public void offButtonWasPushed(int slot){
offCommands[slot].execute();
}
}

加上撤销功能

好了,我们现在需要在遥控器上加上撤销的功能。这个功能使用起来就像是这样的:比如说客厅的电灯是关闭的,然后你按下遥控器上的开启按钮,自然电灯就被打开了。现在如果按下撤销按钮,那么上一个动作将被倒转,在这个例子里,电灯将被关闭。

当命令支持撤销时,该命令就必须提供和execute()方法相反的undo()方法。不管execute()刚才做什么,undo()都会倒转过来。我们需要在Command接口中加入undo()方法。

public interface Command {
public void execute();
public void undo();
}

现在让我们深入电灯的命令,并实现undo()方法。

public class LightOnCommand implements Command {
Light light; public LightOnCommand(Light light){
this.light = light;
} @Override
public void execute() {
light.on();
} @Override
public void undo() {
light.off();
}
}

要加上撤销按钮,必须对遥控器做一些小修改。

public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
// 前一个命令将被记录在这里
Command undoCommand; public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7]; Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
// 一开始并没有所谓的“前一个命令”,所以将它设置成NoCommand
undoCommand = noCommand;
} public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
} // 当按下按钮,我们取得这个命令,并优先执行它,然后将它记录在undoCommand中。
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
undoCommand = onCommands[slot];
} public void offButtonWasPushed(int slot){
offCommands[slot].execute();
undoCommand = offCommands[slot];
} // 当按下撤销按钮,我们调用undoCommand实例变量的undo()方法,就可以倒转前一个命令
public void undoButtonWasPushed(){
undoCommand.undo();
}
}

《Head first设计模式》之命令模式的更多相关文章

  1. Java进阶篇设计模式之八 ----- 责任链模式和命令模式

    前言 在上一篇中我们学习了结构型模式的享元模式和代理模式.本篇则来学习下行为型模式的两个模式, 责任链模式(Chain of Responsibility Pattern)和命令模式(Command ...

  2. Java设计模式学习记录-命令模式

    前言 这次要介绍的是命令模式,这也是一种行为型模式.最近反正没有面试机会我就写博客呗,该投的简历都投了.然后就继续看书,其实看书也会给自己带来成就感,原来以前不明白的东西,书上已经给彻底的介绍清楚了, ...

  3. 《JAVA设计模式》之命令模式(Command)

    在阎宏博士的<JAVA与模式>一书中开头是这样描述命令(Command)模式的: 命令模式属于对象的行为模式.命令模式又称为行动(Action)模式或交易(Transaction)模式. ...

  4. Java设计模式之八 ----- 责任链模式和命令模式

    前言 在上一篇中我们学习了结构型模式的享元模式和代理模式.本篇则来学习下行为型模式的两个模式, 责任链模式(Chain of Responsibility Pattern)和命令模式(Command ...

  5. java设计模式-----23、命令模式

    概念: Command模式也叫命令模式 ,是行为设计模式的一种.Command模式通过被称为Command的类封装了对目标对象的调用行为以及调用参数. 命令模式(Command Pattern)是一种 ...

  6. Java设计模式(20):命令模式

    本文源码:GitHub·点这里 || GitEE·点这里 一.生活场景 1.场景描述 智能电脑的品牌越来越多,由此诞生了一款电脑控制的APP,万能遥控器,用户在使用遥控器的时候,可以切换为自家电视的品 ...

  7. 重学 Java 设计模式:实战命令模式「模拟高档餐厅八大菜系,小二点单厨师烹饪场景」

    作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 持之以恒的重要性 初学编程往往都很懵,几乎在学习的过程中会遇到 ...

  8. IOS设计模式之四(备忘录模式,命令模式)

    本文原文请见:http://www.raywenderlich.com/46988/ios-design-patterns. 由 @krq_tiger(http://weibo.com/xmuzyq) ...

  9. IDEA terminal无法从vim的编辑模式转换为命令模式

    Git 修改最后一次的commit历史记录:https://www.baidu.com/link?url=2WF8yFd0iBuVmXLWfutmSoXa12K9D143e_B0A3PTYYHEP9r ...

  10. java设计模式8.迭代子模式、责任链模式、命令模式

    迭代子模式 迭代子模式可以顺序地访问一个聚集中的元素而不必暴露聚集的内部表象.它将迭代逻辑封装到一个独立的迭代子对象中,从而与聚集本身分开.迭代子对象是对遍历的抽象化,不同的聚集对象可以提供相同的迭代 ...

随机推荐

  1. 输入n个字符串,找出最长最短字符串(若有个数相同的,都打印出来)

    首先,要求找到最长最短字符串,我们应该用数组将其存起来,输入的个数是不固定的,我们就可以用Scanner获取要输入的个数,最终找到的个数也不固定,我们可以封装两个方法,并且返回值类型为数组. 我遇到的 ...

  2. Spring Boot从零入门3_创建Hello World及项目剖析

    目录 1 前言 2 名词术语 3 创建Hello World项目 3.1 基于STS4创建项目 3.2 使用Spring Initializr Website创建项目并导入 3.3 基于Spring ...

  3. Bonny手机APP试用体验

    在上周四(即6月13日)下午,应王建民老师的邀请,我参观了学长学姐们的软件设计评比以及专业交流的活动,看到了形形色色学长学姐设计出的软件我觉得非常有趣,并对学长学姐们设计的软件的种类与功能感到由衷的钦 ...

  4. 极简安装 TensorFlow 2.0 GPU

    前言 之前写了几篇关于 TensorFlow 1.x GPU 版本安装的博客,但几乎没怎么学习过.之前基本在搞 Machine Learning 和 Data Mining 方面的东西,极少用到 NN ...

  5. Python 任务自动化工具:nox 的配置与 API

    英文 | Configuration & API 出处 | nox 官方文档 译者 | 豌豆花下猫@Python猫 Github地址:https://github.com/chinesehua ...

  6. canal 基于Mysql数据库增量日志解析

    canal 基于Mysql数据库增量日志解析  1.前言  最近太多事情 工作的事情,以及终身大事等等 耽误更新,由于最近做项目需要同步监听 未来电视 mysql的变更了解到公司会用canal做增量监 ...

  7. ORM基础5

    一.一对一 场景:字段多,且一部分字段使用率高 优点:提高效率 实质:唯一的外键 # Person表 class Person(models.Model): id = models.AutoField ...

  8. DirectX11 Windows Windows SDK--28 计算着色器:波浪(水波)

    前言 有关计算着色器的基础其实并不是很多.接下来继续讲解如何使用计算着色器实现水波效果,即龙书中所实现的水波.但是光看代码可是完全看不出来是在做什么的.个人根据书中所给的参考书籍找到了对应的实现原理, ...

  9. 多级反向代理java获取真实IP地址

    public static String getIpAddress(HttpServletRequest request){ String ip = request.getHeader("x ...

  10. EFK教程(5) - ES集群开启用户认证

    基于ES内置及自定义用户实现kibana和filebeat的认证 作者:"发颠的小狼",欢迎转载 目录 ▪ 用途 ▪ 关闭服务 ▪ elasticsearch-修改elastics ...