《HF 设计模式》 C6 命令模式

通过命令模式,可以把方法封装起来,因此调用此方法的对象就不需要关心方法是如何完成的,只需要知道如何使用包装成形的方法来完成它就可以

1 模拟家电遥控器

1.1 家电遥控器的需求

在这里插入图片描述
给一个上述形状的遥控器,左边的插槽可以插拔家电控制块,右边的按键可以可以控制对应家电的开关,我们需要设计一组控制遥控器的API,让每个按键都能控制一个家电还有一个撤销功能,可以撤销最后一次进行的操作

厂商提供的各种家电API:
在这里插入图片描述

1.2 粗糙的实现遥控器

我们对每一种家电创建一个类,先尝试实现控制一个家电的遥控器

插槽和家电:

/**
 * @author 雫
 * @date 2021/3/4 - 14:16
 * @function 插槽
 */
public class Slot {
    private String name;

    public Slot(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

/**
 * @author 雫
 * @date 2021/3/4 - 14:17
 * @function 灯
 */
public class Light {

    public Light() {}

    public void on() {
        System.out.println("灯亮");
    }

    public void off() {
        System.out.println("灯灭");
    }
}

遥控器:

/**
 * @author 雫
 * @date 2021/3/4 - 14:15
 * @function 遥控器
 */
public class Remoter {
    private Slot slot;
    private Light light;

    public Remoter(Slot slot) {
        this.slot = slot;
        light = new Light();
    }

    public void buttonOnPress() {
        if(slot.getName().equals("light")) {
            light.on();
        }
    }

    public void buttonOffPress() {
        if(slot.getName().equals("light")) {
            light.off();
        }
    }

}

测试:
在这里插入图片描述
这样粗糙的实现,违背了:
1,针对接口编程,而不是针对实现编程,代码的功能被写死,为了实现功能而写代码

2,开放关闭原则,如果要增加新的插槽或替换某个插槽控制的家电,就需要对源码进行大量的修改

3,依赖倒置原则,这里的Remoter将来会存在大量的对象依赖,实现功能与创建对象被捆绑到了一起,Remoter依赖大量的具体家电类

为了遵循这些设计原则,设计出干净,可扩展性强,松耦合的代码,我们需要一种新的思考方式:

上面的Remoter必须实例化一些列对象(Light,TV…)才能调用这些对象的方法来操作具体的家电,但是这样就让Remoter内充满了对象,我们应该让遥控器知道什么按键被按下了,去发出一个正确的请求,由这个请求来实现具体的功能(开关灯),而不是遥控器来实现功能

我们现在需要创建一个 “命令对象”,利用命令对象来与正确的对象(家电)沟通,让按键控制命令对象,而不是具体家电,这样就能让遥控器内的代码保持简洁

1.3 看看餐厅是如何工作的

在这里插入图片描述

1,客户点单,将需要的产品记录在订单里
2,服务员将订单交给厨师
3,厨师根据订单制作客户需要的产品

我们再来看上面一个点单过程涉及到的对象:

1,客户:客户将需要的产品“封装”到了订单里
订单里封装的是一些方法createBurger(),createHotDog()...
客户只需要生成订单即可,和厨师没有关系
客户只需要把订单递给服务员

2,服务员:将订单搬运到厨师
服务员不关心订单的内容是什么,或者创建订单的客户是谁
只负责将订单送给厨师,订单里包含了封装的方法

3,厨师:等待订单,一旦有订单传来,解析订单
厨师开始执行订单内被封装的方法
厨师和服务员之间没有关系,实现了解耦,他们不需要直接沟通
厨师只负责制作具体的产品

1.4 采用命令对象实现遥控器

在这里插入图片描述
具体家电:

/**
 * @author 雫
 * @date 2021/3/4 - 15:29
 * @function 电灯
 */
public class Light {

    public Light() {}

    public void on() {
        System.out.println("打开电灯");
    }

    public void off() {
        System.out.println("关闭电灯");
    }
}


/**
 * @author 雫
 * @date 2021/3/4 - 15:32
 * @function 电视
 */
public class TV {

    public TV() {}

    public void on() {
        System.out.println("打开电视");
    }

    public void off() {
        System.out.println("关闭电视");
    }
}

Command接口及它的实现类:

/**
 * @author 雫
 * @date 2021/3/4 - 15:28
 * @function 命令接口
 */
public interface Command {
    void execute();
}


/**
 * @author 雫
 * @date 2021/3/4 - 15:29
 * @function 开灯命令
 */
public class LightOnCommand implements Command {
    private Light light;

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

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

}

/**
 * @author 雫
 * @date 2021/3/4 - 15:30
 * @function 关灯命令
 */
public class LightOffCommand implements Command {
    private Light light;

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

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

Remote:

/**
 * @author 雫
 * @date 2021/3/4 - 15:34
 * @function 简单遥控器
 */
public class SimpleRemote {
    private Command command;

    public SimpleRemote() {}

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

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

测试:
在这里插入图片描述
在上述的程序中,我们委托了命令对象来执行对家电的操作,从而实现了解耦

通过组合将实现了Command接口的类的对象作为Remote的成员,使用setCommand方法从而可以获取不同的命令对象,从而调用它们的方法来控制接收者

上述程序中:

Remote:调用者

Command:命令接口

xxxCommand:命令对象

Light:接收者

1.5 命令模式

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

调用实现了同一接口的类中的方法,而不是实例化对象后通过对象来调用方法

在这里插入图片描述
调用者不在乎自己拥有的命令对象是谁,只要该命令对象实现了Commad接口,它就可以间接地调用命令对象中的方法,从而控制接收者的行为

通过命令模式,我们成功实现了解耦减少对象依赖实现开闭原则让系统变得可扩展,有弹性

1.6 继续实现遥控器

懂了命令模式的工作原理后,我们再更新一下遥控器的设计:

在这里插入图片描述
命令模式变得一目了然,调用者通过命令对象控制接收者的行为

现在我们继续实现遥控器,让遥控器的每个插槽,对应到一个接收者(Light,TV),而遥控器的ON,OFF按键,分别对应到一个命令对象,每当按下按键,相应命令对象的excute()方法就会被调用,接收者的行为就会发生变化

在这里插入图片描述
Remote:

/**
 * @author 雫
 * @date 2021/3/4 - 15:34
 * @function 遥控器
 */
public class Remote {
    private Command[] onCommands;
    private Command[] offCommands;

    /*初始化remote对象时,为每个插槽对应的开关键装上空的命令对象*/
    public Remote() {
        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;
        }
    }

    /*设定插槽的开关键分别对应的命令对象*/
    public void setOneCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot]= offCommand;
    }

    /*指定插槽的开键被按下,调用对应命令对象的execute方法*/
    public void onButtonPressed(int slot) {
        onCommands[slot].execute();
        System.out.println(slot + "号插槽的开键被按下");
    }

    /*指定插槽的关键被按下,调用对应命令对象的execute方法*/
    public void offButtonPressed(int slot) {
        offCommands[slot].execute();
        System.out.println(slot + "号插槽的关键被按下");
    }

}

测试代码:
在这里插入图片描述
在初始化Remote类对象时,我们用NoCommand填满了所有命令对象的位置
在这里插入图片描述
这样做是为了避免空指针异常

1.7 为遥控器添加撤销功能

所谓的撤销,即执行与上一次相反的操作,我们需要在每个命令对象中新增一个undo()方法执行该命令对象excute()相反的方法,且还需要在Remote内新增一个变量记录执行最后一次操作的命令对象

Command接口和命令类:

/**
 * @author 雫
 * @date 2021/3/4 - 15:28
 * @function 命令接口
 */
public interface Command {
    void execute();
    void undo();
}


/**
 * @author 雫
 * @date 2021/3/4 - 15:29
 * @function 开灯命令
 */
public class LightOnCommand implements Command {
    private Light light;

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

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

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

}

/**
 * @author 雫
 * @date 2021/3/4 - 15:30
 * @function 关灯命令
 */
public class LightOffCommand implements Command {
    private Light light;

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

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

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

Remote:

/**
 * @author 雫
 * @date 2021/3/4 - 15:34
 * @function 遥控器
 */
public class Remote {
    private Command[] onCommands;
    private Command[] offCommands;

	//保存上一次的命令对象
    private Command undoCommand;

    /*初始化remote对象时,为每个插槽对应的开关键装上空的命令对象*/
    public Remote() {
        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 setOneCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot]= offCommand;
    }

    /*指定插槽的开键被按下,调用对应命令对象的execute方法*/
    public void onButtonPressed(int slot) {
        System.out.println(slot + "号插槽的开键被按下");
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    /*指定插槽的关键被按下,调用对应命令对象的execute方法*/
    public void offButtonPressed(int slot) {
        System.out.println(slot + "号插槽的关键被按下");
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonPressed() {
        System.out.println("撤销了上次的操作");
        undoCommand.undo();
    }

}

测试:
在这里插入图片描述

1.8 使用状态实现撤销

打开关闭电灯,撤销上次对电灯的操作太过容易,通常为了实现撤销的功能,需要记录一些状态,比如说电灯,电灯有多种亮度,每种都是一种状态,电灯默认打开是中等亮度,我们调到高亮,按下撤销,希望电灯能回到中等亮度

我们重写电灯类:

/**
 * @author 雫
 * @date 2021/3/4 - 15:29
 * @function 电灯
 */
public class Light {
    public static final int HIGH = 3;
    public static final int MEDIUM = 2;
    public static final int LOW = 1;
    public static final int OFF = 0;

    private int bright;

    public Light() {
        this.bright = OFF;
    }

    public int getBright() {
        return bright;
    }

    public void on() {
        System.out.println("打开电灯");
        this.bright = MEDIUM;
    }

    public void high() {
        System.out.println("最高亮度");
        this.bright = HIGH;
    }

    public void low() {
        System.out.println("最低亮度");
        this.bright = LOW;
    }

    public void off() {
        System.out.println("关闭电灯");
        this.bright = OFF;
    }
}

关于电灯的命令类:

/**
 * @author 雫
 * @date 2021/3/4 - 17:58
 * @function 电灯低亮
 */
public class LightLowCommand implements Command {
    private Light light;
    private int preBright;

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

    @Override
    public void execute() {
        preBright = light.getBright();
        light.low();
    }

    @Override
    public void undo() {
        if(preBright == Light.HIGH) {
            light.high();
        } else if(preBright == Light.MEDIUM) {
            light.on();
        } else if(preBright == Light.LOW) {
            light.low();
        } else if(preBright == Light.OFF) {
            light.off();
        }
    }
}


/**
 * @author 雫
 * @date 2021/3/4 - 15:30
 * @function 关灯命令
 */
public class LightOffCommand implements Command {
    private Light light;

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

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

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

测试:
在这里插入图片描述
可以在Light中增加变量preBright记录上次的亮度,每次要执行关于电灯的命令时,通过get方法取到preBright,从而在undo中完成状态的撤销

1.9 宏命令

我们现在想要通过一个命令打开/关闭所有的家电,为此我们制作一对新的命令来完成这个需求:

/**
 * @author 雫
 * @date 2021/3/4 - 18:26
 * @function 所有设备全部打开
 */
public class AllWorkCommand implements Command {
    private Command[] commands;

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

    @Override
    public void execute() {
        for(int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }

    @Override
    public void undo() {
        for(int i =0; i < commands.length; i++) {
            commands[i].undo();
        }
    }

}

/**
 * @author 雫
 * @date 2021/3/4 - 18:28
 * @function 所有设备全部关闭
 */
public class AllStopCommand implements Command {
    private Command[] commands;

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

    @Override
    public void execute() {
        for(int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }

    @Override
    public void undo() {
        for(int i =0; i < commands.length; i++) {
            commands[i].undo();
        }
    }
}

测试:
在这里插入图片描述

1.10 命令模式的更多用途

1,队列请求
在这里插入图片描述
想象有一个工作队列,在一端添加命令,另一端则是线程,线程从队列中取出一个命令,然后调用它的excute()方法,等待这个调用完成后,取出下一个命令…

工作队列类和接收者是完全解耦的,工作队列中的线程不在乎接收者是谁,只会调用命令对象中的excute()方法,从而改变接收者的行为,此刻的线程可能在进行算数运算,下一刻就开始处理网络数据

2,日志请求
在这里插入图片描述
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机后,重新调用这些动作恢复到之前的状态,通过新增两个方法 store(),load(),命令模式能支持这一点

当我们执行命令时,每次执行完excute()时,同时执行store(),将本次操作存储在磁盘中,一旦系统死机,我们可以调用命令对象的load(),从磁盘中读取先前的数据,将程序恢复到死机前的状态

对于更高级的应用,这些技巧可以扩展到事物处理中即一群操作必须全部进行完成,或者什么操作也没有进行

1.11 命令模式小结

命令模式允许我们将动作封装成命令对象,这样就可以随意存储,传递和调用它们
在这里插入图片描述

1,命令模式将发出请求的对象和执行请求的对象解耦

2,调用者通过调用命令对象的excute()方法发出请求,来控制接收者的行为

3,命令可以支持撤销,在每个命令对象中实现undo()方法来返回到excute()被执行前的状态

4,命令也可以用来实现日志和事物系统

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值