りゅうじの学習blog

学習したことをアウトプットしていきます。

5/17 ドメイン駆動設計Chapter5 テストから

テスト用のリポジトリを作成する

データベースへのアクセスを行うことなく、実装したリポジトリが正常に動作するかのテストを行うために作成する。

class InMemoryUserRepository implements IUserRepository {
  private store: User[] = [];

  private clone(user: User): User {
    return new User(user.getId(), user.getName());
  }

  public find(userName: UserName): User | null {
    const target = this.store.find(user => userName === user.getName());
    if (target) {
      return this.clone(target);
    } else {
      return null;
    }
  }

  public save(user: User): void {
    const index = this.store.findIndex(storedUser => storedUser.getId().equals(user.getId()));
    if (index !== -1) {
      this.store[index] = this.clone(user);
    } else {
      this.store.push(this.clone(user));
    }
  }
}
  1. InMemoryUserRepositoryIUserRepositoryインターフェースを実装したクラスです。これにより、savefindという2つのメソッドを持つことが保証されます。
  2. private store: User[] = [];というコードは、Userオブジェクトの配列を格納するstoreという名前のプライベートなプロパティをクラスに追加しています。この配列は初期化時に空で、ユーザーのデータをメモリ内に保持します。
  3. saveメソッドは、引数として受け取ったUserオブジェクトをstoreに保存します。もし同じIDを持つユーザーがすでに存在する場合は、そのユーザーを新しいUserオブジェクトで置き換えます。
  4. findメソッドは、引数として受け取ったUserNameオブジェクトに一致する名前を持つユーザーをstoreから検索します。一致するユーザーが見つかった場合はそのユーザーのディープコピーを返し、見つからなければnullを返します。
  5. cloneメソッドは、引数として受け取ったUserオブジェクトのディープコピー(値が同じ新しいオブジェクト)を作成し返します。これにより、Userオブジェクトの状態がInMemoryUserRepositoryの外部で変更されても、リポジトリ内部のデータは影響を受けません。
  6. InMemoryUserRepositoryは「インメモリ」リポジトリで、つまり全てのデータはメモリ上に保存されます。そのため、プログラムが終了すると全てのデータが消えます。

リポジトリのコードのまとめ

// リポジトリのインターフェース
interface IUserRepository {
  save(user: User): void;
  find(name: UserName): User | null;
}

// リポジトリの実体(Prismaでの例)
class UserRepository implements IUserRepository {
  private prisma: PrismaClient;

  constructor(prisma: PrismaClient) {
    this.prisma = prisma;
  }

  async save(user: User): Promise<void> {
    const { id, name } = user;
    await this.prisma.user.create({
      data: {
        id: id,
        name: name,
      },
    });
  }

  async find(name: UserName): Promise<User | null> {
    const user = await this.prisma.user.findUnique({
      where: {
        name: name,
      },
    });
    return user ? new User(user.id, user.name) : null;
  }
}

// テスト用のリポジトリ
class InMemoryUserRepository implements IUserRepository {
  private store: User[] = [];

  private clone(user: User): User {
    return new User(user.getId(), user.getName());
  }

  public find(userName: UserName): User | null {
    const target = this.store.find(user => userName === user.getName());
    if (target) {
      return this.clone(target);
    } else {
      return null;
    }
  }

  public save(user: User): void {
    const index = this.store.findIndex(storedUser => storedUser.getId().equals(user.getId()));
    if (index !== -1) {
      this.store[index] = this.clone(user);
    } else {
      this.store.push(this.clone(user));
    }
  }
}

ディープコピーしないと、リポジトリ内部のインスタンスに影響してしまうので、テストのリポジトリではディープコピーを行いましょう。

ユーザ作成処理をテストする

後の章で出てくるアプリケーションサービスにcreateUserメソッドを実装しています、

Programクラス(アプリケーションサービス)

class Program {
  constructor(private userRepository: IUserRepository) {}

  async createUser(name: string): Promise<void> {
    // ユーザー名オブジェクトを作成
    const userName = new UserName(name);

    // ユーザーオブジェクトを作成
    const user = new User(userName);

    // ユーザーリポジトリを通じてユーザーを保存
    await this.userRepository.save(user);
  }
}
const userRepository = new InMemoryUserRepository();
const program = new Program(userRepository);

program.createUser('nrs');

const head = userRepository.getStore()[0];

assert.equal(head.getName().getValue(), 'nrs');

リポジトリに定義されるメソッド

永続化に関するメソッド

saveメソッド

saveメソッド(名前は他のものでも良いです)

永続化のメソッドは永続化を行うオブジェクトを引数にとります。

interface IUserRepository {
  save(user: User): void;
}

永続化のメソッドは永続化を行うオブジェクトを引数にとるので、対象の識別子と更新項目を引数にとって更新させるような書き方ではダメです。

ダメな例

interface IUserRepository {
  updateName(userid: UserId, name: UserName): void;
}
  • 対象のエンティティ全体を引数に取るべきです
    • リポジトリはエンティティの持つ状態を更新する役割は持たない

deleteメソッド

interface IUserRepository {
  delete(user: User): void;
}
  • ライフサイクルのあるオブジェクト(エンティティ)は不要になったら破棄されるので、それをサポートするのはリポジトリの役目です

再構築に関するメソッド

findメソッド

interface IUserRepository {
  find(id: UserId): User;
}
  • 基本的にはこのように識別子のよる検索メソッドを利用する

例外として、ユーザ名の重複確認のメソッドの場合、全件取得する必要があります。

interface IUserRepository {
  findAll(): User[];
}
  • この定義をするのであれば慎重になる必要がある
    • 再構築されるオブジェクトの数によっては、多すぎてPCのリソースを食い潰してしまうからです

対策

limit(上限)パラメータを追加して、一度に返せるエンティティの最大数を制限してしまう。

interface IUserRepository {
  findAll(limit: number): User[];
}

全件取得のような、全件の数がわからない(多すぎた場合に困る)メソッドには注意が必要で上限を制限させる対策を取ると良いでしょう。

イメージ図まとめ

- Programをインスタンス化するときの引数をテストリポジトリにインスタンスにすればDBを介さずに動作確認できます - アプリケーションサービスに関しては次章なのでここでは説明なしです

参考

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本