Help us understand the problem. What is going on with this article?

コンストラクタでオブジェクトの安全性を上げよ

オブジェクトの初期化

オブジェクト指向言語では、オブジェクトを初期化する際 コンストラクタ を呼び出します。

user.ts
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 というオブジェクトを完成させています。

安全性

しかし、これだけではオブジェクトの安全性に問題があります。

index.ts
// プロパティがパブリックなので、外部から完成されたオブジェクトを変化させてしまえます
user.id = 100;

// setter メソッドがあれば、 User オブジェクトのあずかり知らないアルゴリズムでプロパティが変化してしまいます
user.setNewId(100);

// 実は `initialize` メソッドを呼ばないと、プロパティが初期値であったり null であったりするかもしれません
user.initialize();

完全コンストラクタ Complete Constructor パターン

これらを解決し、オブジェクト利用時の安全性を確保する手法が「完全コンストラクタ(Complete Constructor)パターン」です。

単純なことですが、 「コンストラクタで全てのプロパティの値が確定し、そこから変化しないこと」 がこのパターンの実装となります。

このパターンを適用すると、オブジェクトは 不変(Immutable) となり、処理にべき等性(何度同じことをしても同じ結果が返ることを保証)が加わるので、安全性が高まります。

user.ts
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;

ビルダーパターンとの組み合わせ

依存する値やオブジェクトが多い場合は、ビルダーパターンを利用するのが良いでしょう。

user.ts
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 でもないオブジェクトに関しては、コンストラクタに依存を全て集約してしまうのが後で見たり修正する時もわかりやすく安全です。

また、インターフェースを用いる時もコンストラクタを定義することはほとんどないので、具象クラスで仕方なく必要になる依存関係をインターフェース側に要求する必要がなくなります。

entity-builder.ts
// インターフェースには Repository の情報は何も出てこない
interface IEntityBuilder<T> {
  build(id: number): Promise<T>
}

class UserBuilder implements IEntityBuilder<User> {
  // ...
}

class UserHouseBuilder implements IEntityBuilder<UserHouse> {
  // ...
}

オブジェクト同士の依存関係を一箇所でまとめて定義することで、関係性も一目瞭然になります。

設計する時に、この完全コンストラクタを思い出してくれればきっと安全性が高まるでしょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした