在Node.js中调度CQRS消息

时间:2018-12-17 13:03:07

标签: node.js rxjs cqrs mediator mediatr

我想为Node应用程序执行CQRS。

我不是Node的人,我来自.NET,它具有一个称为MediatR的出色库,该库可将命令/查询分派给可用于解耦请求和处理程序的介体。因此,它允许非常简单而优雅的CQRS。

在Node世界中,我发现了许多库/博客,但它们始终也包含Event Sourcing。我对ES不感兴趣。

我可以很好地对命令和查询进行建模,但是那又如何呢?需要将它们分派到某个地方,以避免混乱的方式。

据我到目前为止对Node平台的了解,一个可能的解决方案是使用观察者模式(通过RxJs库),以便控制器可以将消息(即CQRS请求)分发给观察者,然后该观察者发布相应的事件对于订阅者(即请求处理程序)。这在类似DDD的设计中将控制器和服务分离。尽管我不确定如何将结果传递回控制器。

这是其他人的做法吗?在Node中有更好的方法吗?

1 个答案:

答案 0 :(得分:2)

TL:DR :您不需要一些特别的框架,尤其是在仅进行进程内通信时,即可应用CQRS体系结构。来自events模块的本地EventEmitter就足够了。如果您希望进行进程​​间通信,servicebus的确很不错。要查看实现示例(以下较长版本的答案),可以深入研究该存储库的代码:simple node cqrs

让我们以一个非常简单的“聊天”应用程序为例,在该应用程序中,如果“聊天”未关闭,您可以发送消息以及喜欢/不喜欢的消息。

我们的主要集合体(或概念上的集合根)是ChatwriteModel/domain/chat.js):

const Chat = ({ id, isClosed } = {}) =>
  Object.freeze({
    id,
    isClosed,
  });

然后,我们得到一个Message集合(writeModel/domain/message.js):

const Message = ({ id, chatId, userId, content, sentAt, messageLikes = [] } = {}) =>
  Object.freeze({
    id,
    chatId,
    userId,
    content,
    sentAt,
    messageLikes,
  });

发送消息的行为可能是(writeModel/domain/chat.js):

const invariant = require('invariant');
const { Message } = require('./message');

const Chat = ({ id, isClosed } = {}) =>
  Object.freeze({
    id,
    isClosed,
  });

const sendMessage = ({ chatState, messageId, userId, content, sentAt }) => {
  invariant(!chatState.isClosed, "can't post in a closed chat");
  return Message({ id: messageId, chatId: chatState.id, userId, content, sentAt });
};

我们现在需要以下命令(writeModel/domain/commands.js):

const commands = {
  types: {
    SEND_MESSAGE: '[chat] send a message',
  },
  sendMessage({ chatId, userId, content, sentAt }) {
    return Object.freeze({
      type: commands.types.SEND_MESSAGE,
      payload: {
        chatId,
        userId,
        content,
        sentAt,
      },
    });
  },
};

module.exports = {
  commands,
};

由于我们使用的是Java语言,因此我们没有interface来提供抽象,因此我们使用higher order functionswriteModel/domain/getChatOfId.js):

const { Chat } = require('./message');

const getChatOfId = (getChatOfId = async id => Chat({ id })) => async id => {
  try {
    const chatState = await getChatOfId(id);
    if (typeof chatState === 'undefined') {
      throw chatState;
    }
    return chatState;
  } catch (e) {
    throw new Error(`chat with id ${id} was not found`);
  }
};

module.exports = {
  getChatOfId,
};

({writeModel/domain/saveMessage.js):

const { Message } = require('./message');

const saveMessage = (saveMessage = async (messageState = Message()) => {}) => saveMessage;

module.exports = {
  saveMessage,
};

我们现在需要实现我们的commandHandlers(应用程序服务层):

({writeModel/commandHandlers/handleSendMessage.js

const { sendMessage } = require('../domain/chat');

const handleSendMessage = ({
  getChatOfId,
  getNextMessageId,
  saveMessage,
}) => async sendMessageCommandPayload => {
  const { chatId, userId, content, sentAt } = sendMessageCommandPayload;
  const chat = await getChatOfId(chatId);
  return saveMessage(
    sendMessage({
      chatState: chat,
      messageId: getNextMessageId(),
      userId,
      content,
      sentAt,
    }),
  );
};

module.exports = {
  handleSendMessage,
};

由于JavaScript中没有interface,因此我们使用higher order functions通过在运行时注入依赖项来应用依赖项反转原理。

然后我们可以实现写模型的组合根:(`writeModel / index.js):

const { handleSendMessage } = require('./commandHandlers/handleSendMessage');
const { commands } = require('./domain/commands');

const SimpleNodeCQRSwriteModel = ({
  dispatchCommand,
  handleCommand,
  getChatOfId,
  getNextMessageId,
  saveMessage,
}) => {
  handleCommand(
    commands.types.SEND_MESSAGE,
    handleSendMessage({ getChatOfId, getNextMessageId, saveMessage }),
  );
};

module.exports = {
  SimpleNodeCQRSwriteModel,
};

您的commandscommand handler没有联系在一起,然后可以在运行时提供这些功能的实现,例如,内存数据库和节点EventEmitter({{ 1}}):

writeModel/infrastructure/inMemory/index.js

然后我们的const uuid = require('uuid/v1'); const { saveMessage } = require('../../domain/saveMessage'); const { getChatOfId } = require('../../domain/getChatOfId'); const { getNextMessageId } = require('../../domain/getNextMessageId'); const InMemoryRepository = (initialDbState = { chats: {}, messages: {}, users: {} }) => { const listeners = []; const db = { ...initialDbState, }; const addOnDbUpdatedListener = onDbUpdated => listeners.push(onDbUpdated); const updateDb = updater => { updater(); listeners.map(listener => listener(db)); }; const saveMessageInMemory = saveMessage(async messageState => { updateDb(() => (db.messages[messageState.id] = messageState)); }); const getChatOfIdFromMemory = getChatOfId(async id => db.chats[id]); const getNextMessageUuid = getNextMessageId(uuid); return { addOnDbUpdatedListener, saveMessage: saveMessageInMemory, getChatOfId: getChatOfIdFromMemory, getNextMessageId: getNextMessageUuid, }; }; module.exports = { InMemoryRepository, }; 将它们绑在一起:

TestWriteModel

您可以使用以下存储库中的代码(非常简单的const EventEmitter = require('events'); const { SimpleNodeCQRSwriteModel } = require('../writeModel'); const { InMemoryRepository } = require('../writeModel/infrastructure/inMemory'); const TestWriteModel = () => { const { saveMessage, getChatOfId, getNextMessageId } = InMemoryRepository(); const commandEmitter = new EventEmitter(); const dispatchCommand = command => commandEmitter.emit(command.type, command.payload); const handleCommand = (commandType, commandHandler) => { commandEmitter.on(commandType, commandHandler); }; return SimpleNodeCQRSwriteModel({ dispatchCommand, handleCommand, getChatOfId, getNextMessageId, saveMessage, }); }; ):simple node cqrs