オブジェクトの初期化
オブジェクト指向言語では、オブジェクトを初期化する際 コンストラクタ
を呼び出します。
class User {
public id: number;
public name: string;
public constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
// 利用時
const user = new User(1, 'yahho-');
コンストラクタでオブジェクト内のプロパティ(ここでいう id, name
)の値を初期化し、 User
というオブジェクトを完成させています。
安全性
しかし、これだけではオブジェクトの安全性に問題があります。
// プロパティがパブリックなので、外部から完成されたオブジェクトを変化させてしまえます
user.id = 100;
// setter メソッドがあれば、 User オブジェクトのあずかり知らないアルゴリズムでプロパティが変化してしまいます
user.setNewId(100);
// 実は `initialize` メソッドを呼ばないと、プロパティが初期値であったり null であったりするかもしれません
user.initialize();
完全コンストラクタ Complete Constructor
パターン
これらを解決し、オブジェクト利用時の安全性を確保する手法が「完全コンストラクタ(Complete Constructor
)パターン」です。
単純なことですが、 「コンストラクタで全てのプロパティの値が確定し、そこから変化しないこと」 がこのパターンの実装となります。
このパターンを適用すると、オブジェクトは 不変(Immutable) となり、処理にべき等性(何度同じことをしても同じ結果が返ることを保証)が加わるので、安全性が高まります。
class User {
public readonly id: number;
public readonly name: string;
public constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// readonly 属性がついているため、 setter メソッドは機能しません
public setNewId(newId: number) {
// this.id = newId;
}
}
// 利用時
const user = new User(2, 'yoohoo-');
// プロパティを直接更新することは出来ません
user.id = 200;
ビルダーパターンとの組み合わせ
依存する値やオブジェクトが多い場合は、ビルダーパターンを利用するのが良いでしょう。
class User {
// 若干 DDD チックな型定義
public readonly id: EntityId;
public readonly name: EntityName;
public readonly position: EntityPosition<User>;
public readonly level: UserLevel;
public readonly coin: Coin;
public readonly primaryWeapon: Weapon;
public readonly secondaryWeapon?: Weapon;
public constructor(params: {
id: EntityId,
name: EntityName,
position: EntityPosition<User>,
level: UserLevel,
coin: Coin,
primaryWeapon: Weapon,
secondaryWeapon?: Weapon,
}) {
this.id = params.id;
this.name = params.name;
this.position = params.position;
this.level = params.level;
this.coin = params.coin;
this.primaryWeapon = params.primaryWeapon;
this.secondaryWeapon = params.secondaryWeapon;
// TODO: この定義を 3 回書くのはなんとかしたい...
}
}
// ビルダー自体も完全コンストラクタ
class UserBuilder {
public readonly userRepo: UserRepository;
public readonly coinRepo: CoinRepository;
public readonly weaponRepo: WeaponRepository;
public constructor(userRepo: UserRepository, coinRepo: CoinRepository, weaponRepo: WeaponRepository) {
this.userRepo = userRepo;
this.coinRepo = coinRepo;
this.weaponRepo = weaponRepo;
}
public async build(id: number): Promise<User> {
const userRecord = await this.userRepo.find(id);
const coinRecord = await this.coinRepo.findByUserId(id);
const primaryWeaponRecord = await this.weaponRepo.findByUserIdPrimary(id);
const secondaryWeaponRecord = await this.weaponRepo.findByUserIdSecondary(id);
return new User({
id: new EntityId(id),
name: new EntityName(userRecord.name),
position: new EntityPosition<User>(userRecord.x, userRecord.y, userRecord.z),
level: new UserLevel(userRecord.level),
coin: new Coin(coinRecord),
primaryWeapon: new PrimaryWeapon(primaryWeaponRecord),
secondaryWeapon: new SecondaryWeapon(secondaryWeaponRecord),
});
}
}
インターフェースとの親和性
ビルダーやサービス・リポジトリのように、 DDD 的に Entity でも Value Object でもないオブジェクトに関しては、コンストラクタに依存を全て集約してしまうのが後で見たり修正する時もわかりやすく安全です。
また、インターフェースを用いる時もコンストラクタを定義することはほとんどないので、具象クラスで仕方なく必要になる依存関係をインターフェース側に要求する必要がなくなります。
// インターフェースには Repository の情報は何も出てこない
interface IEntityBuilder<T> {
build(id: number): Promise<T>
}
class UserBuilder implements IEntityBuilder<User> {
// ...
}
class UserHouseBuilder implements IEntityBuilder<UserHouse> {
// ...
}
オブジェクト同士の依存関係を一箇所でまとめて定義することで、関係性も一目瞭然になります。
設計する時に、この完全コンストラクタを思い出してくれればきっと安全性が高まるでしょう。