设计之禅——我只要结果(命令模式)

时间:2022-07-24
本文章向大家介绍设计之禅——我只要结果(命令模式),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

生活中我们会接收到各种各样的命令,也会发出各种各样的命令,虽然命令的事情都各不相同,但是都具有一个共同的特质,那就是对于发出命令的人而言他不需要知道你是怎么实现的,他只要得到结果就行了,相信大家都会常听到BOSS这样对自己说(当老板真好啊!)。那么,我们在写程序时,如果能将请求的调用和请求的执行解耦,对于客户端而言就不用再关心后台复杂的实现逻辑了,因此,命令模式也就应运而生。

概述

命令模式将“请求”封装成对象,以便使用不同的请求、队列或日志来参数化其它对象。命令模式也支持可以撤销的操作。

这句话描述得比较抽象,先来看看下面这张类图:

可以看到命令模式也就包含了四大角色:客户端、请求者(调用者)、命令对象以及接收者,其中请求者和接收者是完全松耦合的,而命令对象是通过组合保存在请求者中,这样,如果需要扩展新的命令就只需要创建新的接收者和命令对象,并让命令对象实现Command接口,最后由客户自己设置保存到请求者中就行了。

Coding

命令模式通过命令对象将请求和请求的执行解耦,那这个过程如何通过代码来实现呢?这里通过《Head First设计模式》中的例子来说明。

操控一切的遥控器

假设有一个多功能遥控器,上面共有十对按钮(开、关),用户可以自行设定每个按钮具体控制何种家电(点灯、冰箱、电视、风扇等)开关。分析一下这个例子中各个物件和上述类图的对应关系是如何的呢?毋庸置疑,遥控器肯定是调用者,各种家电也就是具体的接收者,那么当按下遥控器上的按钮时对应的家电就要执行对应的动作,这个请求当然可以直接发送给接收者,也就是家电,但是这样的话相当于这个遥控器所能控制的对象就是固定的了,没有办法能够随心所欲的更换,所以我们才需要将此封装为一个命令对象,如此遥控器就不需要关心具体执行该请求的对象究竟是什么了。下面看看代码: 首先是家电族,这里为了简单,它们都只具有简单的开和关方法:

public class Light {

    public void on() {
        System.out.println("Light on!");
    }

    public void off() {
        System.out.println("Light off!");
    }

}

那么如何实现命令对象呢?它具有什么样的特点?首先对于遥控器而言,它调用的也就是命令对象的方法,且能动态的改变,因此所有的命令对象都要实现自一个接口(针对接口编程,而不是实现);其次对于命令对象而言,它们需要去调用接收者的方法来执行请求,所以它们有一个共同的执行方法(当然还可以加入撤销前一个动作的方法,这里先简单一点)。代码如下:

public interface Command {

   void excute();

}

public class LightOnCommand implements Command {

    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void excute() {
        light.on();
    }
}

public class LightOffCommand implements Command {

    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void excute() {
        light.off();
    }
}

看到这里相信对于其他家电的命令对象的实现应该难不倒你了,我们再来看看遥控器的实现:

public class RemoteController {

    private Command[] onCommands;
    private Command[] offCommands;

    public RemoteController() {
        this.onCommands = new Command[10];
        this.offCommands = new Command[10];

        NoCommand noCommand = new NoCommand();
        for (int i = 0; i < onCommands.length; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }

    public void setCommands(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void pressOnButton(int slot) {
        onCommands[slot].excute();
    }

    public void pressOffButton(int slot) {
        offCommands[slot].excute();
    }
    
}

首先使用两个数组分别存储开和关的命令对象,并在构造函数里面对其初始化设置默认的命令对象NoCommand,这个NoCommand是有什么用呢?看看它的实现:

public class NoCommand implements Command {

    @Override
    public void excute() {}
}

什么都不做,我没敲错,你也没看错,既然什么都不做,那这个对象有什么用呢?了解算法的同学应该都清楚,这就是“哨兵”的思想,简化代码,假如没有这个对象,那我们初始化了数组,里面每一个都是null对象,那我们每次调用方法的时候都要去判断一次,有了“哨兵”,我们也就简化了代码量和编程难度。 回到正题,我们可以通过setCommands方法设置需要控制的对象,然后按下按钮家电就能工作了,当需要增加或更改控制的对象,只需要再次调用setCommands方法,而且对于家电产家而言,当新产出一个家电时,只需要再提供一个命令对象就行了,不用去修改遥控器,这是多么棒的一个模式啊!

    public static void main(String[] args) {
        Light light = new Light();
        Command onCommand = new LightOnCommand(light);
        Command offCommand = new LightOffCommand(light);
        RemoteController remote = new RemoteController();
        remote.setCommands(0, onCommand, offCommand);

        IceBox iceBox = new IceBox();
        onCommand = new IceBoxOnCommand(iceBox);
        offCommand = new IceBoxOffCommand(iceBox);
        remote.setCommands(1, onCommand, offCommand);

        remote.pressOnButton(0);
        remote.pressOnButton(1);
    }

我要一个能撤销的遥控器

但是,刚刚我们提到了撤销,这也是一个很正常的需求,当客户打开点灯就后悔了,但是又觉得遥控器上按钮太多,记住每个家电关闭按钮对应的位置是非常麻烦的,希望能按下一个固定的按钮就能撤销前一个动作,这该如何实现?如果你认真思考了,应该不难想到我们首先应该在命令对象中新增一个undo方法,然后执行和excute方法相反的动作就行了:

public interface Command {

   void excute();

   void undo();

}

public class IceBoxOffCommand implements Command {

    private IceBox iceBox;

    public IceBoxOffCommand(IceBox iceBox) {
        this.iceBox = iceBox;
    }

    @Override
    public void excute() {
        iceBox.off();
    }

    @Override
    public void undo() {
        iceBox.on();
    }
}

public class IceBoxOnCommand implements Command {
    private IceBox iceBox;

    public IceBoxOnCommand(IceBox iceBox) {
        this.iceBox = iceBox;
    }

    @Override
    public void excute() {
        iceBox.on();
    }

    @Override
    public void undo() {
        iceBox.off();
    }
}

命令对象搞定了,但是遥控器如何去调用呢?也很简单,每当按下按钮时就把当前的命令对象保存下来,当按撤销按钮时再调用该对象的undo方法。

public class RemoteController {

    private Command[] onCommands;
    private Command[] offCommands;

    private Command preCommand;

    .......
    
    public void pressOnButton(int slot) {
        onCommands[slot].excute();
        // 注意此处需要将该对象保存
        preCommand = onCommands[slot];
    }

    public void pressOffButton(int slot) {
        offCommands[slot].excute();
        // 注意此处需要将该对象保存
        preCommand = offCommands[slot];
    }

    public void pressUndo() {
        preCommand.undo();
    }
    
}

而客户就更简单了,不用再记住按钮的位置,只需要按下撤销按钮:

remote.pressUndo();

Party模式

但是,还没完,客户的需求总是不断变更的,当客户下班回到家中已经非常累了,不想再一个个去按遥控器上的按钮来挨个打开电器工作,换作是自己也会觉得非常麻烦,要是只需要按下一个按钮所有家电都自动开始工作那该多好。 这个对于命令模式来讲也不难实现,只需要实现一个新的命令对象,依次调用其它命令对象的excute方法就实现了,撤销方法也是一样,这就像是开Party一样,所以称为Party模式。

public class PartyOnCommand implements Command {
    private Command[] commands;

    public PartyOnCommand(Command[] commands) {
        this.commands = commands;
    }
    
    @Override
    public void excute() {
        for (Command command : commands) {
            command.excute();
        }
    }

    @Override
    public void undo() {
        for (Command command : commands) {
            command.undo();
        }
    }
}

这里为了简单,就没有校验数组的安全性,实际编码中这里肯定是需要考虑的。

简化命令模式的创建

讲到这儿,篇幅已经很长了,但是还存在一个问题,对客户端而言,命令模式中对象的创建也是非常繁复的,一个好的应用暴露给客户的接口一定是简单的,基于这一原则我们应该极力减少客户端的工作,那应该如何做呢?在我的上一篇文章《装饰者模式》中也讲过利用工厂模式和生成器模式来简化创建过程,命令模式也不例外,这里就不详细阐述了,原理都是一样的,感兴趣的可以看我之前的文章再来自己实现一下。

总结

最后总结一下:

  • 命令模式将请求的调用和执行完全的解耦,使客户端无需关注请求具体的执行者。
  • 命令模式也支持撤销的动作,如果需要多级撤销,那么只需要用栈来保存之前的命令对象。
  • 命令模式支持宏命令操作,可以省却一系列繁复的操作过程。
  • 命令模式复杂的创建可以结合工厂模式和生成器模式来简化创建过程。