命令设计模式


什么是命令设计模式

通过将“请求”封装成命令对象,来将操作的请求者与操作的执行者解耦,以便使用不同的请求队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。

为什么要使用命令设计模式

试想下面一种业务场景。 现在拥有一个中间件,名称为 cache。这个 cache 承担了异步向用户外发送邮件与短信的功能。 系统中已经有设计好的独立的邮件与短信对象,如下: 屏幕快照 2016-12-27 下午2.33.36.png-16.4kB 发送一封邮件要比发送短信复杂的多。所以,MAIL 对象相对于 SMS 对象要复杂不少。除了 title 与 body 以外,还可以设置邮件的附件。

现在让我们开始设计中间件的具体逻辑。我的思路很清晰:首先设计一个中间件。为了能够快速的从容器中获取和添加对象,我们采用 LinkedList 对象。 然后将发邮件与发短信的任务逐个添加进 cache 中,最后循环 cache,做出相应处理。

/**
 * 发送邮件
 */
public class MAIL {
    public void setMailTitle() {
        System.out.println("设置邮件标题");
    }

    public void setMailBody() {
        System.out.println("设置邮件内容");
    }

    public void setMailAttachment() {
        System.out.println("设置邮件附件");
    }

    public void sendMail() {
        System.out.println("发送邮件");
    }
}

/**
 *
 *
 */
public class SMS {

    public void setMsg() {
        System.out.println("设置短信内容");
    }

    public void sendSms() {
        System.out.println("发送短信");
    }
}


import java.util.LinkedList;
import java.util.List;

/**
 * 命令设计模式
 * 中间件任务执行器
 */
public class JobExecuter {

    final static private List<Object> caches = new LinkedList<>();

    /**
     * 执行中间件任务
     */
    private static void executeJobs() {
        for (Object cache : caches) {
            if (cache instanceof MAIL) {
                ((MAIL) cache).setMailTitle();
                ((MAIL) cache).setMailBody();
                ((MAIL) cache).setMailAttachment();
                ((MAIL) cache).sendMail();
            } else if (cache instanceof SMS) {
                ((SMS) cache).setMsg();
                ((SMS) cache).sendSms();
            }
        }
    }


    public static void main(String[] args) {
        caches.add(new MAIL());
        caches.add(new SMS());
        executeJobs();
    }
}

为了简明扼要的说明设计模式,我们省略了各种线程任务的创建执行代码。 任务已经顺利完成了,中间件能够根据任务类型自动判断不同的业务,并顺利完成需求。

遗憾的是,这样的设计拥有一些“重大缺陷”。

  1. 请求与执行的耦合度很高。假设需要再增加一个推送 app 消息的业务,那么你的代码很快就会陷入 if else 的海洋。
  2. 类没有对扩展开发,对修改关闭。当需求变化时我们仅仅能够通过修改代码来应对。
  3. executeJobs() 是一个典型的依赖具体对象,而不是接口或抽象的失败案例。
  4. 如果我们想要将发送给某1个用户的消息一起执行,应该怎么办?

好消息是,命令设计模式能够帮助我们解决上面的所有问题。 在学习命令设计模式之前,我们先试着用餐厅就餐这个日常生活中的行为,来接近命令设计模式。 当我们在餐厅就餐时,我们往往会先向服务员点餐,然后通过服务员把点好的餐点送往后厨,并通知后厨根据新订单准备餐点。

client=>start: 客户
waiter=>operation: 服务员
createOrders=>operation: 点餐(createOrders)
notifyChef=>operation: 通知后厨
chef=>end: 准备餐点

client->createOrders->waiter->notifyChef->chef

通过点餐创建的订单,屏蔽了不同的客户的不同需求。对于 waiter 来说,不需要知道订单的具体内容。只需要将客户的点餐订单传递给后厨,并通知后厨任务就算完成了。后厨通过订单内容,着手准备工作。

从餐厅到命令设计模式

将餐厅想象成 OO 设计模式的一种模型。客户就相当于 client、点餐的动作就相当于创建请求、服务员就相当于异步中间件、后厨则是真正 action 的执行者。 好了,让我们根据这样的思路重新设计之前的需求。 屏幕快照 2016-12-27 下午4.37.55.png-44.8kB


/**
 * 命令设计模式
 */
public interface Command {
    void execute();
}


/**
 * 发送邮件
 */
public class MAIL implements Command {
    public void setMailTitle() {
        System.out.println("设置邮件标题");
    }

    public void setMailBody() {
        System.out.println("设置邮件内容");
    }

    public void setMailAttachment() {
        System.out.println("设置邮件附件");
    }

    public void sendMail() {
        System.out.println("发送邮件");
    }

    @Override
    public void execute() {
        setMailAttachment();
        setMailBody();
        setMailTitle();
        sendMail();
    }
}


/**
 * 命令设计模式
 */
public class SMS implements Command {

    public void setMsg() {
        System.out.println("设置短信内容");
    }

    public void sendSms() {
        System.out.println("发送短信");
    }

    @Override
    public void execute() {
        setMsg();
        sendSms();
    }
}


import java.util.LinkedList;
import java.util.List;

/**
 * 命令设计模式
 */
public class CommandInvoker {

    private List<Command> caches = new LinkedList<>();

    /**
     * 设置Invoker所需要的命令
     *
     * @param e
     */
    public void addCommand(Command e) {
        caches.add(e);
    }

    /**
     * 执行中间件任务
     */
    public void executeJobs() {
        for (Command cache : caches) {
            cache.execute();
        }
    }

}


/**
 * 命令客户端
 */
public class CommandClient {

    public static void main(String[] args) {
        CommandInvoker commandInvoker = new CommandInvoker();
        Command commandMail = new MAIL();
        Command commandSms = new SMS();
        commandInvoker.addCommand(commandMail);
        commandInvoker.addCommand(commandSms);
        //TODO add command in futrue on pg run

        commandInvoker.executeJobs();

    }
}
设置邮件附件
设置邮件内容
设置邮件标题
发送邮件
设置短信内容
发送短信

问题回顾

总算完成了。最后再回顾一下之前出现的重大问题,看看是否这些问题都通过命令设计模式都得到了解决

请求与执行的耦合度很高。假设需要再增加一个推送 app 消息的业务,那么你的代码很快就会陷入 if else 的海洋。

通过将请求封装为命令,解耦了请求者与执行者。当有新需求时,新的请求只需要实现命令接口,就能被我们的代码所复用。

类没有对扩展开发,对修改关闭。当需求变化时我们仅仅能够通过修改代码来应对。

需求有变化时,通过 setCommand() / addCommand() 方法实现了对扩展开放,对修改关闭。

  1. executeJobs() 是一个典型的依赖具体对象,而不是接口或抽象的失败案例。

现在依赖的是Command接口对象。

如果我们想要将发送给某1个用户的消息一起执行,应该怎么办?

将所需要的任务组合成为一个新的 commandMacro 宏对象。宏对象持有一个 command 对象的数组。并在 execute 中调用数组中所有对象的 execute 方法,就能实现这样的需求。

总结

命令设计模式通过一个中间人/件将请求封装为统一的命令,达到了与命令的执行者之间实现解耦的目的。这样的设计使得中间人的代码尽可能的简单,并且能够与任意的对象组合。完整的命令设计模式还拥有 undo,撤销上一次命令的功能。 你可以试着自己来实现这个功能来加深对命令设计模式的理解。


Copyright 2017/08/15 by Chuck Lin

若文章有幸帮到了您,您可以捐助我,以鼓励我写出更棒的作品!

alipay.jpg-17.7kBwechat.jpg-16.7kB

Copyright © chuck Lin 2017 all right reserved,powered by Gitbook该文件修订时间: 2018-07-15 10:39:59

results matching ""

    No results matching ""