设计模式学习笔记(9)命令

本文实例代码:https://github.com/JamesZBL/java_design_patterns

命令(Command)模式是一种数据驱动的设计模式,它属于行为型模式。请求被包装成一个命令对象,并由调用者传递给被调用对象。被调用对象寻找可以处理该命令的合适的处理对象,并把该命令传给这个处理对象,该处理对象执行命令。

命令模式中,命令的发出者和接收者是独立的,发出命令的职责和处理命令的职责被分别指派给不同的对象。命令模式解决了一般的调用过程中,“行为请求者”与“行为实现者” 之间的强耦合关系。比如某些场合中,需要对命令进行“撤销”、“重做”,亦或其他不得不以 “事务” 的形式实现的场合,命令发出者和命令实现者之间的解耦就显得至关重要了。

实例

命令模式最常见的场景就是字处理软件了,软件必须允许使用者进行重做或撤销的操作,仿佛没有这种特性的字处理软件几乎不会有人乐意使用。

首先不考虑使用命令模式,而是以传统的对象间调用来实现这种需求。假设我们现在需要对某个字依次进行如下操作:增大字号、设置字体颜色为红色、设置为加粗,那么对于命令发出者,要处理的逻辑就是这样的:


if(改变字号){

    if(增大字号){

        字号渲染器.增大字号();

    }else if(减小字号){

        字号渲染器.减小字号();

    }

}

if(改变颜色){

    颜色渲染器.改变颜色(颜色);

}

if(改变粗细){

    if(加粗){

        粗细渲染器.加粗();

    }else if(不加粗){

        粗细渲染器.变细();

    }

}

这种 “紧耦合” 的结构下,行为请求者与行为实现者之间的关系大概是这样的:

首先,所有命令都要有对应的处理者去执行,这就意味着行为请求者需要持有多个处理者的引用。这样,每修改或添加一个命令就必须修改行为请求者的逻辑,没办法实现对扩展开放。其次,如果要实现对命令的撤销或重做,那么本来就复杂的行为判断逻辑会变得愈发臃肿,对程序员来说这简直是一场灾难。

那么将行为请求者和行为实现者进行解耦后是怎样的呢?大概如图:

将行为请求者和行为实现者解耦,直观的改变就是:行为请求者不会直接调用行为实现者的具体方法,而是向行为实现者发出包含行为的具体命令,这个命令通常以对象的形式出现,他们之间传递的消息就是命令模式中的核心元素 —— “命令”,他在原始的行为请求者和行为实现者之间架起了一条高速公路,原来臃肿的逻辑判断代码得到了缩减,逻辑判断的职责被指派给了行为实现者,而行为请求者只需专注于发出正确的命令。

为了将使行为请求者和行为的具体实现解耦,应当将命令进行抽象。继续上面字处理软件的例子,用户发出的命令应当至少包含 “撤销” 和 “重做” 的功能,因此我们可以这样设计 “命令” 抽象类:

Command.java


public abstract class Command {

  /**

   * 执行

   */

  public abstract void execute();

  /**

   * 撤销

   */

  public abstract void undo();

  /**

   * 重做

   */

  public abstract void redo();

}

增大字体的命令:

Enlarge.java


public class Enlarge extends Command {

  private final AbstractFont font;

  private final Size oriSize;

  public Enlarge(AbstractFont font) {

    this.font = font;

    oriSize = font.getSize();

  }

  @Override

  public void execute() {

    font.setSize(Size.LARGE);

  }

  @Override

  public void undo() {

    font.setSize(oriSize);

  }

  @Override

  public void redo() {

    execute();

  }

}

使字体变红:

Rubify.java


public class Rubify extends Command {

  private final AbstractFont font;

  private final Color oriColor;

  public Rubify(AbstractFont font) {

    this.font = font;

    oriColor = font.getColor();

  }

  @Override

  public void execute() {

    font.setColor(Color.RED);

  }

  @Override

  public void undo() {

    font.setColor(oriColor);

  }

  @Override

  public void redo() {

    execute();

  }

}

这两种命令的共同点是都继承自命令抽象类,并持有一个行为实施对象的引用,也就是说,行为的具体实现过程被封装到了每个具体的命令中。

那么现在就需要一个行为请求者来发出命令了:

Typist.java


public final class Typist {

  private static final Logger LOGGER = LoggerFactory.getLogger(Typist.class);

  private Deque<Command> redoStack = new LinkedList<>();

  private Deque<Command> undoStack = new LinkedList<>();

  public void cast(Command command, AbstractFont font) {

    LOGGER.info("{}正在处理字体,命令为:{},处理的字体为:{}", this, command, font);

    command.execute();

    undoStack.offerLast(command);

  }

  public void undo() {

    if (!undoStack.isEmpty()) {

      Command previousCommand = undoStack.pollLast();

      redoStack.offerLast(previousCommand);

      LOGGER.info("{}正在进行撤销操作,命令为:{}", this, previousCommand);

      previousCommand.undo();

    } else {

      LOGGER.info("没有可以撤销的操作了");

    }

  }

  public void redo() {

    if (!redoStack.isEmpty()) {

      Command previousCommand = redoStack.pollLast();

      undoStack.offerLast(previousCommand);

      LOGGER.info("{}正在进行重做操作,命令为:{}", this, previousCommand);

      previousCommand.redo();

    } else {

      LOGGER.info("没有可以重做的操作了");

    }

  }

}

我们分别用两个队列来记录执行过的命令与撤销过的命令,“撤销” 操作对应 “已执行” 的操作, “重做” 操作对应 “已撤销” 的操作。每次执行命令,同时把命令放到 “撤销” 队列的队尾;同样的,每次执行 “撤销” 操作,也同时将命令放到 “重做” 队列的队尾。

为了方便 “观察” 字体的状态,我们将字体抽象成一个类:

AbstractFont.java


public abstract class AbstractFont {

  private static final Logger LOGGER = LoggerFactory.getLogger(AbstractFont.class);

  private Size size;

  private Color color;

  public Size getSize() {

    return size;

  }

  public void setSize(Size size) {

    this.size = size;

  }

  public Color getColor() {

    return color;

  }

  public void setColor(Color color) {

    this.color = color;

  }

  @Override

  public abstract String toString();

  /**

   * 打印当前状态

   */

  public void printStatus() {

    LOGGER.info("字体当前状态为:\t字体大小:{}\t颜色:{}", getSize(), getColor());

  }

}

拉以为用户来试试这个字处理软件吧,我们在旁边观察字体的状态:

App.java


public class Application {

  public static void main(String[] args) {

    Typist sizeTypist = new Typist();

    Typist colorTypist = new Typist();

    RegularScript font = new RegularScript();

    Command rubify = new Rubify(font);

    Command enlarge = new Enlarge(font);

    font.printStatus();

    // 设置字体颜色

    colorTypist.cast(rubify, font);

    font.printStatus();

    // 设置字体大小

    sizeTypist.cast(enlarge, font);

    font.printStatus();

    // 撤销颜色更改

    colorTypist.undo();

    font.printStatus();

    colorTypist.undo();

    // 撤销大小更改

    sizeTypist.undo();

    font.printStatus();

    sizeTypist.undo();

    // 字体颜色重做

    colorTypist.redo();

    font.printStatus();

    colorTypist.redo();

    // 大小重做

    sizeTypist.redo();

    font.printStatus();

    sizeTypist.redo();

  }

}

观察到的结果:


小字,黑色

小字,红色

大字,红色

大字,黑色

小字,黑色

大字,黑色

大字,红色

总结

命令模式的主要适用场景有:

  • 需要将行为请求者和行为实现者解耦,使得请求者和行为作用对象不直接交互

  • 需要在不同的时间发出请求、使请求排队以及执行队列中的请求

  • 需要支持命令的撤销操作和重做等类似操作

  • 需要将一组操作组合在一起,即实现宏命令

个人博客同步更新,获取更多技术分享请关注:郑保乐的博客

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容