命令模式(封装命令)

公告

如果您是第一次阅读我的设计模式系列文章,建议先阅读设计模式开篇,希望能得到您宝贵的建议。

前言

随着上文 装饰器模式 中的顾客Alice购买了机器人回家后,他开始了机器人使用之旅。

正文

机器人:“你好,Alice!” Alice:“你好!你叫什么?” 机器人:“Alice,我是你忠臣的仆人 Samu。” Alice:“Samu,你有什么功能?” 机器人:“我会唱歌,我也会跳舞。Alice,你只要说 唱歌+歌曲名称或者跳舞+舞蹈名称我就会唱歌跳舞。如果你连续的说唱歌+歌曲名称 跳舞+舞蹈名称,我会一边唱歌一边跳舞。” Alice:“唱歌稻香 跳舞肚皮舞” 机器人:“对这个世界如果你有太多的抱怨” 机器人:“跌倒了 就不敢继续往前走” 机器人:“为什么 人要这么的脆弱 堕落” ……(画外音:曼妙的舞姿)……

程序员视角

现在要实现对机器人Samu说(发出指令) 唱歌+歌曲名称或者跳舞+舞蹈名称,机器人便会自动的唱歌或跳舞。

代码实现

定义命令的接口的目的是为了抽象类型,并且将命令实现分离。

public interface ICommand {
    void excute();
}

内部命令抽象对象,用于提供命令的上下文。

public abstract class Command implements ICommand {

    private String param;

    public Command(String param) {
        this.param = param;
    }

    protected String param() {
        return param;
    }
}

唱歌命令的实现(跳舞命令实现类似)

public class SongCommandImpl extends Command {

    public static final String KEY_SONG = "唱歌";

    public SongCommandImpl(String param) {
        super(param);
    }

    @Override
    public void excute() {
        System.out.println("调用指令 " + KEY_SONG + param());
    }
}

命令的接口与实现均已准备妥当,接下去是思考如何调用命令。 为了避免客户端与具体的命令对象耦合,所以通常建议搭配适配器模式唱歌跳舞这些指令,转化为程序可理解的SongCommandImplDanceCommandImpl

// 适配器对象用于适配字符串到命令的执行接口
public class StringCommandAdapter implements ICommand {

    private String method;
    private String param;
    private HashMap<String, Command> map = new HashMap<>();

    private Command pickCommand(String method, String param) {
        Command command = null;
        if (method.startsWith(DanceCommandImpl.KEY_DANCE)) {
            command = createCommand(DanceCommandImpl.KEY_DANCE, param, DanceCommandImpl.class);
        } else if (method.startsWith(SongCommandImpl.KEY_SONG)) {
            command = createCommand(SongCommandImpl.KEY_SONG, param, SongCommandImpl.class);
        }
        return command;
    }

    private Command createCommand(String key, String param, Class<?> clazz) {
        if (map.containsKey(key)) {
            return map.get(key);
        }
        Command command = null;
        try {
            Constructor<?> constructor = clazz.getConstructor(String.class);
            command = (Command) constructor.newInstance(param);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        if (command != null) {
            map.put(key, command);
            return command;
        } else {
            throw new IllegalArgumentException("NO COMMAND CREATED!!!");
        }
    }

    public StringCommandAdapter(String method, String param) {
        this.method = method;
        this.param = param;
    }

    @Override
    public void excute() {
        System.out.println("调用指令:" + toString());
        Command command = pickCommand(this.method, this.param);
        command.excute();
    }

    @Override
    public String toString() {
        return "StringCommandAdapter{" +
                "method='" + method + '\'' +
                ", param='" + param + '\'' +
                ", map=" + map +
                '}';
    }
}

命令如何构建已经完毕,接着是命令如何被触发。这里模拟构建机器人接收到命令在触发

// 构建命令管理器,命令的日志跟踪都可以在这里实现。
public class CommandManager {

    public void invoke(StringCommandAdapter adapter) {
        adapter.excute();
    }

}

考虑到通常命令都是通过观察者接收到消息后才触发调用的,所以这里模拟了观察者接收到消息的调用过程。

public class SamuCommandReceiver {

    private CommandManager invoke = new CommandManager();
    private Machine machine;

    public SamuCommandReceiver(Machine machine) {
        this.machine = machine;
        System.out.printf("机器人%s的接收功能正常开启%n", machine);
    }

    public void onReceive(String command, String param) {
        System.out.printf("机器人%s接收到指令:%s,%s%n", machine, command, param);
        invoke.invoke(new StringCommandAdapter(command, param));
    }
}

客户端的调用

    public static void main(String args[]) {


        Machine machine = new Machine("Samu");

        SamuCommandReceiver receiver = new SamuCommandReceiver(machine);

        receiver.onReceive("唱歌", "稻香");
        receiver.onReceive("跳舞", "肚皮舞");
    }

运行结果

创建了机器人 Samu
机器人Samu的接收功能正常开启
机器人Samu接收到指令:唱歌,稻香
调用指令:StringCommandAdapter{method='唱歌', param='稻香', map={}}
调用指令 唱歌稻香
机器人Samu接收到指令:跳舞,肚皮舞
调用指令:StringCommandAdapter{method='跳舞', param='肚皮舞', map={}}
调用指令 跳舞肚皮舞

总结

在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。

命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。

命令模式

在命令模式中,将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作模式或事务模式。

命令模式包含四个角色:

  • 抽象命令类中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作;
  • 具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中;
  • 调用者即请求的发送者,又称为请求者,它通过命令对象来执行请求;
  • 接收者执行与请求相关的操作,它具体实现对请求的业务处理。

命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。

命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。

命令模式的主要优点在于降低系统的耦合度,增加新的命令很方便,而且可以比较容易地设计一个命令队列和宏命令,并方便地实现对请求的撤销和恢复;

其主要缺点在于可能会导致某些系统有过多的具体命令类。

命令模式适用情况包括:

  • 需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互;
  • 需要在不同的时间指定请求、将请求排队和执行请求;
  • 需要支持命令的撤销操作和恢复操作,需要将一组操作组合在一起,即支持宏命令。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏梧雨北辰的开发录

iOS面试知识总结之基本概念总结

凡经历过iOS面试的我们总会发觉,即使实际开发中做过许多项目,也难免为一个普通的面试题受挫。这也许不是因为我们技术不过关,而是因为在平时我们忽略了怎样将用到的知...

42170
来自专栏冰霜之地

如何设计并实现一个线程安全的 Map ?(下篇)

在上篇中,我们已经讨论过如何去实现一个 Map 了,并且也讨论了诸多优化点。在下篇中,我们将继续讨论如何实现一个线程安全的 Map。说到线程安全,需要从概念开始...

55770
来自专栏java一日一条

能让你少写1000行代码的20个正则表达式

正则表达式,一个十分古老而又强大的文本处理工具,仅仅用一段非常简短的表达式语句,便能够快速实现一个非常复杂的业务逻辑。熟练地掌握正则表达式的话,能够使你的开发效...

10420
来自专栏玄魂工作室

PYTHON黑帽编程 4.1 SNIFFER(嗅探器)之数据捕获(下)

上一节(《4.1 SNIFFER(嗅探器)之数据捕获(上)》)中, 我们讲解了通过Raw Socket的方式来编写Sniffer的基本方法。 本节我们继续来编写...

57650
来自专栏FreeBuf

Windows内核漏洞CVE-2016-0143分析

0x00 背景 4月20日,Nils Sommer在exploitdb上爆出了一枚新的Windows内核漏洞PoC。该漏洞影响所有版本的Windows操作系统,...

24060
来自专栏分布式系统和大数据处理

.Net 项目代码风格参考

代码风格没有正确与否,重要的是整齐划一,这是我拟的一份《.Net 项目代码风格参考》,供大家参考。

15020
来自专栏FreeBuf

缓冲区溢出攻击初学者手册(更新版)

说明 之前版本翻译质量不佳,本人赵阳在这里对本文的读者表示深深的歉意。由于本人的疏忽和大意导致您不能很好的读完这篇文章,同时也对原文内容进行了破坏,也对IDF和...

30390
来自专栏帘卷西风的专栏

关于lua扩展库lpack的使用指南

lpack的具体用法 1、打包接口pack的使用,全局名字容易混淆lua本身函数unpack,使用string.pack好些,也可以修改源码修改函数名。

19030
来自专栏大闲人柴毛毛

轻量级线程池的实现

写在前面 最近因为项目需要,自己写了个单生产者-多消费者的消息队列模型。多线程真的不是等闲之辈能玩儿的,我花了两个小时进行设计与编码,却花了两天的时间调试与运...

53740
来自专栏玄魂工作室

CTF实战32 综合实战和讲解二

该培训中提及的技术只适用于合法CTF比赛和有合法授权的渗透测试,请勿用于其他非法用途,如用作其他非法用途与本文作者无关

8430

扫码关注云+社区

领取腾讯云代金券