DMMグループ Advent Calendar 2019 4日目の記事です。
この記事では、弊チームの一部プロダクトで採用したリポジトリパターンについて紹介します。
リポジトリパターンについて
リポジトリパターンとはビジネスロジックとデータ操作のロジックを分離し、データ操作を抽象化したレイヤに任せるデザインパターンのことです。
リポジトリパターンでは、DBの操作や外部APIによるデータ取得等のデータソースへのアクセス部分は Repository インターフェースから完全に隠蔽されます。
そのため、アプリケーションはデータソースがDBであっても外部API静的ファイルであっても、それを意識することなくデータ操作を行うことができます。
ディレクトリ構造
今回採用したディレクトリ構造の例です。
app
|--Console
|--Exceptions
|--Http
| |--Controllers
| |--Middleware
| |--Requests
|--Models
| |--Article.php
| |--ArticleImage.php
| |--User.php
|--Providers
| |--AppServiceProvider.php
|--Repositories
| |--Article
| | |--ArticleRepository.php
| | |--EloquentArticleRepository.php
| |--User
| | |--UserRepository.php
| | |--DummyUserRepository.php
| | |--UserAPIRepository.php
|--Services
| |--ArticleService.php
Model クラスはデフォルトの場合 app 直下に設置されますが、Model が増えた際に視認性が悪くなるため、今回は models/ 配下に配置しています。
以下で各層の役割を説明します。
Service
この構造では Repository と Controller の間に、ビジネスロジックを管理するための Service を追加しています。
複雑なビジネスロジックを Service に切り出し、 Repository はデータの操作のみを行うように責務を分離することで、 Repository が複雑になることを防ぎます。
また、今回の場合だと Controller は外部からと外部への値の受け渡しのみを担っています。
しかし、ビジネスロジックがそれほど複雑でない場合は、 Service 層を実装せずに Controller に実装する方法でも良いかもしれません。
Repository
Repository 層は1つのインターフェースと1つ以上の Repository の実態で構成されています。
|--Repositories
| |--User
| | |--UserRepository.php
| | |--DummyUserRepository.php
| | |--UserAPIRepository.php
リポジトリパターンを使用することで、「特定の環境時は外部のAPIへの参照をダミーデータに変更する」「使用するDBを変更する」といった場合に対応しやすくなります。
例として、「通常は外部のAPIからユーザデータを取得し、テスト時はダミーデータを取得したい」という場合を考えてみます。
UserRepository はインターフェイスです。
interface UserRepository
{
public function findUserByToken(string $token): User;
}
UserAPIRepository では外部のAPIから取得したデータを返しますが、 DummyUserRepository ではダミーデータを返すような作りにします。
class UserAPIRepository implements UserRepository
{
public function findUserByToken(string $token): User
{
try {
$user = $this->getUser($token); // 外部APIからユーザー情報を取得する処理(省略)
return $user;
} catch(Exceptions $e) {
throw new NoUserException();
}
}
}
class DummyUserRepository implements UserRepository
{
public function findUserByToken(string $token): User
{
$dummyUser = User(); // Eloquent モデルではないただのクラス
$dummyUser->id = 1;
$dummyUser->name = 'ほげほげ';
$dummyUser->token = 'abcd1234';
if ($dummyUser->token === $accessToken) {
return $dummyUser;
}
throw new NoUserException();
}
}
AppServiceProvider で環境ごとに注入する Repository を変更します。
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// テスト環境の場合はダミーデータ用の Repository に向ける
if (App::environment('testing')) {
$this->app->bind(UserRepository::class, DummyUserRepository::class);
} else {
$this->app->bind(UserRepository::class, UserAPIRepository::class);
}
}
}
これにより、テスト環境のときはデータの取得方法を変更することができます。
このようにインターフェースを介すことで、実際の操作方法を意識することなくデータの操作を行うことができます。
Repository と Model
Model と Repository は 1:1 とは限りません。
例えば、以下のような要件があったとします。
- 「記事( articles )」には必ず「記事の作者であるユーザ( users )」がおり、記事の削除・更新は作者のみ可能
- 「記事( articles )」には「画像( article_images )」が紐付いており、この画像は全くない場合もあれば複数ある場合もある
仮に、Article クラスと ArticleImage クラスに対してリポジトリ、 ArticleRepository と ArticleImageRepository を実装したとします。
「画像( article_images )」は「記事の作者であるユーザ( users )」を知らないため、画像を削除・追加しようとした場合に「記事( articles )」を経由して「作者( users )」を取得するようなロジックを ArticleImageRepository に記述する必要があります。
もちろんこのロジックは ArticleRepository にも必要なため、データの整合性を担保するためのロジックが複数の Repository に分散してしまうことになります。
これを防ぐために、関連するオブジェクト郡( Article と ArticleImage )を1つの塊として考えて、両方のオブジェクトを扱う ArticleRepository を実装します。
関連するオブジェクトを集約することで、ロジックの分散を防ぎつつデータの整合性を担保することができます。
|--Models
| |--Article.php
| |--ArticleImage.php
| |--User.php
|--Repositories
| |--Article
| | |--ArticleRepository.php
| | |--EloquentArticleRepository.php
おわりに
この記事では、弊プロダクトで採用したリポジトリパターンを紹介しました。
以下、まとめです。
- リポジトリパターンを使用することで、アプリケーションはデータの操作方法を意識することなくデータ操作を行うことができる
- Repository には複雑なビジネスロジックを記述せず、データの操作のみ行うよう責務を分離することで Repository の複雑化を防ぐ
- 関連するオブジェクト( Model )は1つの塊として考え、1つの Repository に集約する
この記事を通してどなたかのお役に立てれば幸いです!
明日の DMMグループ Advent Calendar 2019 の担当は、 @arika_nashika さんです!