こんにちはAnother works CTOの塩原です。
複業クラウドのバックエンドではDDDとクリーンアーキテクチャを導入しています。
社内では、松岡さんの書籍やTwitterなどを参考にしながらDDDの開発に取り組んでいます。
DDDの記事は参考になるものが松岡さんの記事やその他の人が書いた記事など豊富にあるため、設計方法についてはそこまで困ることはありません。
DDDの記事を見ているとJavaやKotlin、Scalaだったり、一部フレームワーク独自構文を使ってDDDを実装しているものも多いため、今回はTypescriptで実装するための方法を記事にすることで、Node.jsを使っている方や、Typescriptを使っている方に参考になればと思い、書かせていただきました。
この記事で書くこと・書かないこと
書くこと
- ドメインイベントをTypescriptで実装する方法
書かないこと
- ドメインイベントを始め、DDDに関する説明
シナリオ
求人作成した際に、求人のデータなどを把握することができる求人レポートを生成することができる
簡易ドメイン図
イベント周りの処理フロー図
ドメインイベント
まずはドメインイベントクラスを実装します
EventListenerでイベントごとに実行する処理を変える必要があるので、nameで判断できるようにします
export type DomainEventName = 'projectCreateEvent'
export interface DomainEvent {
name: DomainEventName
}
type ProjectInfo = {
title: string
}
/**
* 求人作成イベント
*/
export class ProjectCreateEvent implements DomainEvent {
name: 'projectCreate' = 'projectCreate'
projectInfo: ProjectInfo
constructor(params: { projectInfo: ProjectInfo }) {
this.projectInfo = params.projectInfo
}
}
ドメインイベントの生成
DomainEventStorable
という抽象クラスで、エンティティーがイベントを保持するための機能を抽象化しています。
ProjectDomainModel
で継承し、create関数の中で ProjectCreateEvent
を発火するようにしています。
export abstract class DomainEventStorable {
private domainEvents: DomainEvent[] = []
protected addDomainEvent(domainEvent: DomainEvent) {
this.domainEvents.push(domainEvent)
}
getDomainEvents(): DomainEvent[] {
return this.domainEvents
}
clearDomainEvents() {
this.domainEvents = []
}
}
export default class ProjectDomainModel extends DomainEventStorable {
constructor() {
super()
//...
}
static create(title: string) {
//...
//
model.addDomainEvent(
new ProjectCreateEvent({
projectInfo: {
title
},
})
)
}
}
イベント発行処理
Node.jsにはEventEmitterという標準クラスがあり、イベント処理など行えるので DomainEventListener
というクラスを使ってイベント処理をラップしています。
DomainEventPublisherImpl
はイベントを発行する処理で ProjectRepositoryImpl
の関数の引数に渡して、repository内でpublish処理を行うようにする
publishする処理はドメインイベント全体で共通となっているので、ドメインイベントごとに作成する必要はありません。
import EventEmitter from 'events'
const emitter = new EventEmitter()
emitter.setMaxListeners(0)
export class DomainEventListener {
static on<T extends DomainEvent>(
eventName: T['name'],
callback: (event: T) => void
) {
emitter.on(eventName as string, callback)
}
static emit(eventName: DomainEvent['name'], event: DomainEvent) {
emitter.emit(eventName as string, event)
}
}
export interface DomainEventPublisher {
publish(event: DomainEvent): Promise<void>
}
export class DomainEventPublisherImpl implements DomainEventPublisher {
async publish(event: DomainEvent): Promise<void> {
try {
DomainEventListener.emit(event.name, event)
new DomainEventEntity(DotEnvConsts.getFcLoggerEnv())
.setData({
eventInfo: JSON.stringify(event),
eventName: event.name,
time: new DateLib().formatDate(
new Date(),
'YYYY-MM-DD HH:mm:ss'
),
})
.save()
} catch (e) {
SentryLib.captureException(e)
LoggerLib.error(`${event.name}: ${e};event: ${event}`)
}
}
}
export class ProjectRepositoryImpl implements ProjectRepository {
async save(
model: ProjectDomainModel,
publisher: DomainEventPublisher
): Promise<void> {
// 保存処理
model.getDomainEvents().forEach((event) => publisher.publish(event))
}
}
イベントリスナー
イベントリスナークラスはドメインごとに複数作成します。
今回イベントを受け取って振る舞いを定義するのはProjectReportになるので、 ProjectReportListener
として作成して受け取ったイベントの振る舞いを定義しています。
export class ProjectReportEventListener {
constructor(private repository: ProjectReportRepository) {
DomainEventListener.on<ProjectCreateDomainEvent>(
'projectCreate',
(event: ProjectCreateDomainEvent) =>
this.callback(event)
)
}
private callback(event: ProjectCreateDomainEvent) {
// repositoryを使って、ProjectReportのsave処理を行う
}
}
// index.ts
createConnection().then(() => {
app.listen(DotEnvConsts.getAppPort(), () => {
LoggerLib.debug(`Start on port ${DotEnvConsts.getAppPort()}.`)
// Event Lisntenerを起動
new ProjectReportEventListener()
})
})
まとめ
今回はNode.jsとTypescriptを使って実装する方法を書きました。
TypescriptはkotlinやScaleなどと比較すると型システムとして足りない機能などありますが、ジェネリクスなどをうまく使うことでやりたいことを達成することができます。
Share this post