设计模式之命令模式

定义

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
可以类比现实生活中我们使用电视遥控器开关机,或者去餐厅吃饭向服务员点餐的过程,用户不需要知道点的菜是具体哪个厨师做的,
厨师也不需要知道这个菜是哪个用户点的,命令发送者和执行者之间解耦。

结构

设计模式之命令模式
  • Command,命令接口,定义执行的方法。
  • ConcreteCommand,具体命令,拥有接收者对象,调用接收者的功能来完成命令要执行的操作。
  • Receiver,接收者,真正执行命令的对象。
  • Invoker,调用者,是请求的发送者,通常会拥有很多命令对象,并通过访问命令对象来执行相关请求。
  • Client,客户端,也可以称为装配者,组装命令对象和接收者,并触发执行。

以用户餐厅点餐为例,用户就是客户端,服务员就是调用者,点餐就是命令,厨师就是接收者。

简单实现

命令接口

public interface Command {

  void execute();
}

具体命令

public class ConcreteCommand implements Command {

  private Receiver receiver;

  public ConcreteCommand(Receiver receiver) {
    this.receiver = receiver;
  }

  @Override
  public void execute() {
    receiver.action();
  }
}

接收者

/**
 * 命令接收者
 */
public class Receiver {

  public void action() {
    System.out.println("Receiver the command and execute");
  }
}

调用者

public class Invoker {

  private Command command;

  public Invoker(Command command) {
    this.command = command;
  }

  public void runCommand() {
    command.execute();
  }
}

客户端

public class Client {

  public static void main(String[] args) {
    //组装命令和执行者
    Receiver receiver = new Receiver();
    Command command = new ConcreteCommand(receiver);
    Invoker invoker = new Invoker(command);
    invoker.runCommand();
  }

}

命令的撤销和恢复

命令模式的关键之处就是将请求封装成对象,也就是命令对象,并定义了统一的执行操作的接口,这个命令对象可以被存储,转发,记录,处理,撤销等,
整个命令模式都是围绕这个对象在进行。这里我们模拟实现一个支持撤销和恢复的简单文本编辑器,类似EditPlus或Word的撤销和恢复功能。

设计模式之命令模式

有两种思路来实现这种撤销功能

  • 一种是补偿式,又称反操作式,比如被撤销的操作是添加,撤销就是删除。
  • 另一种是存储恢复式,将操作前的状态记录下来,撤销的时候直接恢复回去就可以了。关于这种方式,我们学习到备忘录模式时再详解。

这里我们使用第一种方式实现撤销功能。

/**
 * 命令接收者
 */
public class Receiver {

  /**
   * 文本内容
   */
  private String textContent = "";

  /**
   * 文本追加
   */
  public void append(String target) {
    System.out.println("操作前内容:" + textContent);
    textContent = textContent.concat(target);
    System.out.println("操作后内容:" + textContent);
  }

  /**
   * 文本删除
   */
  public void remove(String target) {
    System.out.println("操作前内容:" + textContent);
    if (textContent.endsWith(target)) {
      textContent = textContent.substring(0, textContent.length() - target.length());
    }
    System.out.println("操作后内容:" + textContent);
  }
}

命令接口

public interface Command {

  /**
   * 命令执行
   */
  void execute();

  /**
   * 命令撤销
   */
  void undo();

}

文本追加命令

public class AppendCommand implements Command {

  private Receiver receiver;
  private String target;

  public AppendCommand(Receiver receiver, String target) {
    this.receiver = receiver;
    this.target = target;
  }

  @Override
  public void execute() {
    receiver.append(target);
  }

  @Override
  public void undo() {
    receiver.remove(target);
  }
}

文本删除命令

public class RemoveCommand implements Command {

  private Receiver receiver;
  private String target;

  public RemoveCommand(Receiver receiver, String target) {
    this.receiver = receiver;
    this.target = target;
  }

  @Override
  public void execute() {
    receiver.remove(target);
  }

  @Override
  public void undo() {
    receiver.append(target);
  }
}

文本编辑器(调用者),内部保存命令执行的历史记录(可撤销的列表)和撤销执行的历史记录(可恢复的列表),有撤销才会有恢复,
所以在执行撤销的时候向可恢复列表添加命令。撤销和恢复都是最后执行的要先撤销和恢复,所以使用栈存储。

import java.util.Stack;

/**
 * 文本编辑器,支持撤销和恢复
 */
public class TextEditor {

  private Command command;
  //操作的历史记录
  private Stack<Command> undoStack = new Stack<>();
  //撤销的历史记录
  private Stack<Command> redoStack = new Stack<>();

  public void setCommand(Command command) {
    this.command = command;
  }

  public void editText() {
    command.execute();
    undoStack.push(command);
  }

  /**
   * 撤销功能
   */
  public void undoText() {
    if (!undoStack.isEmpty()) {
      Command command = undoStack.pop();
      command.undo();
      redoStack.push(command);
    }
  }

  /**
   * 恢复功能
   */
  public void redoText() {
    if (!redoStack.isEmpty()) {
      Command command = redoStack.pop();
      command.execute();
    }
  }
}

客户端

public class Client {

  public static void main(String[] args) {
    //组装命令和执行者
    Receiver receiver = new Receiver();
    TextEditor textEditor = new TextEditor();
    //追加hello
    textEditor.setCommand(new AppendCommand(receiver, "hello"));
    textEditor.editText();
    //追加world
    textEditor.setCommand(new AppendCommand(receiver, "world"));
    textEditor.editText();
    //删除orld
    textEditor.setCommand(new RemoveCommand(receiver, "orld"));
    textEditor.editText();
    //撤销
    textEditor.undoText();
    //撤销
    textEditor.undoText();
    //恢复
    textEditor.redoText();
  }

}

输出为

操作前内容:
操作后内容:hello
操作前内容:hello
操作后内容:helloworld
操作前内容:helloworld
操作后内容:hellow
操作前内容:hellow
操作后内容:helloworld
操作前内容:helloworld
操作后内容:hello
操作前内容:hello
操作后内容:helloworld

结果符合预期

宏命令

简单来说就是包含多个命令的命令,在餐厅点餐中,用户所有点的菜组成的菜单就是一个宏命令,每一道菜都是一个命令,类似于组合模式,这里就不实现了。

简化的命令模式

在实际开发中,我们可以简化命令模式,将具体命令和接收者合二为一,调用者也不需要持有命令对象了,直接通过方法参数传递过来,
将具体命令类的实现改成匿名内部类实现,这个时候的命令模式基本上等同于java回调机制的实现。

public interface Command {

  void execute();
}
/**
 * 调用者
 */
public class Invoker {

  public void runCommand(Command command) {
    command.execute();
  }
}
public class Client {

  public static void main(String[] args) {
    Invoker invoker = new Invoker();
    invoker.runCommand(() -> {
      System.out.println("Concrete Command execute");
    });
  }

}

命令模式在JDK的实现

jdk中线程池ThreadPoolExecutor的实现

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Client {

  public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>());
    executor.execute(() -> {
      System.out.println("test execute");
    });
    executor.shutdown();
  }

}

ThreadPoolExecutor可以看做Invoker调用者,Runnable就是Command接口,将来不及执行的请求放到队列中,这也是命令模式的队列化。

总结

优点

  1. 更松散的耦合,命令的发起者和命令的执行者相互之间不需要知道对方。
  2. 更动态的控制,可以动态的对命令对象进行队列化,日志化,撤销等操作。
  3. 很容易组成复合命令,也就是宏命令,使系统操作更简单,功能更强大。
  4. 更好的扩展性,很容易增加新的命令对象。

缺点

  1. 可能会导致创建过多的具体命令类。

本质

命令模式的本质是封装请求,封装为请求就可以进行撤销,队列化,宏命令等处理了。

使用场景

  1. 如果需要在不同的时刻排队执行请求,可以使用命令模式,将请求封装成命令对象并队列化。
  2. 如果需要支持撤销操作。

参考

大战设计模式【8】—— 命令模式
设计模式的征途—19.命令(Command)模式
设计模式(十五)——命令模式(Spring框架的JdbcTemplate源码分析)
命令模式(详解版)
设计模式——命令模式
研磨设计模式-书籍

上一篇:在table中引用开关


下一篇:Nginx 缓存针对打开的文件句柄与原文件信息