Repositoryパターンのメモ
背景
前回Data Mapperパターンについて学んだ。
Data Mapperパターンのためにデータの取得方法を考えた際、「Repositoryパターンについて改めて学びたいな」と感じたのでメモを残す。
事前知識
この記事を読むにあたって知っておくと良いものを列挙しておきます。
- Active Recordパターン
- Data Mapperパターン
- Unit of Work(記事内で軽く補足します)
- ドメイン駆動設計でいう「集約」
Repositoryパターン
毎度のごとくMartin Fowler氏の記事です。
A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
ドメインやデータマッピングの仲介を行うもの。
Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer.
リポジトリはデータ永続などの操作をカプセル化する。永続化層をよりオブジェクト指向らしく提供できる。
個人的Repository パターンの気になるところ
責務の分離ができる
Data Mapperパターン同様で特定の処理を行うだけのクラスに分割できます。Repositoryパターンであればデータストアへの操作処理だけを持つクラスになれます。
後述しますが、設計を工夫すれば利用するデータストアを特定技術に縛らなくても良くなります。
選択肢の幅が増える(実装の柔軟性)
interfaceに依存させることでデータストア先を気にする必要はなくなります。
サンプルコードを書いてみます。Userというドメインオブジェクトを操作するRepositoryを作成します。データストアはRDB, NoSQL, InMemoryです。
<?php
namespace App\Domain\User;
// Interfaceを用意
interface UserRepositoryInterface
{
public function findById(UserId $userId): User
public function insert(User $user): void
public function update(User $user): void
public function delete(UserId $userId): void
}
Interfaceを使ってデータストア別にRepositoryを実装する。
<?php
namespace App\Repository\User;
use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\UserRepositoryInterface;
class RdbUserRepository implements UserRepositoryInterface
{
public function findById(UserId $userId): User
{
// RDBでの処理
}
public function insert(User $user): void
{
// RDBでの処理
}
public function update(User $user): void
{
// RDBでの処理
}
public function delete(UserId $userId): void
{
// RDBでの処理
}
}
<?php
namespace App\Repository\User;
use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\UserRepositoryInterface;
class NoSqlUserRepository implements UserRepositoryInterface
{
public function findById(UserId $userId): User
{
// NoSQLでの処理
}
public function insert(User $user): void
{
// NoSQLでの処理
}
public function update(User $user): void
{
// NoSQLでの処理
}
public function delete(UserId $userId): void
{
// NoSQLでの処理
}
}
<?php
namespace App\Repository\User;
use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\UserRepositoryInterface;
class InMemoryUserRepository implements UserRepositoryInterface
{
public function findById(UserId $userId): User
{
// InMemoryでの処理
}
public function insert(User $user): void
{
// InMemoryでの処理
}
public function update(User $user): void
{
// InMemoryでの処理
}
public function delete(UserId $userId): void
{
// InMemoryでの処理
}
}
こんなふうにRepository層を設ければデータストアが変わっても新しいクラスを作ることで対応できます。namespaceを工夫することでファイル構成をすっきりさせることも可能です。
Active Recordパターンでどのデータストアとやり取りをしているか伝えるためにメソッド名の工夫やコードコメントを書くことになります。そして時間が経てば経つほどModel層が肥大します。
別にUnit of Workパターンとセットではない
Unit of Workというのがあります。別でまとめるかも知れないですが今回は下記資料から抜粋します。
Martin Fowler によると、Unit of Work パターンでは「ビジネス トランザクションによって影響を受けるオブジェクトのリストを保持し、変更の書き込みと同時実行の問題の解決を調整します」。
簡単にいうと「Databaseなどへデータストアへの書き込みを保持してまとめて行おう」というところです。全部貯めて一括実行なので 全て更新される or 全て更新されない の二択になります。途中で失敗した時のロールバックする処理がActive Recordパターンでのトランザクション制御と異なります。
Repositryはデータの永続化などを一つの層に切り出そうという話です。
データの保存方法をUnit of Workで行おうというものではないのでRepositoryパターンを利用するからUnit of Workも使わないとというのは違います。Unit of Workを使った方が良いというのであれば併用すれば良いです。
もう少し踏み込むために
「Repositoryパターンってこういうことか!じゃあどんどん使おう!!」となることを否定しませんがもう少し深掘りたい。いろんなケースがあって考慮すべきことは多い。
Repositoryをどこに配置するのか
本当にRepositoryだけにロジックを集めて良いのか
ReadとWriteを一緒の場所に書くべきか、それともreadとwrite、それぞれの責務を持つもの別々に作成するべきか。
アンチパターン集
下記記事が非常に勉強になります。
Repositoryパターンを学び、とりあえずの気持ちで実践投入してしまわないよう目を通しておくと良さそうです。定期的に見返したい内容になってます。
調べてみて
別のことについても深掘る必要がありそう
Active Recordパターン、Data Mapperパターンの時同様で追加で調べたほうが良いことがたくさんある。
- Unit of Work
- 集約
- CQS
おわり
次こそ実装するぞ!Data MapperパターンとRepositoryパターンやっていくぞ!
参考文献
Data Mapperについてまとめた自分の過去メモ
Repository
インフラストラクチャの永続レイヤーの設計
Unit of Work
little-hands/ddd-q-and-a/issues/151
Repositoryパターンのアンチパターン
クリーアーキテクチャでのアンチパターン
それ... リポジトリじゃなくね?~実案件から学ぶアンチパターン~/Learn repository pattern from real projects
Discussion