🎊
TypeScript で値オブジェクトをエンティティに組み込む
前回「TypeScript での値オブジェクト実装」の続きです。
前回でイミュータブルな値オブジェクトを実現できるクラスを作りました。今回はドメイン層のエンティティのプロパティとして値オブジェクトを組み込んでいきます。
実装
この記事がとても参考になりました。
単純なデータ形式、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()
でプロパティを強制書き換えしているので、よりスマートな方法に改良できそうです。
それではまた!
Discussion