一个类在进行工作时会调用自己或者是其他类的方法,虽然调用结果会反映在对象的状态中,但并不会留下工作的历史记录。
这时,如果我们有一个类,用来表示“请进行这项工作”的“命令”就会方便很多。每一项想做的工作就不再是“方法的调用”这种动态处理了,而是一个表示命令的类的实例,即可以用“物”表示。要想管理工作的历史记录,只需管理这些实例的集合即可,而且还可以随时再次执行过去的命令,或是将多个过去的命令整合为一个新命令并执行。
Command有时也被称为时间。当发生点击鼠标、按下键盘按键等事件时,我们可以先将这些事件作成实例,然后按照发生顺序放入队列中。接着,再依次去处理它们。
示例程序要实现的功能是,用户拖动鼠标时程序会绘制出红色圆点,点击clear按钮后会清除所有的圆点。用户每拖动一次鼠标,应用程序都会为“在这个位置画一个点”这条命令生成一个DrawCommand类的实例。只要保存了这条命令,以后有需要时就可以重新绘制。
1 package bigjunoba.bjtu.command; 2 3 public interface Command { 4 public abstract void execute(); 5 }
Command接口是表示“命令”的接口。作用就是“执行”什么东西。
package bigjunoba.bjtu.command; import java.util.Stack; import java.util.Iterator; public class MacroCommand implements Command { // 命令的集合 private Stack<Command> commands = new Stack<Command>(); // 执行 public void execute() { Iterator<Command> it = commands.iterator(); while (it.hasNext()) { ((Command)it.next()).execute(); } } // 添加命令,同时防止不小心将自己(this)添加进去。 public void append(Command cmd) { if (cmd != this) { commands.push(cmd); } } // 删除push方法添加的最后一条命令 public void undo() { if (!commands.empty()) { commands.pop(); } } // 删除所有命令 public void clear() { commands.clear(); } }
MacroCommand类表示“由多条命令整合成的命令”。该类实现了Command接口。commands字段是Stack类型的,它是保存了多个Command(即实现了Command接口的实例)的集合。
1 package bigjunoba.bjtu.drawer; 2 3 import bigjunoba.bjtu.command.*; 4 import java.awt.Point; 5 6 public class DrawCommand implements Command { 7 // 绘制对象 8 protected Drawable drawable; 9 // 绘制位置 10 private Point position; 11 // 构造函数 12 public DrawCommand(Drawable drawable, Point position) { 13 this.drawable = drawable; 14 this.position = position; 15 } 16 // 执行 17 public void execute() { 18 drawable.draw(position.x, position.y); 19 } 20 }
DrawCommand类实现了Command接口,表示“绘制一个点的命令”。构造函数的作用是生成“在这个位置绘制点”的命令。execute方法的作用是执行命令。
1 package bigjunoba.bjtu.drawer; 2 3 public interface Drawable { 4 public abstract void draw(int x, int y); 5 }
Drawable接口是表示“绘制对象”的接口。draw方法是用于绘制的方法。
1 package bigjunoba.bjtu.drawer; 2 3 import bigjunoba.bjtu.command.*; 4 import java.util.*; 5 import java.awt.*; 6 import java.awt.event.*; 7 import javax.swing.*; 8 9 public class DrawCanvas extends Canvas implements Drawable { 10 // 颜色 11 private Color color = Color.red; 12 // 要绘制的圆点的半径 13 private int radius = 6; 14 // 命令的历史记录 15 private MacroCommand history; 16 // 构造函数 17 public DrawCanvas(int width, int height, MacroCommand history) { 18 setSize(width, height); 19 setBackground(Color.white); 20 this.history = history; 21 } 22 // 重新全部绘制 23 public void paint(Graphics g) { 24 history.execute(); 25 } 26 // 绘制 27 public void draw(int x, int y) { 28 Graphics g = getGraphics(); 29 g.setColor(color); 30 g.fillOval(x - radius, y - radius, radius * 2, radius * 2); //画圆点 31 } 32 }
history字段中保存的是DrawCanvas类自己应当执行的绘制命令的集合。该字段是Command.MacroCommand类型的。当需要重新绘制DrawCanvas是,java处理会调用paint方法。它所做的事情仅仅是调用了history.excute方法。这样,记录在history中的所有历史命令都会被重新执行一遍。
1 package bigjunoba.bjtu.test; 2 3 import bigjunoba.bjtu.command.*; 4 import bigjunoba.bjtu.drawer.*; 5 6 import java.awt.*; 7 import java.awt.event.*; 8 import javax.swing.*; 9 10 public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener { 11 // 绘制的历史记录 12 private MacroCommand history = new MacroCommand(); 13 // 绘制区域 14 private DrawCanvas canvas = new DrawCanvas(400, 400, history); 15 // 删除按钮 16 private JButton clearButton = new JButton("clear"); 17 18 // 构造函数 19 public Main(String title) { 20 super(title); 21 22 this.addWindowListener(this); 23 canvas.addMouseMotionListener(this); 24 clearButton.addActionListener(this); 25 26 Box buttonBox = new Box(BoxLayout.X_AXIS); 27 buttonBox.add(clearButton); 28 Box mainBox = new Box(BoxLayout.Y_AXIS); 29 mainBox.add(buttonBox); 30 mainBox.add(canvas); 31 getContentPane().add(mainBox); 32 33 pack(); 34 show(); 35 } 36 37 // ActionListener接口中的方法 38 public void actionPerformed(ActionEvent e) { 39 if (e.getSource() == clearButton) { 40 history.clear(); 41 canvas.repaint(); 42 } 43 } 44 45 // MouseMotionListener接口中的方法 46 public void mouseMoved(MouseEvent e) { 47 } 48 public void mouseDragged(MouseEvent e) { 49 Command cmd = new DrawCommand(canvas, e.getPoint()); 50 history.append(cmd); 51 cmd.execute(); 52 } 53 54 // WindowListener接口中的方法 55 public void windowClosing(WindowEvent e) { 56 System.exit(0); 57 } 58 public void windowActivated(WindowEvent e) {} 59 public void windowClosed(WindowEvent e) {} 60 public void windowDeactivated(WindowEvent e) {} 61 public void windowDeiconified(WindowEvent e) {} 62 public void windowIconified(WindowEvent e) {} 63 public void windowOpened(WindowEvent e) {} 64 65 public static void main(String[] args) { 66 new Main("Command Pattern Sample"); 67 } 68 }
Main类实现了mouseDragged方法,当鼠标被拖动时,会生成一条“在这个位置画点”的命令,Command cmd = new DrawCommand(canvas, e.getPoint());该命令会先被添加至绘制历史记录中,history.append(cmd);然后立即执行,cmd.execute();。
测试结果如上图所示。
示例程序的时序图。
Command模式的类图。