- Welcome to the Node.js Platform
- Node.js Essential Patterns
- Asynchronous Control Flow Patterns with Callbacks
- Asynchronous Control Flow Patterns with ES2015 and Beyond
- Coding with Streams
- Design Patterns
- Writing Modules
- Advanced Asynchronous Recipes
- Scalability and Architectural Patterns
- Messaging and Integration Patterns
- Welcome to the Node.js Platform
- Node.js 的发展
- Node.js 的特点
- 介绍 Node.js 6 和 ES2015 的新语法
- reactor 模式
- Node.js Essential Patterns
- Asynchronous Control Flow Patterns with Callbacks
- Asynchronous Control Flow Patterns with ES2015 and Beyond
- Coding with Streams
- Design Patterns
- Writing Modules
- Advanced Asynchronous Recipes
- Scalability and Architectural Patterns
- Messaging and Integration Patterns
命令模式( Command )
命令模式是在 Node.js
中另一个重要的设计模式。在其最通用的定义中,命令模式封装了主体对象信息,并对主体对象执行一个动作,而不是在主体对象上直接调用一个方法或一个函数,我们创建一个对象 invocation
执行这样一个 调用;那么实现这个意图将是另一个组件的责任,将其转化为实际行动。传统上,这个模式是围绕着四个主要的组件,如下图所示:
命令模式的典型组织可以描述如下:
Command
:这是封装调用一个必要信息的对象方法或功能。Client
:这将创建该命令并将其提供给调用者。Invoker
:这是负责执行目标上的命令。Target
(或Receiver
):这是调用的主题。它可以是一个单独的功能或对象的方法。
正如我们将看到的,这四个组件可以根据我们想要的方式变化很多实施模式;在这一点上,这听起来不是什么新鲜事。使用命令模式而不是直接执行一个操作有好几个。
优点和应用:
- 命令可以安排在稍后执行。
- 一个命令可以很容易地序列化并通过网络发送。这很简单,属性允许我们在远程机器上分配作业,传输命令
- 从浏览器到服务器,创建
RPC
系统等等。 - 通过命令可以很容易地在系统上保存所有执行的操作历史记录。
- 命令是一些数据同步算法的重要组成部分和解决冲突。
- 计划执行的命令如果尚未执行,则可以取消。它 也可以恢复(撤消),使应用程序的状态的重点 在命令执行之前。
- 几个命令可以组合在一起。这可以用来创建原子交易或实施一个机制,从而在所有的操作组立即执行。
- 可以对一组命令执行不同类型的转换,例如 作为重复删除,加入和拆分,或应用更复杂的算法如
Operational Transformation
(OT
),这是当今大多数的基础实时协作软件,如协同文本编辑。
前面的列表清楚地向我们展示了这种模式的重要性,特别是在 node.js
这样的平台中,网络和异步执行是必不可少的参与者。
灵活模式
正如我们已经提到的, JavaScript
中的命令模式可以通过许多不同的方式实现;我们现在只演示其中的几个,只是为了给出它的范围的概念。
任务模式
我们可以从最基本的和平凡的实现开始:任务模式。当然, JavaScript
中创建一个表示调用的对象的最简单方法是创建一个关闭:
function createTask(target, args) {
return () => {
target.apply(null, args);
}
}
这看起来一点也不新鲜;我们已经在书中多次使用了这种模式,特别是在第 3 章,带有回调的异步控制流模式中。这种技术允许我们使用单独的组件来控制和调度任务的执行,这在本质上等同于命令模式的调用者。例如,您还记得我们是如何定义传递给异步库的任务的吗?或者更好的是,你还记得我们是如何结合使用发电机的吗?回调模式本身可以被认为是命令模式的一个非常简单的版本。
较复杂的命令模式
现在让我们来处理一个更复杂的命令的示例;这一次我们希望支持撤消和序列化。让我们从命令的目标开始,这个小对象负责向 Twitter 这样的服务发送状态更新。为了简单起见,我们使用这种服务的模拟:
const statusUpdateService = {
statusUpdates: {},
sendUpdate: function(status) {
console.log('Status sent: ' + status);
Design Patterns
[252]
let id = Math.floor(Math.random() * 1000000);
statusUpdateService.statusUpdates[id] = status;
return id;
},
destroyUpdate: id => {
console.log('Status removed: ' + id);
delete statusUpdateService.statusUpdates[id];
}
};
现在,让我们创建一个命令来表示新状态更新的发布:
function createSendStatusCmd(service, status) {
let postId = null;
const command = () => {
postId = service.sendUpdate(status);
};
command.undo = () => {
if (postId) {
service.destroyUpdate(postId);
postId = null;
}
};
command.serialize = () => {
return {
type: 'status',
action: 'post',
status: status
};
};
return command;
}
前面的函数是一个工厂,它生成新的 sendstate
命令。每个命令实现以下三个功能:
- 命令本身是一个函数,当调用它时,它将触发操作;换句话说,它实现了我们前面看到的任务模式。该命令在执行时将使用目标服务的方法发送新的状态更新。
- 连接到主任务的
auto()
函数,该函数恢复操作的效果。在我们的例子中,我们只是调用目标服务上的deadyupdate()
方法。 serialize()
函数,它构建一个json
对象,该对象包含重建同一个命令对象所需的所有信息。 在此之后,我们可以构建一个调用程序;我们可以通过实现它的构造函数和它的run()
方法来开始:
前面定义的class Invoker { constructor() { this.history = []; } run(cmd) { this.history.push(cmd); cmd(); console.log('Command executed', cmd.serialize()); } }
run()
方法是Invoker
的基本功能;它负责将命令保存到history
实例变量中,然后触发命令本身的执行。接下来,我们可以添加一个延迟执行命令的新方法:
然后,我们可以实现一个delay(cmd, delay) { setTimeout(() => { this.run(cmd); }, delay) }
undo()
方法来恢复最后一个命令:
最后,我们还希望能够在远程服务器上运行命令,方法是使用undo() { const cmd = this.history.pop(); cmd.undo(); console.log('Command undone', cmd.serialize()); }
Web
服务序列化并通过网络传输命令:
既然我们有了命令、调用程序和目标,唯一缺少的组件就是客户端。让我们从实例化runRemotely(cmd) { request.post('http://localhost:3000/cmd', { json: cmd.serialize() }, err => { console.log('Command executed remotely', cmd.serialize()); } ); }
Invoker
开始:
然后,我们可以使用以下代码行创建一个命令:const invoker = new Invoker();
现在我们有了一个命令,表示状态消息的发布;然后我们可以决定立即发送它:const command = createSendStatusCmd(statusUpdateService, 'HI!');
但是,我们犯了一个错误;让我们恢复到时间线的状态,就像发送最后一条消息之前的情况一样:invoker.run(command);
我们还可以决定从现在起一小时内发送消息:invoker.undo();
或者,我们可以通过将任务迁移到另一台机器来分配应用程序的负载:invoker.delay(command, 1000 * 60 * 60);
invoker.runRemotely(command);
我们刚刚创建的一个小例子展示了如何在命令中包装一个操作可以打开一个可能性的世界,这只是冰山一角。
正如最后的讨论,值得注意的是,只有在真正需要的时候才会使用成熟的命令模式。事实上,我们看到了我们需要编写多少额外的代码来简单地调用 statuupdatesservice
方法;如果我们所需要的只是一个调用,那么一个复杂的命令就会被杀死。但是,如果我们需要安排任务的执行,或者运行异步操作,那么简单的任务模式提供了最好的折衷。如果相反,我们需要更高级的特性,如撤销支持、转换、冲突解决,或者我们前面描述的其他花哨用例之一,那么对命令使用更复杂的表示几乎是必要的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论