命令模式定义:将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

# 什么是“命令模式”?

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

简单来说,它的核心思想是:不直接调用类的内部方法,而是通过给“指令函数”传递参数,由“指令函数”来调用类的内部方法。

在这过程中,分别有 3 个不同的主体:调用者、传递者和执行者。

# 应用场景

当想降低调用者与执行者(类的内部方法)之间的耦合度时,可以使用此种设计模式。比如:设计一个命令队列,将命令调用记入日志。

# ES6 实现

为了方便演示,这里模拟了购物的场景。封装一个商场类,可以查看已有商品的名称和单价。

// 为了方便演示,mock的假数据
const mockData = {
    10001: {
        name: "电视",
        price: 3888
    },
    10002: {
        name: "MacPro",
        price: 17000
    }
};
/**
 * 商品类(执行者)
 */
class Mall {
    static request(id) {
        if (!mockData[id]) {
            return `商品不存在`;
        }
        const { name, price } = mockData[id];
        return `商品名: ${name} 单价: ${price}`;
    }
    static buy(id, number) {
        if (!mockData[id]) {
            return `商品不存在`;
        }
        if (number < 1) {
            return `至少购买1个商品`;
        }
        return mockData[id].price * number;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

毫无疑问,我们可以直接调用商场类上的方法。但是这样会增加调用者和执行者的耦合度。如果之后的函数名称改变了,那么修改成本自然高。

根据命令模式的思想,封装一个“传递者”函数,专门用来传递指令和参数。如果之后商场类的函数名改变了,只需要在“传递者”函数中做个简单映射即可。

/**
 * 传递者
 */
function execCmd(cmd, ...args) {
    if (typeof Mall[cmd] !== "function") {
        return;
    }
    console.log(`<LOG> At ${Date.now()}, call ${cmd}`); // 真实场景中,可以向数据库写入日志,或者微服务上报日志
    return Mall[cmd](...args);
}
1
2
3
4
5
6
7
8
9
10

最后,下面代码展示了外界的“调用者”如何调用命令:

// 调用者
console.log(execCmd("request", 10001));
console.log("10个mbp的总价是", execCmd("buy", 10002, 10));
1
2
3

# 更多思考

在写这篇文章的时候,发现“命令模式”的思路,可以很好的组织不同版本的 api 调用。只需要在“传递者”函数中进行版本识别,然后传递到对应版本的类中即可。

这对于外界调用者来说,是无感的。即便想调用老版本的函数 api,也可以通过给“传递者”函数指定代表版本的参数来实现。

# 参考

来自: 设计模式手册之命令模式 | 心谭博客
作者:心谭
Star仓库:github