識別情報と属性情報を分離し、Entity<Attribute, Id>型で「識別子を持つ」という関心を表現する
主として同一性によって定義されるオブジェクトはエンティティと呼ばれる
Eric Evans. エリック・エヴァンスのドメイン駆動設計
DDD における「Entity」とは一意なものを表現する概念です。そのため ID といった識別情報をプロパティとして持っています。
interface User {
id: string; // 識別情報
name: string; // 属性情報 1
age: number; // 属性情報 2
}
上記は一般的な Entity を表す interface ですが、この定義だと Repository を扱う場合に不都合が生じます。
interface UserRepository {
create: (user: User) => void; // ここの引数 User に注目
findById: (userId: User["id"]) => User;
}
新規追加の create 関数に渡している User が今回の肝になります。ID の採番を Repository の利用側で作成している場合は別ですが、採番については DB に任せている設計もあります。
DB により ID の採番を行う場合、create 関数を呼ぶ際には ID が必要ありません。しかし型として id を持った User を定義している以上、id を設定しないといけないため、id を Optional にするなどといった対策が取られたりします。
interface User {
id?: string; // undefined でも OK
name: string;
age: number;
}
User は Entity であったはずなのに、識別情報が Optional というのは違和感がありますね。
Entity に識別子と属性を定義してしまった場合に上記のようなインピーダンスミスマッチが DB との間で発生します。
識別情報と属性情報を分離する
create 時に必要な情報は属性情報だけです。識別情報と属性情報の定義を分離し、併せた Entity の型を用意します。
// 識別情報
type UserId = string
// 属性情報
interface UserAttribute {
name: string;
age: string;
}
// この Entity<T, U> を実装する
type User = Entity<UserAttribute, UserId>
実装としてはシンプルで、U
には識別情報となる型を入力します。
type Entity<T extends Object, U extends Object = string> = T & {id: U}
Entity から属性情報を取得する型を作っておくと、Entity に対して属性情報を取得する方法が固定にできるので便利です。UserAttribute
を export する必要もなくなります。
type Attribute<T extends Entity<Object>> = Omit<T, 'id'>;
補足情報ですが、過去記事の以下を参考にすると ID<User>
型が作れます。
こうすることで、User
は id が required のままで、create 関数の引数を Attribute<User>
型にできました。
interface UserRepository {
create: (user: Attribute<User>) => void;
findById: (userId: ID<User>) => User;
}
function CreateUserUsecase (userRepository: UserRepository) {
return {
excute: (user: Attribute<User>) => userRepository.create(user)
};
};
function FindByIdUserUsecase (userRepository: UserRepository) {
return {
excute: (userId: ID<User>) => userRepository.findById(userId)
};
};
まとめ
記事中で紹介した記事や以下の記事と合わせて、モジュール間の可視性や ValueObject の設計をしています。なるべく Class を使わずに DDD などの考えを参考にした設計ができないかと試行錯誤しています。ある程度まとまったらあらためて記事を書こうと思っています
Discussion