CQRS

CQRS

可以使用以下步骤描述最简单的CRUD应用程序的流程:

  • 控制器层处理HTTP请求并将任务委派给服务。

在大多数情况下,没有理由让中小型应用程序更复杂。但有时它还不够,当我们的需求变得更加复杂时,我们希望拥有可扩展的系统,并且数据流动简单。

这就是Nest提供轻量级CQRS模块的原因,下面将详细介绍这些组件。

命令

为了使应用程序更易于理解,每个更改都必须以命令开头。分派任何命令时,应用程序必须对其作出反应。可以从服务调度命令并在相应的命令处理程序中使用命令

英雄,game.service.ts

JS

@Injectable() export class HeroesGameService { constructor(private readonly commandBus: CommandBus) {} async killDragon(heroId: string, killDragonDto: KillDragonDto) { return await this.commandBus.execute( new KillDragonCommand(heroId, killDragonDto.dragonId) } }

这是一个发送的示例服务KillDragonCommand。让我们看一下命令的样子:

杀-dragon.command.ts

JS

export class KillDragonCommand implements ICommand { constructor( public readonly heroId: string, public readonly dragonId: string, ) {} }

CommandBus是一个命令。它将命令委托给等效的处理程序。每个Command都必须有相应的Command Handler

杀-dragon.handler.ts

JS

@CommandHandler(KillDragonCommand) export class KillDragonHandler implements ICommandHandler<KillDragonCommand> { constructor(private readonly repository: HeroRepository) {} async execute(command: KillDragonCommand, resolve: (value?) => void) { const { heroId, dragonId } = command; const hero = this.repository.findOneById(+heroId hero.killEnemy(dragonId await this.repository.persist(hero resolve( } }

现在,每个应用程序状态更改都是Command发生的结果。逻辑封装在处理程序中。如果我们想要在这里添加日志记录甚至更多,我们可以将命令保存在数据库中(例如用于诊断目的)。

为什么我们需要resolve()功能?有时我们可能希望将消息从处理程序返回到服务。此外,我们可以在execute()方法的开头调用此函数,因此应用程序将首先返回到服务并将响应返回给客户端,然后异步返回此处以处理调度的命令。

活动

由于我们在处理程序中封装了命令,因此我们阻止它们之间的交互 ​​- 应用程序结构仍然不灵活,不具有反应性。解决方案是使用事件

英雄杀,dragon.event.ts

JS

export class HeroKilledDragonEvent implements IEvent { constructor( public readonly heroId: string, public readonly dragonId: string) {} }

事件是异步的。它们由模型派遣。模型必须扩展AggregateRoot课程。

hero.model.ts

JS

export class Hero extends AggregateRoot { constructor(private readonly id: string) { super( } killEnemy(enemyId: string) { // logic this.apply(new HeroKilledDragonEvent(this.id, enemyId) } }

apply()方法尚未调度事件,因为模型和EventPublisher类之间没有关系。如何告诉模特关于出版商?我们需要mergeObjectContext()在命令处理程序中使用publisher 方法。

杀-dragon.handler.ts

JS

@CommandHandler(KillDragonCommand) export class KillDragonHandler implements ICommandHandler<KillDragonCommand> { constructor( private readonly repository: HeroRepository, private readonly publisher: EventPublisher, ) {} async execute(command: KillDragonCommand, resolve: (value?) => void) { const { heroId, dragonId } = command; const hero = this.publisher.mergeObjectContext( await this.repository.findOneById(+heroId), hero.killEnemy(dragonId hero.commit( resolve( } }

现在一切都按预期工作。请注意,我们需要commit()事件,因为它们不会立即发送。当然,对象不一定存在。我们也可以轻松地合并类型上下文:

const HeroModel = this.publisher.mergeContext(Hero new HeroModel('id'

而已。模型现在可以发布事件。我们必须处理它们。

每个事件都可以有很多事件处理程序。他们不必彼此了解。

英雄杀,dragon.handler.ts

JS

@EventsHandler(HeroKilledDragonEvent) export class HeroKilledDragonHandler implements IEventHandler<HeroKilledDragonEvent> { constructor(private readonly repository: HeroRepository) {} handle(event: HeroKilledDragonEvent) { // logic } }

现在我们可以将写入逻辑移动到事件处理程序中。

传奇

这种类型的事件驱动架构提高了应用程序的反应性和可伸缩性。现在,当我们有活动时,我们可以简单地以各种方式对它们做出反应。的传奇故事是从架构上看过去的积木。

传奇是一个非常强大的功能。单传奇可以听1 .. *事件。它可以组合,合并,过滤事件流。RxJS库是神奇来源的地方。简单来说,每个saga都必须返回一个包含命令的Observable。异步调度此命令。

英雄,game.saga.ts

JS

@Component() export class HeroesGameSagas { dragonKilled = (events$: EventObservable<any>): Observable<ICommand> => { return events$.ofType(HeroKilledDragonEvent) .map((event) => new DropAncientItemCommand(event.heroId, fakeItemID) } }

我们宣布了一个规则,当任何英雄杀死龙时 - 它应该获得古代物品。然后DropAncientItemCommand由适当的处理程序调度和处理。

建立

我们要处理的最后一件事是建立整个机制。

英雄,game.module.ts

JS

export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; @Module{ imports: [CQRSModule], controllers: [HeroesGameController], providers: [ HeroesGameService, HeroesGameSagas, ...CommandHandlers, ...EventHandlers, HeroRepository, ] }) export class HeroesGameModule implements OnModuleInit { constructor( private readonly moduleRef: ModuleRef, private readonly command$: CommandBus, private readonly event$: EventBus, private readonly heroesGameSagas: HeroesGameSagas, ) {} onModuleInit() { this.command$.setModuleRef(this.moduleRef this.event$.setModuleRef(this.moduleRef this.event$.register(EventHandlers this.command$.register(CommandHandlers this.event$.combineSagas([ this.heroesGameSagas.dragonKilled, ] } }

概要

这两个CommandBusEventBus观测量。这意味着您可以轻松订阅整个流,并通过Event Sourcing丰富您的应用程序。