🥺

DDDにおけるRepositoryパターン

2022/09/27に公開約3,200字

Repositoryパターンについて再学習した際の備忘録です。

Repositoryパターン

ドメインオブジェクトの集まり (以降、集約)を抽象化する設計手法。

DAO(DataAccessObject)とよく似ているが、DAOはデータアクセスの処理を抽象化する手法であり、Repositoryとは意識する点が真逆になっている。
また、DAOはクラスの分割方法については定義されていない点がRepositoryとは違う所となる。

データアクセスの抽象化という観点から、ORMもDAOの一種とも言える。
その特徴からRepositoryの内部でDAOを使用してデータを取得する事はあるが、逆にDAOの内部でRepositoryを使用してデータを取得する事は基本的には無い。

class MySQLPostRepository implements PostRepository {
  constructor(
    client: Client, // ORM的なやつ
  ) {}

  public findById(postId: PostId): Promise<Post> {
    return await client.post.where({ id: postId });
  }
}

設計手法

Repositoryパターンの設計手法は2種類存在する。

コレクション指向 (CollectionOriented)

コレクションのように振る舞うインターフェースを提供する。

add, remove, findのように追加、削除、取得に関するメソッドを持ち、updateのような更新系のメソッドは持たない。
Repositoryから集約を取得し、集約の要素を変更することでデータを更新するように実装する。
データの変更を検知して自動で保存するような仕組みが必要な為、何らかのライブラリに強依存する必要があり、あまり現実的な設計手法ではない。

// interface定義
interface PostRepository {
  add(post: Post): Promise<void>;
  remove(post: Post): Promise<void>;
  findById(postId: PostId): Promise<Post>:
}

// 使用側
const post = await postRepository.findById(postId);
post.moveFolder(folderId); // この時点で同時にデータが更新される

永続化指向 (PersistenceOriented)

永続化に重点を置いたインターフェースを提供する。

save, delete, findのように保存、削除、取得に関するメソッドを持ち、データ変更の度にsaveを呼び出して更新を保存する必要がある。
Collection指向と違いデータ変更の検知が必要無いため、実装の難易度は低い。

// interface定義
interface PostRepository {
  save(post: Post): Promise<Post>;
  delete(post: Post): Promise<void>;
  findById(postId: PostId): Promise<Post>:
}

// 使用側
const post = await postRepository.findById(postId);
post.moveFolder(folderId);

// データを書き換えた後にsaveメソッドに渡して保存する必要がある
const updatedPost = await postRepository.save(post);

ドメインオブジェクトとは

ドメインモデルを実際にコードへ落とし込んだ物。
ドメインオブジェクトの設計手法としてはEntityValueObject等が挙げられる。

Repositoryでは集約単位でドメインオブジェクトを入出力する。
また、集約として扱うドメインオブジェクトの事を集約ルートと呼ぶ。

ドメインとは

ソフトウェアとはある領域の問題を解決する為に開発する事が多い。
そのある領域の事をドメインと言う。

ドメインモデルとは

ドメインに存在する概念を抽象化した物。
ドメインモデルの表し方はユースケース図ドメインモデル図等、様々な手法がある。
また、ドメインモデルを表す過程をモデリングと呼ぶ。

バッドプラクティス

Repositoryパターンを実践するにあたり実際に遭遇したバッドプラクティスについて挙げていきます。

作成と更新でメソッドを分ける

interface PostRepository {
  insert(post: Post): Promise<Post>;
  update(post: Post): Promise<Post>;
} 

insert(作成), update(更新)とメソッドを分けるパターン。
メソッドを呼び分けるのが面倒になるので、save(作成/更新)メソッド1つで済むようにしておく方が良い。
また、将来的にupsertが必要になっってくる場面は必ず出てくるので、saveメソッドにしておく方が無難である。

更新用のメソッドを複数作る

interface PostRepository {
  updateTitle(postId: PostId, title: string): Promise<Post>;
  updateContent(post: Post, content: Content): Promise<Post>;
} 

そもそもRepositoryパターンをあまり理解していない人が実装しがちなパターン。
ユースケース毎に更新用のメソッドを作ってしまうとキリが無いので更新はsaveメソッドだけで済ませよう。

interface Repository<Aggregate> {
  save(aggregate: Aggregate): Promise<Aggregate>;
  delete(aggregate: Aggregate): Promise<void>;
}

上記のようにRepositoryの基底となるinterfaceを作成してルール化するのが良い。

メソッド名を抽象的にしてしまう

interface PostRepository {
  /** 人気な投稿を取得する */
  findByPopulars(): Promise<Array<Post>>;
  /** 自分の投稿を取得する */
  findOwnPosts(userId: UserId): Promise<Array<Post>>;
}

ドメインロジックがRepository側に流出してしまうので抽象的な名称を付けるのは避けるべき。
どのプロパティを元にどういった条件で取得するのかを具体的に表すようにしよう。

interface PostRepository {
  findInDescOfViewCount(): Promise<Array<Post>>;
  findByPostedUserId(userId: UserId): Promise<Array<Post>>;
}

Discussion

ログインするとコメントできます