CQRS
可以使用以下步骤描述最简单的CRUD应用程序的流程:
- 控制器层处理HTTP请求并将任务委派给服务。
- 服务层是完成大部分业务逻辑的地方。
- 服务使用存储库/ DAO来更改/保留实体。
- 实体是我们的模型 - 只是值的容器,有setter和getter。
在大多数情况下,没有理由让中小型应用程序更复杂。但有时它还不够,当我们的需求变得更加复杂时,我们希望拥有可扩展的系统,并且数据流动简单。
这就是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,
]);
}
}
概要
这两个CommandBus
和EventBus
的观测量。这意味着您可以轻松订阅整个流,并通过Event Sourcing丰富您的应用程序。
本文档系腾讯云开发者社区成员共同维护,如有问题请联系 cloudcommunity@tencent.com