0%

设计模式之Command模式

命令也是类

前言

命令也是类,将命令作为一个类来保存,当要使用的时候可以直接拿来使用,比如脚本语言写出的脚本,只需要一个命令就能执行得到我们想要的需要操作很长时间才能得到的结果。这是一个非常有意思的模式,将操作的步骤保存下来,本例之中我们使用java自带的GUI来画图,然后将画图的过程(在哪个地方画了什么东西)保存下来,可以把每一次我们的操作作为一个命令,其实就是<使用什么画布,画点的坐标>,将这个命令对应的对象保存到所有命令对象的集合之中去,这样命令集合就记录下来了每一个命令,如果要显示画的内容的时候,直接将这些命令组合读取出来在进行一次重画即可。通过这种模式保存下来已经执行的步骤,通过重画再复述出来,是一种非常重要的开发理念,在需要保存历史纪录并恢复的场合是非常有用的。

示例程序

  1. Command接口
    1
    2
    3
    4
    5
    package com.edu.tju.GOF.Command;

    public interface Command {
    public abstract void execute();
    }
  2. DrawCommand类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.edu.tju.GOF.Command;

    import java.awt.Point;

    public class DrawCommand implements Command {
    // 绘制对象
    protected Drawable drawable;
    // 绘制位置
    private Point position;
    // 构造函数
    public DrawCommand(Drawable drawable, Point position) {
    this.drawable = drawable;
    this.position = position;
    }
    // 执行
    public void execute() {
    drawable.draw(position.x, position.y);
    }
    }
  3. MacroCommand类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.edu.tju.GOF.Command;


    import java.util.Stack;
    import java.util.Iterator;

    public class MacroCommand implements Command {
    // 命令的集合
    private Stack commands = new Stack();
    // 执行
    public void execute() {
    Iterator it = commands.iterator();
    while (it.hasNext()) {
    ((Command)it.next()).execute();
    }
    }
    // 添加命令
    public void append(Command cmd) {
    if (cmd != this) {
    commands.push(cmd);
    }
    }
    // 删除最后一条命令
    public void undo() {
    if (!commands.empty()) {
    commands.pop();
    }
    }
    // 删除所有命令
    public void clear() {
    commands.clear();
    }
    }
  4. Drawable接口
    1
    2
    3
    4
    5
    package com.edu.tju.GOF.Command;

    public interface Drawable {
    public abstract void draw(int x, int y);
    }
  5. DrawCanvas 类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.edu.tju.GOF.Command;
    import java.util.*;
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;

    public class DrawCanvas extends Canvas implements Drawable {
    // 颜色
    private Color color = Color.red;
    // 要绘制的圆点的半径
    private int radius = 6;
    // 命令的历史记录
    private MacroCommand history;
    // 构造函数
    public DrawCanvas(int width, int height, MacroCommand history) {
    setSize(width, height);
    setBackground(Color.white);
    this.history = history;
    }
    // 重新全部绘制
    public void paint(Graphics g) {
    System.out.println("执行一次刷新!"+System.currentTimeMillis());
    history.execute();
    }
    // 绘制
    public void draw(int x, int y) {
    Random random = new Random();
    Graphics g = getGraphics();
    g.setColor((random.nextBoolean())? Color.yellow : Color.MAGENTA);
    g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
    }
    }
  6. Main类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    package com.edu.tju.GOF.Command;

    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;

    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("清除");
    private JButton btnRePaint=new JButton("重现");
    // 构造函数
    public Main(String title) {
    super(title);

    this.addWindowListener(this);
    canvas.addMouseMotionListener(this);
    btnRePaint.addActionListener(this);
    clearButton.addActionListener(this);

    Box buttonBox = new Box(BoxLayout.X_AXIS);
    buttonBox.add(clearButton);
    buttonBox.add(btnRePaint);
    Box mainBox = new Box(BoxLayout.Y_AXIS);
    mainBox.add(buttonBox);
    mainBox.add(canvas);
    getContentPane().add(mainBox);

    pack();
    show();
    }

    // ActionListener接口中的方法
    public void actionPerformed(ActionEvent e) {
    if (e.getSource() == clearButton) {
    history.clear();
    canvas.repaint();
    }else if (e.getSource()==btnRePaint) {
    canvas.repaint();
    }
    }

    // MouseMotionListener接口中的方法
    public void mouseMoved(MouseEvent e) {
    }
    public void mouseDragged(MouseEvent e) {
    Command cmd = new DrawCommand(canvas, e.getPoint());
    history.append(cmd);
    cmd.execute();
    }

    // WindowListener接口中的方法
    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");
    }
    }
    实现结果自行运行吧;

分析过程

运行后,我们可以看到保存了的命令九一个个的再次执行了一遍,我们分析一下程序运行的过程

  1. 开始执行初始化界面,然后显示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public Main(String title){
    super(title);

    this.addWindowListener(this);
    canvas.addMouseMotionListener(this);
    btnClear.addActionListener(this);
    btnRePaint.addActionListener(this);

    Box btnBox=new Box(BoxLayout.X_AXIS);
    btnBox.add(btnClear);
    btnBox.add(btnRePaint);

    Box mainBox=new Box(BoxLayout.Y_AXIS);
    mainBox.add(btnBox);
    mainBox.add(canvas);

    getContentPane().add(mainBox);

    pack();
    show();
    }
  2. 然后等待用户的操作,当监听到用户在界面上拖动鼠标的时候,执行:
    1
    2
    3
    4
    5
    6
    @Override
    public void mouseDragged(MouseEvent e) {
    Command cmd=new DrawCommand(canvas,e.getPoint());
    history.append(cmd);
    cmd.execute();
    }
  3. 创建一个命令对象,然后记录进命令堆栈之中之后我们跟踪cmd.execute();
    1
    2
    3
    public interface Command {
    public abstract void execute();
    }
  4. 这里就看到我们的面向抽象编程的好处了,根本不需要知道是谁执行了我们的命令,在命令的时候自然知道了,那就是new DrawCommand(canvas,e.getPoint());
    1
    2
    3
    4
    5
    6
    7
    8
    public class DrawCommand implements Command {

    。。。
    public void execute() {
    drawable.draw(position.x, position.y);
    }

    }
  5. 我们继续跟踪:
    1
    2
    3
    4
    5
    public interface Drawable {

    public abstract void draw(int x,int y);

    }
  6. 同理,谁实现了Drawable ,并被传递进去了,Command cmd=new DrawCommand(canvas,e.getPoint());
    1
    2
    3
    4
    5
    6
    7
    public void draw(int x, int y) {
    Random random = new Random();

    Graphics g = getGraphics();
    g.setColor((random.nextBoolean())? Color.yellow : Color.MAGENTA);
    g.fillOval(x-radius, y-radius, radius*2, radius*2);
    }
    因此执行我们的程序,画了一个点。之后我们的鼠标不断拖动着,这个流程就一直执行着,直到我们停止为止。

之后我们分析重画方法:
当用户点击按钮:

1
2
3
4
5
6
7
8
9
@Override
public void actionPerformed(ActionEvent e) {
if(e.getSource()==btnClear){
history.clear();
canvas.repaint();
}else if(e.getSource()==btnRePaint){
canvas.repaint();
}
}

调用 canvas.repaint();方法,这是Canvas自动实现的,我们不必深究,只需要知道这个函数之中会调用,我们的继承了Canvas并且重写的方法:

1
2
3
4
public void paint(Graphics g) {
System.out.println("执行一次刷新!"+System.currentTimeMillis());
history.execute();
}

跟踪: history.execute();

1
2
3
4
5
6
7
public void execute() {
Iterator it = commands.iterator();
while(it.hasNext()){
Command command=(Command)it.next();
command.execute();
}
}

可以看到将保存的命令一个个都拿出来,重新走了一遍我们上面的command.execute();所走的流程,这就是命令模式,现在很清晰了。

总结

对于命令模式,在本例之中使用了Composite模式,迭代器等模式作为辅助,另外在生成对象的时候还可能使用原型模式,在保存命令的时候还可能使用备忘录模式。本例是一个很好的例子,从本质上说明了命令模式就是将命令抽象成一个类,通过保存接收者的引用,在后期还可以让接收者去执行,同样的使用了组合模式将这些对象一个个的保存了下来,然后一步步的调用单个命令的执行方法,该执行方法通知命令的接收者去再次执行命令,这种方式特别的方便,因为我们保存的是用户的操作,能够一直记录下来,甚至可以保存到文件之中以后可以恢复,由此可以看到命令模式的强大。

角色

  1. 发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。

  2. 命令 (Command) 接口通常仅声明一个执行命令的方法。

  3. 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。
    接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。

  4. 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。

  5. 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。

实现方式

  1. 声明仅有一个执行方法的命令接口。

  2. 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。

  3. 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。

  4. 修改发送者使其执行命令, 而非直接将请求发送给接收者。

  5. 客户端必须按照以下顺序来初始化对象:

    • 创建接收者。
    • 创建命令, 如有需要可将其关联至接收者。
    • 创建发送者并将其与特定命令关联。

精心出精品
参考文献
Command Pattern
<<图解设计模式>>

Thank you for your support