TypeScript中的DDD实践(五):域事件

图片

让我们继续 TypeScript 中的 DDD 故事,介绍一个描述现实世界中事件的构建块 —— 域事件。

假设我们,软件开发人员,喜欢做的事情是使用一些新的东西,一些很酷的东西,一些尖端的东西。如果我们可以选择,那将是我们每天的工作:做一些独特的东西。

软件开发中的事件部分内容在领域驱动设计中也有涉及,在这里,领域事件为业务逻辑提供了关键的功能。

尽管Eric Evans在他的书的第一版中没有涉及域事件模式,但今天,不使用事件来完成域层是具有挑战性的。

Domain Event 模式在代码中表示这样的事件。它们用于描述与我们的业务逻辑相关的任何来自现实世界的事件。

如今,商业世界中的一切都与一些事件有关。

事件就是一切

图片

Domain Events 可以是任何东西,尽管它们需要满足一些规则。第一个规则是 — 它们是不可变的。为了支持这个特性,我们应该在内部使用私有字段。

一个特定的事件只能发生一次。

这意味着我们只能创建一个具有某个 Identity 的  Order  实体一次,因此我们的代码只能触发一次事件,描述创建具有特定 Identity 的  Order 。

任何其他的事件,对于那个 Order 来说,都是不同类型的事件。任何其他的创建事件,都是一些不同 Order 实体的事件。

每个事件实际上都描述了已经发生的事情。

它代表过去,也就是说当我们触发 OrderCreated 事件时,我们已经创建了 Order ,并且我们没有再次创建它的意图。

一个简单的通用事件

interface DomainEvent {  name(): string;}
class GeneralError extends String implements DomainEvent {  constructor(err: Error) {    super(err.message);  }    name(): string {    return 'event.general.error'  }}

上面的代码示例展示了简单的 Domain Events。这段代码是数以亿计的解决方案之一。在某些情况下,如这里  GeneralError ,我们可以只使用简单的字符串。

但是,有时候它们可能是复杂的对象,那么我们应该用一些更具体的抽象类(或接口)扩展主  DomainEvent  接口,以添加额外的方法,就像  OrderEvent  一样。

一些复杂的业务相关事件​​​​​​​

abstract class OrderEvent implements DomainEvent {  constructor(    private _orderId: string) {}
  abstract name(): string;
  orderId(): string {    return this._orderId;  }}
class OrderDispatched extends OrderEvent {  name(): string {    return 'event.order.dispatched'  }}
class OrderDelivered extends OrderEvent {  name(): string {    return 'event.order.delivery.success'  }}
class OrderDeliveryFailed extends OrderEvent {  name(): string {    return 'event.order.delivery.failed'  }}

域事件作为接口不需要实现任何方法。它可以是任何你想要的。如上所述,有时我使用字符串,但任何东西都足够好了。

为了泛化,我仍然不时地声明  DomainEvent  接口。

观察者模式

图片

域事件作为一种模式,并没有提供新的结构,但它是观察者模式的另一种表示,观察者模式将发布者、订阅者以及事件视为主要参与者。

域事件遵循同样的逻辑,订阅者或事件处理器是一个结构,它应该对它订阅的特定域事件做出响应。

发布者是一个结构,一旦某个事件发生,它会通知所有事件处理程序。

Publisher 是触发任何事件的入口点,它包含所有事件处理程序,并为任何域服务、工厂或其他想要发布事件的对象提供一个简单的接口。

一个框架不可知的事件发布器​​​​​​​

// EventHandler interface for describing any object // that should be notified upon some DomainEvent has happenedinterface EventHandler {  notify(event: DomainEvent): void;}
// EventPublisher central structure for notifying all EventHandlerclass EventPublisher {  private handlers: Record<string, EventHandler[]>;
  constructor() {    this.handlers = {};  }   // subscribes EventHandler to particular Event  subscribe(handler: EventHandler, events: DomainEvent[]) {    for (const event of events) {      let handlers = this.handlers[event.name()];      if (!handlers) {        handlers = [];      }            handlers.push(handler);      this.handlers[event.name()] = handlers    }  }    // notifies subscribed EventHandler for particular Event  notify(event: DomainEvent) {    for (const handler of this.handlers[event.name()]) {      handler.notify(event)    }  }}

上面的代码片段展示了域事件模式的其余部分。接口 EventHandler  表示任何应该对某个事件做出反应的对象。

它只有一个notify方法,该方法将Event作为参数。类 EventPublisher 更复杂。它提供了通用的 notify 方法,该方法负责通知订阅该 Event 的所有 EventHandlers

另一个函数subscribe 为任何EventHandler添加了订阅任何 Event 的可能性。EventPublisher 类可以更简单。

相反,为了给 EventHandler  一个机会通过使用映射订阅特定的 Event  ,它只能处理一个简单的 EventHandlers  数组,并通知所有 Event  。

创建

图片

没有创建 Event 的正确位置。

我们可以在任何地方使用它们,唯一的规则应该是有状态对象(实体、值对象)不能通知 EventPublisher。此外,有状态对象也不应该成为  EventHandlers 。

如果我们需要在特定事件发生时对某个实体做一些事情,我们应该创建一个包含 Repository 的  EvenHandler  ,然后,Repository 可以提供一个应该被操纵的实体。

尽管如此,在聚合的某个方法中创建事件对象还是可以的,有时,我在实体的方法中创建它们,并作为结果返回它们。

然后我们可以使用像Domain Service Factory这样的结构来通知 EventPublisher 。

聚合内部的创建​​​​​​​

class Order {  constructor(    private id: string,    private isDispatched: boolean,    private deliveryAddress: Address,) {}    changeAddress(address: Address): OrderEvent {    if (this.isDispatched) {      return new DeliveryAddressChangeFailed(this.id);    }        //    // some business logic    //        return new DeliveryAddressChanged(this.id);  }}


从域名服务发布​​​​​​​

class OrderService {  constructor(    private repository: OrderRepository,    private publisher: EventPublisher,  ) {}    async create(order: Order): Promise<Order> {    const result = await this.repository.create(order)        //    // some business logic    //        this.publisher.notify(new OrderCreated(result.id))        return result  }    changeAddress(order: Order, address: Address) {    const event = order.changeAddress(address);        // publishing of events only inside stateless objects    this.publisher.notify(event);  }}

在上面的例子中, Order 聚合提供了一个更新传递地址的方法。该方法的结果可能是一个事件。这意味着 Order 可以创建一些事件,但这都是来自那个实体。

另一方面, OrderService  可以创建和发布事件,它还可以在更新传递地址时触发从  Order  实体接收的事件,这是可能的,因为它包含  EventPublisher 。

其他层上的事件

图片

我们可以在其他层上监听域事件,比如应用层、表示层或基础架构层,我们也可以定义单独的事件,只用于这些层,在这些情况下,我们不讨论域事件。

一个简单的例子是应用层的 Events.在创建一个  Order  之后,在大多数情况下,我们应该发送一个  Email  给客户.尽管它可能看起来像一个业务规则,但发送电子邮件可能是特定于应用程序的.

在下面的例子中,有一个简单的代码,包含  EmailEvent 。 Email  可以处于许多不同的状态,并且在某些  Events  期间总是执行从一种状态到另一种状态的切换。

域名层上的电子邮件实体​​​​​​​

class Email {  constructor(    id: string,    //    // some fields    //) {}}


域层上的电子邮件事件​​​​​​​

interface EmailEvent extends DomainEvent {  emailId(): string;}
class EmailSent {  constructor(    private _emailId: string) {}    name(): string {    return 'event.email.sent'  }    emailId(): string {     return this._emailID;  }}


域层的事件处理器​​​​​​​

class EmailHandler {  notify(event: DomainEvent) {    if (event instanceof EmailSent) {      //      // do something      //    }  }}

有时我们想在我们的BoundedContext之外触发DomainEvent,这些DomainEvent是BoundedContext的内部事件,但它们是其他事件的外部事件。

虽然这更像是战略领域驱动设计的主题,但我们可以检查一个示例,为了在微服务之外发布事件,我们可以使用一些消息传递服务,比如SQS。

在基础设施层上通过AWS SQS将事件作为消息发送​​​​​​​

import AWS from 'aws-sdk'
// EventSQSHandler publishes internal events to external worldclass EventSQSHandler {  constructor(    private svc: AWS.SQS  ) {}    // notify publishes Event over SQS  notify(event: DomainEvent) {    const data = {      'event': event.name()    };        const body = JSON.stringify(data);        this.svc.sendMessage({      MessageBody: body,      QueueUrl: this.svc.endpoint,    })  }}

上面的例子显示了基础架构层中的  SQSService ,这个服务监听内部事件并将它们映射到 SQS 消息。

我没有过多使用这种方法,但在某些情况下,它是值得的,例如,如果许多微服务应该对  Order  的创建做出反应,或者当  Customer  被注册时。

结论

领域事件是领域逻辑中不可避免的对象。今天,商业世界中的一切都与特定的事件绑定在一起,因此用事件来描述我们的领域模型是一个很好的实践。



TypeScript领域驱动设计(DDD)系列:

1. TypeScript中的实用领域驱动设计(DDD):为什么重要?

2. TypeScript 中 DDD 的实践:值对象

3. 在TypeScript中实践DDD(领域驱动设计):实体

 欢迎关注公众号:文本魔术,了解更多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值