案例引入
- 有一套智能家电,其中有照明灯、风扇、冰箱、洗衣机,这些智能家电来自不同的厂家,我们不想针对每一种家电都安装一个手机App来分别控制,希望只要一个app就可以控制全部智能家电
- 要实现一个app控制所有智能家电的需要,则每个智能家电厂家都要提供一个统一的接口给app调用,这样可以考虑使用命令模式
- 命令模式可将“动作的请求者”从“动作的执行者”对象中解耦出来
- 在我们的例子中,动作的请求者是手机app,动作的执行者是每个厂商的一个家电产品
介绍
基础介绍
- 一个类在进行工作时会调用自己或是其他类的方法,虽然调用结果会反映在对象的状态中,但并不会留下工作的历史记录。这时,如果有一个类用来表示“请示进行这项工作”的“命令”就会方便很多。每一项想做的工作就不再是“方法的调用”这种动态处理了,而是一个表示命令的类的实例,即可以用“物”来表示。要想管理工作的历史记录,只需管理这些实例的集合即可,而且还可以随时再次执行过去的命令,或是将多个过去的命令整合为一个新命令并执行。这样的模式称为命令模式
- 命令模式使得
请求发送者
与请求接收者
消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦,我们只需在程序运行时指定具体的请求接收者即可 - 在命令模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命令),同时命令模式也支持可撤销的操作
- 通俗易懂的理解:将军发布命令,士兵去执行(将军只需要发布一个进攻命令即可,不需要指定哪位士兵要做什么,士兵就会各司其职)。其中有几个角色:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)
登场角色
- Command(命令):负责定义命令的接口(API)
- ConcreteCommand(具体的命令):ConcreteCommand角色负责实现在Command角色中定义的接口(API)。聚合接收者,将一个接收者对象与一个动作绑定,调用接收者相应的操作来实现execute
- Receiver(接收者):Receiver角色是执行命令的对象,知道如何实施和执行一个请求对应的操作
- Client(请求者):Client角色负责生成ConcreteCommand角色并分配Receiver角色
- Invoker(发动者):调用在Command 角色中定义的接口(API)
案例实现
案例一
实现
【Commond】
package com.atguigu.command; /** * 创建命令接口 */ public interface Command { /** * 执行动作(操作) */ public void execute(); /** * 撤销动作(操作) */ public void undo(); }
【电灯命令执行者】
package com.atguigu.command; /** * 真正执行者 */ public class LightReceiver { public void on() { System.out.println(" 电灯打开了.. "); } public void off() { System.out.println(" 电灯关闭了.. "); } }
【关灯命令】
package com.atguigu.command; public class LightOffCommand implements Command { /** * 聚合LightReceiver */ LightReceiver light; /** * 构造器 * @param light */ public LightOffCommand(LightReceiver light) { super(); this.light = light; } @Override public void execute() { // 调用接收者的方法 light.off(); } @Override public void undo() { // 调用接收者的方法 light.on(); } }
【开灯命令】
package com.atguigu.command; public class LightOnCommand implements Command { /** * 聚合LightReceiver */ LightReceiver light; /** * 构造器 * * @param light */ public LightOnCommand(LightReceiver light) { super(); this.light = light; } @Override public void execute() { //调用接收者的方法 light.on(); } @Override public void undo() { //调用接收者的方法 light.off(); } }
【电视命令执行者】
package com.atguigu.command; public class TVReceiver { public void on() { System.out.println(" 电视机打开了.. "); } public void off() { System.out.println(" 电视机关闭了.. "); } }
【关电视命令】
package com.atguigu.command; public class TVOffCommand implements Command { /** * 聚合TVReceiver */ TVReceiver tv; /** * 构造器 * * @param tv */ public TVOffCommand(TVReceiver tv) { super(); this.tv = tv; } @Override public void execute() { // 调用接收者的方法 tv.off(); } @Override public void undo() { // 调用接收者的方法 tv.on(); } }
【开电视命令】
package com.atguigu.command; public class TVOnCommand implements Command { /** * 聚合TVReceiver */ TVReceiver tv; /** * 构造器 * * @param tv */ public TVOnCommand(TVReceiver tv) { super(); this.tv = tv; } @Override public void execute() { // 调用接收者的方法 tv.on(); } @Override public void undo() { // 调用接收者的方法 tv.off(); } }
【空命令】
package com.atguigu.command; /** * 没有任何命令,即空执行: 用于初始化每个按钮, 当调用空命令时,对象什么都不做 * 其实,这样是一种设计模式, 可以省掉对空判断 * @author Administrator * */ public class NoCommand implements Command { @Override public void execute() { } @Override public void undo() { } }
【遥控器】
package com.atguigu.command; public class RemoteController { /** * 开 按钮的命令数组 */ Command[] onCommands; Command[] offCommands; /** * 执行撤销的命令,必须要记住上一次执行的命令对应的撤销命令才能撤销 */ Command undoCommand; /** * 构造器,完成对按钮初始化 */ public RemoteController() { onCommands = new Command[5]; offCommands = new Command[5]; // 初始化五组命令,初始化为空命令 for (int i = 0; i < 5; i++) { onCommands[i] = new NoCommand(); offCommands[i] = new NoCommand(); } } /** * 给我们的按钮设置你需要的命令 * * @param no 命令编号 * @param onCommand * @param offCommand */ public void setCommand(int no, Command onCommand, Command offCommand) { onCommands[no] = onCommand; offCommands[no] = offCommand; } /** * 按下开按钮 * * @param no */ public void onButtonWasPushed(int no) { // 找到你按下的开的按钮, 并调用对应方法 onCommands[no].execute(); // 记录这次的操作,用于撤销 undoCommand = onCommands[no]; } /** * 按下开按钮 * * @param no */ public void offButtonWasPushed(int no) { // 找到你按下的关的按钮, 并调用对应方法 offCommands[no].execute(); // 记录这次的操作,用于撤销 undoCommand = offCommands[no]; } /** * 按下撤销按钮 */ public void undoButtonWasPushed() { undoCommand.undo(); } }
【客户端】
package com.atguigu.command; public class Client { public static void main(String[] args) { //使用命令设计模式,完成通过遥控器,对电灯的操作 //创建电灯的对象(接受者) LightReceiver lightReceiver = new LightReceiver(); //创建电灯相关的开关命令 LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver); LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver); //需要一个遥控器 RemoteController remoteController = new RemoteController(); //给我们的遥控器设置命令, 比如 no = 0 是电灯的开和关的操作 remoteController.setCommand(0, lightOnCommand, lightOffCommand); System.out.println("--------按下灯的开按钮-----------"); remoteController.onButtonWasPushed(0); System.out.println("--------按下灯的关按钮-----------"); remoteController.offButtonWasPushed(0); System.out.println("--------按下撤销按钮-----------"); remoteController.undoButtonWasPushed(); System.out.println("=========使用遥控器操作电视机=========="); TVReceiver tvReceiver = new TVReceiver(); TVOffCommand tvOffCommand = new TVOffCommand(tvReceiver); TVOnCommand tvOnCommand = new TVOnCommand(tvReceiver); //给我们的遥控器设置命令, 比如 no = 1 是电视机的开和关的操作 remoteController.setCommand(1, tvOnCommand, tvOffCommand); System.out.println("--------按下电视机的开按钮-----------"); remoteController.onButtonWasPushed(1); System.out.println("--------按下电视机的关按钮-----------"); remoteController.offButtonWasPushed(1); System.out.println("--------按下撤销按钮-----------"); remoteController.undoButtonWasPushed(); } }
【运行】
--------按下灯的开按钮----------- 电灯打开了.. --------按下灯的关按钮----------- 电灯关闭了.. --------按下撤销按钮----------- 电灯打开了.. =========使用遥控器操作电视机========== --------按下电视机的开按钮----------- 电视机打开了.. --------按下电视机的关按钮----------- 电视机关闭了.. --------按下撤销按钮----------- 电视机打开了.. Process finished with exit code 0
案例二
介绍
这段示例程序是一个画图软件,它的功能很简单,即用户拖动鼠标时程序会绘制出红色圆点,点击 clear 按钮后会清除所有的圆点。
实现
【命令接口】
package com.atguigu.command.Sample.command; public interface Command { /** * 执行 */ public abstract void execute(); }
【历史命令集:由多条命令整合成的命令】
如果保存这个实例,就可以永久保存历史数据
package com.atguigu.command.Sample.command; import java.util.Iterator; import java.util.Stack; /** * 由多条命令整合成的命令 */ public class MacroCommand implements Command { /** * 存储命令的栈 */ private Stack commands = new Stack(); /** * 执行 */ public void execute() { // 一次性执行一系列命令 Iterator it = commands.iterator(); while (it.hasNext()) { ((Command) it.next()).execute(); } } /** * 添加命令 * * @param cmd */ public void append(Command cmd) { if (cmd != this) { // 判断不是自己再添加进去,不然会死循环 commands.push(cmd); } } /** * 删除最后一条命令 */ public void undo() { if (!commands.empty()) { // 取出最后添加到栈的命令 commands.pop(); } } /** * 删除所有命令 */ public void clear() { commands.clear(); } }
【绘制点命令】
package com.atguigu.command.Sample.drawer; import com.atguigu.command.Sample.command.Command; import java.awt.*; public class DrawCommand implements Command { /** * 绘制对象 */ protected Drawable drawable; /** * 绘制位置 Point 是 java.awt 包的类,含有 (x,y) 坐标 */ private Point position; /** * 构造函数 * * @param drawable * @param position */ public DrawCommand(Drawable drawable, Point position) { this.drawable = drawable; this.position = position; } /** * 执行 */ public void execute() { drawable.draw(position.x, position.y); } }
【绘制对象接口】
package com.atguigu.command.Sample.drawer; public interface Drawable { public abstract void draw(int x, int y); }
【绘图类】
package com.atguigu.command.Sample.drawer; import com.atguigu.command.Sample.command.MacroCommand; import java.awt.*; /** * 继承Canvas */ public class DrawCanvas extends Canvas implements Drawable { /** * 颜色 */ private Color color = Color.red; /** * 要绘制的圆点的半径 */ private int radius = 6; /** * 命令的历史记录 */ private MacroCommand history; /** * 构造函数 * * @param width * @param height * @param history */ public DrawCanvas(int width, int height, MacroCommand history) { // 设置画布尺寸 setSize(width, height); // 设置画布颜色 setBackground(Color.white); this.history = history; } /** * 重新全部绘制 * @param g the specified Graphics context */ public void paint(Graphics g) { history.execute(); } /** * 绘制 * @param x * @param y */ public void draw(int x, int y) { Graphics g = getGraphics(); // 设置笔画颜色 g.setColor(color); // 绘制圆点 g.fillOval(x - radius, y - radius, radius * 2, radius * 2); } }
【主类】
package com.atguigu.command.Sample; import com.atguigu.command.Sample.command.*; import com.atguigu.command.Sample.drawer.*; import javax.swing.*; import java.awt.event.*; public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener { /** * 存储绘制的历史记录 */ private MacroCommand history = new MacroCommand(); /** * 绘制区域 */ private DrawCanvas canvas = new DrawCanvas(400, 400, history); /** * 创建删除按钮 */ private JButton clearButton = new JButton("clear"); /** * 构造函数 * @param title */ public Main(String title) { super(title); this.addWindowListener(this); // 添加鼠标移动事件 canvas.addMouseMotionListener(this); // 添加鼠标点击事件 clearButton.addActionListener(this); /// 创建布局 // 创建横向的盒子来存放按钮 Box buttonBox = new Box(BoxLayout.X_AXIS); buttonBox.add(clearButton); // 创建纵向的盒子来存放按钮盒子和画布 Box mainBox = new Box(BoxLayout.Y_AXIS); mainBox.add(buttonBox); mainBox.add(canvas); getContentPane().add(mainBox); pack(); show(); } /** * ActionListener接口中的方法 * @param e */ public void actionPerformed(ActionEvent e) { if (e.getSource() == clearButton) { // 清空历史命令 history.clear(); // 清空画布 canvas.repaint(); } } public void mouseMoved(MouseEvent e) { } /** * MouseMotionListener接口中的方法 * @param e */ public void mouseDragged(MouseEvent e) { // 鼠标位置一改变,就创建绘制点命令放到命令集合中 Command cmd = new DrawCommand(canvas, e.getPoint()); // 执行绘制 cmd.execute(); history.append(cmd); } /** * WindowListener接口中的方法 * @param e */ public void windowClosing(WindowEvent e) { System.exit(0); } public void windowActivated(WindowEvent e) {} public void windowClosed(WindowEvent e) {} public void windowDeactivated(WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowIconified(WindowEvent e) {} public void windowOpened(WindowEvent e) {} public static void main(String[] args) { new Main("Command Pattern Sample"); } }
【运行】
拓展
如何示例程序中增加“设置颜色”的功能。
【颜色命令】
package com.atguigu.command.A1.drawer; import com.atguigu.command.A1.command.Command; import java.awt.*; public class ColorCommand implements Command { /** * 绘制对象 */ protected Drawable drawable; /** * 颜色 */ private Color color; /** * 构造函数 * * @param drawable * @param color */ public ColorCommand(Drawable drawable, Color color) { this.drawable = drawable; this.color = color; } /** * 执行 * 设置画笔的颜色 */ public void execute() { drawable.setColor(color); } }
【主类】
package com.atguigu.command.A1; import com.atguigu.command.A1.command.Command; import com.atguigu.command.A1.command.MacroCommand; import com.atguigu.command.A1.drawer.ColorCommand; import com.atguigu.command.A1.drawer.DrawCanvas; import com.atguigu.command.A1.drawer.DrawCommand; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener { private MacroCommand history = new MacroCommand(); private DrawCanvas canvas = new DrawCanvas(400, 400, history); private JButton clearButton = new JButton("clear"); /** * 红色按钮 */ private JButton redButton = new JButton("red"); /** * 绿色按钮 */ private JButton greenButton = new JButton("green"); /** * 蓝色按钮 */ private JButton blueButton = new JButton("blue"); /** * 构造函数 * * @param title */ public Main(String title) { super(title); this.addWindowListener(this); canvas.addMouseMotionListener(this); clearButton.addActionListener(this); redButton.addActionListener(this); greenButton.addActionListener(this); blueButton.addActionListener(this); Box buttonBox = new Box(BoxLayout.X_AXIS); buttonBox.add(clearButton); buttonBox.add(redButton); buttonBox.add(greenButton); buttonBox.add(blueButton); Box mainBox = new Box(BoxLayout.Y_AXIS); mainBox.add(buttonBox); mainBox.add(canvas); getContentPane().add(mainBox); pack(); show(); } /** * ActionListener接口中的方法 * 点击不同颜色的按钮,设置不同颜色画笔 * * @param e */ public void actionPerformed(ActionEvent e) { if (e.getSource() == clearButton) { history.clear(); canvas.init(); canvas.repaint(); } else if (e.getSource() == redButton) { Command cmd = new ColorCommand(canvas, Color.red); history.append(cmd); cmd.execute(); } else if (e.getSource() == greenButton) { Command cmd = new ColorCommand(canvas, Color.green); history.append(cmd); cmd.execute(); } else if (e.getSource() == blueButton) { Command cmd = new ColorCommand(canvas, Color.blue); history.append(cmd); cmd.execute(); } } /** * MouseMotionListener接口中的方法 * * @param e */ public void mouseMoved(MouseEvent e) { } public void mouseDragged(MouseEvent e) { Command cmd = new DrawCommand(canvas, e.getPoint()); history.append(cmd); cmd.execute(); } /** * WindowListener接口中的方法 * * @param e */ public void windowClosing(WindowEvent e) { System.exit(0); } public void windowActivated(WindowEvent e) { } public void windowClosed(WindowEvent e) { } public void windowDeactivated(WindowEvent e) { } public void windowDeiconified(WindowEvent e) { } public void windowIconified(WindowEvent e) { } public void windowOpened(WindowEvent e) { } public static void main(String[] args) { new Main("Command Pattern Sample"); } }
【运行】
命令模式在JdbcTemplate源码中的应用
应用模式和标准的命令模式有点区别,但是精髓是差不多的
还有其他的命令执行者
总结
【优点】
- 将
发起请求的对象
与执行请求的对象
解耦。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的。命令对象(就是具体的命令)会负责让接收者执行调用者请求的动作,也就是说:”请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用 - 命令模式容易设计一个命令队列。只要把命令对象放到队列,就可以多线程地执行命令。不仅如此,还容易将命令执行的历史纪录保存起来
- 命令模式容易实现对请求的撤销和重做
- 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一个按键都要判空,这给我们编码带来一定的麻烦(声明命令数组时,首先将数组的元素全部初始化为空命令)
【缺点】
- 可能导致某些系统有过多的具体命令类,增加了系统的复杂度
文章说明
- 本文章为本人学习尚硅谷的学习笔记,文章中大部分内容来源于尚硅谷视频(点击学习尚硅谷相关课程),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对尚硅谷的优质课程表示感谢。
- 本人还同步阅读《图解设计模式》书籍(图解设计模式/(日)结城浩著;杨文轩译–北京:人民邮电出版社,2017.1),进而综合两者的内容,让知识点更加全面