🎊

TypeScript で値オブジェクトをエンティティに組み込む

2023/06/30に公開

前回「TypeScript での値オブジェクト実装」の続きです。

前回でイミュータブルな値オブジェクトを実現できるクラスを作りました。今回はドメイン層のエンティティのプロパティとして値オブジェクトを組み込んでいきます。

実装

この記事がとても参考になりました。

https://blog.mamansoft.net/2019/03/10/battle-with-boundary-typescript/

https://blog.j5ik2o.me/entry/20101229/1293634287

単純なデータ形式、JSONなオブジェクトをエンティティ化するために class-transformer を利用します。

準備

npm i class-transformer reflect-metadata

デコレーターを使うために tsconfig.json のパラメーターの修正をします。

tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    // 省略
  }
}

エンティティ

ユーザーを例にして実装してみます。不変性のあるエンティティ

ポイントは Transform デコレーターを付けて値オブジェクトの生成処理を一緒に記述しておくと、後で登場する plainToInstance() を活用できます。

user.ts
import { Transform, plainToInstance } from "class-transformer";

import { UserId } from "./id-series";
import { NickName } from "./nick-name";
import { RealName } from "./real-name";

export class User {
	@Transform((params) => new UserId(params.value))
	readonly id: UserId;

	@Transform((params) => new RealName(params.value))
	readonly realName: RealName;

	@Transform((params) => new NickName(params.value))
	readonly nickName: NickName;

	@Transform((params) => new Date(params.value))
	readonly birthDay: Date;

	equals(other?: User): boolean {
		if (!other) {
			return false;
		}
		if (this === other) {
			return true;
		}
		return (
			this.id.equals(other.id) &&
			this.realName.equals(other.realName) &&
			this.nickName.equals(other.nickName) &&
			this.birthDay.getTime() === other.birthDay.getTime()
		);
	}

	deepCopy(): User {
		return plainToInstance(User, JSON.parse(JSON.stringify(this)));
	}

	changeNickName(value: NickName): User {
		const other = this.deepCopy();
		return Object.assign(other, { nickName: value });
	}
}

ファクトリー

create は新規用に固有のIDを生成しつつ、パラメーターを受け取ってエンティティを生成します。transform はデータベースから等の既存データをもとに完全体を生成するとき用です。

user-factory.ts
import { randomUUID } from "crypto";

import { plainToInstance } from "class-transformer";

import { User } from "./user";

export type UserPlain = {
	id: string;
	realName: string;
	nickName: string;
	birthDay: string;
};

export class UserFactory {
	create(plain: Omit<UserPlain, "id">): User {
			...plain,
			id: randomUUID(),
		});
	}

	transform(plain: UserPlain): User {
		this.validate(plain);
		return plainToInstance(User, plain);
	}

	private validate(plain: UserPlain) {
		// TODO plain の妥当性チェックをする
	}
}

使ってみる

新しく生成しつつ、ニックネームを変えてみます。

main.ts
import { NickName } from "./nick-name";
import { UserFactory } from "./user-factory";

const factory = new UserFactory();

const user1 = factory.create({
	realName: "上田 次郎",
	nickName: "ueda",
	birthDay: "1965-11-04",
});

const user2 = user1.deepCopy();

const user3 = user1.changeNickName(new NickName("iq240"));

console.log(user1 === user2); // false - インスタンスは異なる
console.log(user1.equals(user2)); // true - エンティティとしては等価
console.log(user1.equals(user3)); // false - ニックネームが違う

// changeNickName() をしたあとでも元のエンティティはそのまま
console.log(user1.nickName.value); // ueda

不変性をもったエンティティになっていて、変更すると変更が反映された新しいエンティティとして入手できます。

おわりに

値コピー・代入が多くなりがちな部分ですが、エンティティを工夫しつつシンプルに纏まったように思います。あとは changeNickName() でプロパティを強制書き換えしているので、よりスマートな方法に改良できそうです。

それではまた!

コラボスタイル Developers

Discussion