《Head First 设计模式》读书笔记(6)——命令模式 Command

本文共5500词,阅读需要15分钟,本项目相关代码地址被托管在 gitee

命令模式是将具体的一个或一组操作封装成一个独立命令的设计方法。
所有命令都实现同一个Command接口和其中的execute方法,调用者找到需要处理的命令并调用execute方法执行。

前言

在本章,我们将把封装带到一个全新的境界:把方法调用(method invocation)封装起来。通过封装方法调用,我们可以把运算块包装成形。所以调用此运算的对象不需要关心事情是如何进行的。只要知道如何使用包装成形的方法来完成它就可以。通过封装方法调用,也可以做一些很聪明的事情,例如记录日志,或者重复使用这些封装来实现撤销(undo)。

栗子

设计一个家电自动化遥控器的API。这个遥控器具有七个可编程的插槽,每个插槽都有对应的开关按钮,这个遥控器还具备一个整体的撤销按钮。希望你能够创建一组控制遥控器的API,让每个插槽都能够控制一个或一组装置,能够控制目前的装置和任何未来可能出现的装置,这一点很重要。(这里有一组Java类,这些类时由多个厂商开发出来的,用来控制家电自动化装置,例如点灯,风扇,热水器,音响设备和其他类似的可控制装置。)

上面是很多的厂商类,看不清不要紧。你只要知道它很多,控制各种电器。
有许多类都具备on()和off()方法,除外还有其他的一些方法。
遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需要知道这些家电自动化的细节,或者如何打开热水器。
提示:命令模式可将“动作的请求者”从“动作的执行者”对象中解耦,在我们的题例中,请求者是遥控器,而执行者对象就是厂商类。利用命令对象,把请求(打开点灯)封装成一个特定对象(客厅点灯对象),如果对每个按钮都存一个命令对象,那么当按钮被按下的时候,就可以请命令对象做相关的工作,遥控器并不需要知道工作内容是什么,只要有个命令对象能和正确的对象沟通,把事情做好久可以了。

用餐厅来更好的理解命令模式

餐厅解析:研究顾客,女招待,订单,以及快餐厨师之间的交互。

1:你,也就是顾客,把订单交个女招待。
2:女招待拿了订单放在订单柜台,然后喊了一声“订单来了”。
3:快餐厨师根据订单准备餐点。

让我们更详细地研究这个交互

顾客知道自己要吃什么,并创建了一张订单createOrder(),订单包含一个订单表格,顾客订购的餐点项目写在上面。女招待拿走了订单tekeOrder(),放在订单柜台,然后调用orderUp()方法,通知厨师准备餐点。订单上有所有准备餐点是只是,知道厨师用类似makeBurger()这样的方法来烹饪。开餐厨师准备餐点。output()。

餐厅的角色和职责

一张订单封装了准备餐点的请求。
把订单想象成一个用来请求准备餐点的对象,和一般的对象一样,订单对象可以被传递:从女招待传递到订单柜台,或者从女招待传递到阶梯下一班的女找到,订单的接口只包含一个方法就是orderUp()。这个方法封装了准备从哪点所需的动作。订单内有一个到“需要进行准备工作的对象”(也就是厨师)的引用。这一切都被疯转起来,所以女招待不需要知道订单上有什么,也不需要知道是谁来准备餐点。

女招待的工作是接收订单,然后调用订单的orderUp()方法。

女招待其实不必担心订单的内容是什么,或者由谁来准备餐点,她只需要知道,订单有一个orderUp()方法可以调用,这就够了。

快餐厨师具备准备餐点的知识。

快餐厨师是一种对象,他真正知道如何准备餐点,一旦女招待调用orderUp()方法,快餐厨师就接手,实现需要创建餐点的所有方法,女找到和厨师之间是彻底的解耦,女招待的订单封装了餐点的细节,厨师只要调用每个订单的方法即可。
把餐厅想着一种设计模式的一种模型,而这个模型允许将“发出请求的对象”和“接收与执行这些请求的队形”分隔开来,对于遥控器API,我们需要分隔开“发送请求的按钮”和“执行请求的厂商特定对象”。

从餐厅到命令模式--第一个命令对象:单按钮遥控器

首先让所有的命令都实现相同的包含一个方法的接口,在餐厅的栗子,是orderup(),实际上一般是execute()

public interface Command {
    public void execute();
}

电灯命令实现

public class LightOnCommand implements Command {

    Light light;

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

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

现在只有一个遥控器,假如就有一个按钮

public class SimpleRemoteControl {
    Command slot;

    public SimpleRemoteControl() {
    }

    public void setSlot(Command slot) {
        this.slot = slot;
    }

    public void buttonWasPressed(){
        slot.execute();
    }
}

遥控器测试:

public class RemoteControlTest {
    public static void main(String[] args) {
        SimpleRemoteControl remote=new SimpleRemoteControl();
        Light light=new Light();
        //GarageDoor garageDoor= new GarageDoor();
        LightOnCommand lighton= new LightOnCommand(light);
        //GarageDoorOpenCommand garageopen = new GarageDoorOpenCommand(garageDoor);
        //remote.setSlot(garageopen);
        //remote.buttonWasPressed();
        remote.setSlot(lighton);
        remote.buttonWasPressed();
    }
}

定义命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列、或者日志来参数化其他对象,命令模式也支持可撤销的操作。
我们知道一个命令对象通过在特定接受者上绑定一组动作来封装一个请求。要达到这个目的,命令对象将动作和接受者包进对象,这个对象就只会漏出一个execute()方法。
对比餐厅:command就是订单,一个人具体点的订单就像是一个具体的开灯命令,各种各样的按钮相当是各种各样的顾客,但是顾客本身定订单就像是告诉遥控器一些命令,遥控器就是女招待,订单告诉对应的厨师做相应的事情相当于把按钮按下的命令传过去各种对应的电器设备。
遥控器--invoker--女招待
各种电器设备--receiver--厨师
按钮--client--顾客
电器设备的指令--command--订单
按压对应指令的按钮--setCommand()--订单给女招待
按钮对应指令被电器设备响应--execute()--女招待打订单给厨师

命令模式类图

遥控器要升级

首先学习一种新的小模式,很常见——空对象模式

public class NoCommand implements Command {

    @Override
    public void execute() {

    }

    @Override
    public void undo() {

    }
}

没错NoCommand对象是一个空对象,当你不想返回一个又意义的对象时,空对象就很有用,客户也可以将处理null的责任转移给空对象,遥控器不可能一出厂就设置了有意义的命令对象,所以提供了NoCommand对象作为代用品,当调用它的execute()方法时,这种对象什么事情都不做
遥控器具体实现:

public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;

    public RemoteControl() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }

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

    public void onButtowWasPushed(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    public void offButtowWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }
    public void undoButtonWasPushed(){
        System.out.println("命令要被撤回");
        undoCommand.undo();
    }
    @Override
    public String toString() {
        StringBuffer stringBuff = new StringBuffer();
        stringBuff.append("\n------Remote Control------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[slot" + i + "]" + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "\n");
        }
        return stringBuff.toString();
    }
}

还是实现一些设备对象以及启动关闭的命令,具体可以查看github
然后遥控器具体被使用:

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        Light l = new Light("l1");
        LightOnCommand l1on = new LightOnCommand(l);
        LightOffCommand l1off = new LightOffCommand(l);
        remoteControl.setCommand(0, l1on, l1off);
        remoteControl.onButtowWasPushed(0);
        remoteControl.offButtowWasPushed(0);
        remoteControl.offButtowWasPushed(0);
        remoteControl.undoButtonWasPushed();
        CeilingFan ceilingFan = new CeilingFan("living room");
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(ceilingFan);
        CeilingFanOFFCommand ceilingFanOFFCommand = new CeilingFanOFFCommand(ceilingFan);
        remoteControl.setCommand(1, ceilingFanHighCommand, ceilingFanOFFCommand);
        remoteControl.onButtowWasPushed(1);
        remoteControl.offButtowWasPushed(1);
        remoteControl.undoButtonWasPushed();
    }
}

希望遥控器带有party模式——一键启动

宏命令--同时调用多个命令

public class MacroCommand implements Command {
    Command[] commands;

    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }

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

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

public class MacroCommandRemote {
    public static void main(String[] args) {
        Light light = new Light("living room");
        GarageDoor garageDoor = new GarageDoor();
        CeilingFan ceilingFan = new CeilingFan("sf");

        LightOnCommand lightOnCommand = new LightOnCommand(light);
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(ceilingFan);

        Command[] on = {lightOnCommand, ceilingFanHighCommand};
        MacroCommand macroCommand = new MacroCommand(on);
        RemoteControl remoteControl = new RemoteControl();
        remoteControl.setCommand(0, macroCommand, macroCommand);
        remoteControl.onButtowWasPushed(0);
    }
}

命令模式的更多用途

队列请求

命令可以将运算块打包(一个接受者和一组动作),然后将它传来传去,就像是一般的对象一样,现在,即使在命令对象被创建许久之后,运算依然可以被调用,事实上,它甚至可以在不同的线程中被调用,我们可以利用这样的特性衍生一些应用,例如:日程安排,线程池,工作队列等。
想象有一个工作队列:你在某一端添加命令,然后另一端则是线程,线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令……

日志请求

更多应用需要我们将所有的动作都记录在日志中,并能在系统死机后,重新调用这些动作恢复到之前的状态。当我们执行命令的时候,将历时记录存储在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的execute()方法。
比如基本所有系统支持撤销命令,word可以恢复刚刚临时保存的文件。

发表评论

电子邮件地址不会被公开。