让我们继续 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 happened
interface EventHandler {
notify(event: DomainEvent): void;
}
// EventPublisher central structure for notifying all EventHandler
class 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 world
class 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):为什么重要?
3.
在TypeScript中实践DDD(领域驱动设计):实体
欢迎关注公众号:文本魔术,了解更多