※前提
PHP(Laravel)で実装しています。
デザインパターンを勉強するときに、こちらの記事をよく参考にするのですが、今回はRepositoryパターンについて学ぶ機会があったのでまとめます。
デザインパターンの書籍や記事は体感Javaで書かれていることが多いイメージですが、私は業務でJavaをほとんど書いたことがないので、今回は言語はPHPで動かしてみます。
Repositoryパターンってなにもの?
デザインパターンの1つでDDD(ドメイン駆動)について調べているとよくみる言葉です。
一言で言うと、
ビジネスロジックとデータ操作を分離する
設計手法だと思います。要はデータベースやデータストア等のインフラ層については忘れて、ドメインに集中したいよねっていうことです。これだけだと3層アーキテクチャじゃね?と思いますが、3層アーキテクチャをデザインパータン的に落とし込んだ型の1つとしてRepositoryパターンが使えるのかなという所感です。今回はLaravelで実際にやってみようと思います。
今回の例
今回はLaravelのデフォルトで作成されるusersテーブルを使って、ユーザー情報の取得、作成、更新、削除の一通りの機能をRepositoryパターンを用いて実装してみたいと思います。
ちなみに、routeは↓です。route/api(web).phpで↓になるようにルートを設定してください。
実装例
まず今回のディレクトリ構成は以下です。(Publish〇〇は無視してください)
1. Entityクラスを作成する
今回はApp/Domain/Entity
フォルダを作成して、ここにEntityクラスを作成します。App/Domain/Entity/User.php
を作成して、以下のように記載します。
<?php
namespace App\Domain\Entity;
class User implements \JsonSerializable
{
protected $id;
protected $name;
protected $email;
public function __construct(?int $id, string $name, string $email)
{
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
//APIの戻り値をjsonにするためのメソッド(ResourceクラスでもOK)
public function jsonSerialize()
{
return [
'id' => $this->getId(),
'name' => $this->getName(),
'email' => $this->getEmail()
];
}
}
エンティティクラスはビジネスロジックの中でデータを一貫させるために使用します。エンティティクラスを利用することで、データの一貫性を保ちつつ、ビジネスロジックをカプセル化することができます。
ここではこのEntityクラスをインスタンス化した時のid,name,emailを取得するgetId()
,getName()
,getEmail()
,と更新の際にセットするためのsetName()
,setEmail()
,を定義します。
2. RepositoryInterfaceを作成する
次にRepositoryInterfaceを作成します。InterfaceなのでRepositoryクラスで使用するメソッドと引数、戻り値の型を定義します。
<?php
namespace App\Domain\Domain;
use App\Domain\Entity\User as EntityUser;
interface UserRepositoryInterface
{
public function findById(int $id): ?EntityUser;
public function findByEmail(string $email): ?EntityUser;
public function store(EntityUser $user): int;
public function update(EntityUser $user): void;
public function delete(int $id): void;
}
今回はidからユーザー情報を取得するfindById()
、メールアドレスからユーザー情報を取得するfindByEmail()
、ユーザー登録処理のstore()
、更新のupdate()
、削除のdelete()
を定義します。Repositoryクラスはデータアクセスを実際に行うので、それを考えると必要なメソッドがなんとなくわかってくるかと思います。
3. Repository(具象)クラスを作成する
インターフェースクラスをもとに、Repository(具象)クラスを作成します。他のデータベース接続先を増やすとなった場合は、この具象クラスを追加し、のちに作成するサービスクラス(ビジネスロジックを含む)はインターフェースに依存する形になります。
<?php
namespace App\Repository;
use App\Domain\Domain\UserRepositoryInterface;
use App\Domain\Entity\User as EntityUser;
use App\Model\User;
class UserRepository implements UserRepositoryInterface
{
public function findById(int $id): ?EntityUser
{
$record = User::find($id);
if($record === null) {
return null;
}
return new EntityUser($record->id, $record->name, $record->email);
}
public function findByEmail(string $email): ?EntityUser
{
$record = User::where('email', $email)->first();
if($record === null) {
return null;
}
return new EntityUser($record->id, $record->name, $record->email);
}
public function store(EntityUser $user): int
{
$eloquentUser = User::create([
'name' => $user->getName(),
'email' => $user->getEmail(),
]);
return (int)$eloquentUser->id;
}
public function update(EntityUser $user): void
{
$record = User::find($user->getId());
if($record !== null) {
$record->name = $user->setName();
$record->email = $user->setEmail();
$record->save();
}
}
public function delete(int $id): void
{
User::destroy($id);
}
}
findById()
ではUser::find($id)
でidからユーザー情報を取得、
findByEmail()
ではUser::where('email', $email)->first();
でemailからユーザー情報を取得、
store()
ではUser::create()
で作成、
update()
ではUser::find()
とsave()
で更新、
delete()
ではUser::destroy();
で削除をしています。
実際のLaravelのDB接続処理(エロクアント)がここに記載されます。
4. サービスクラス(ビジネスロジック)を作成する
次にサービスクラスを作成します。ここにビジネスロジックが入ってきます。
<?php
namespace App\Services;
use App\Domain\Domain\UserRepositoryInterface;
use App\Domain\Entity\User as EntityUser;
use League\CommonMark\Extension\CommonMark\Parser\Inline\EntityParser;
class UserService
{
private $repository;
public function __construct(UserRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function register(string $name, string $email): int
{
$user = new EntityUser(null, $name, $email);
return $this->repository->store($user);
}
public function update(int $id, string $name, string $email): void
{
$user = $this->repository->findById($id);
if($user !== null) {
$user->setName($name);
$user->setEmail($email);
$this->repository->update($user);
}
}
public function delete(int $id): void
{
$this->repository->delete($id);
}
public function getUserById(): ?EntityUser
{
return $this->repository->findById($id);
}
}
ここでは今回のビジネスロジックである、
- 「ユーザー登録する」→
register()
- 「ユーザー情報を更新する」→
update()
- 「ユーザーを削除する」→
delete()
- 「ユーザーを取得する」→
getUserById()
という処理を実際に書いていきます。
コンストラクタ関数でUserRepositoryInterface
を依存注入しています。のちにサービスコンテナに登録するのですが、これで、このサービスクラスがRepository(具象)クラスではなく、インターフェースに依存することになり、依存性を逆転させることができます。
5. コントローラーを作成する
最初に記載した画像のルートに従ってメソッド名を定義します。
コントローラー側では先ほどのサービスクラスをコンストラクタ関数で依存注入してUserService
の各メソッドをそれぞれ呼び出してあげます。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\UserService;
use Symfony\Component\HttpFoundation\Response;
class UserController extends Controller
{
private $service;
public function __construct(UserService $service)
{
$this->service = $service;
}
public function register(Request $request)
{
$id = $this->service->register($request->name, $request->email);
return response('登録しました', Response::HTTP_CREATED)
->header('Location', '/api/user/' . $id);
}
public function update(Request $request)
{
$this->service->update($request->id, $request->name, $request->email);
return response('更新しました', Response::HTTP_OK);
}
public function delete(int $id)
{
$this->service->delete($id);
return response('削除しました', Response::HTTP_NO_CONTENT);
}
public function show(int $id)
{
$user = $this->service->getUserById($id);
if ($user === null) {
return response('ユーザーが見つかりません', Response::HTTP_NOT_FOUND);
}
return response()->json($user);
}
}
6. サービスプロバイダーにインタフェースとリポジトリクラスを登録する
作成したRepositoryInterfaceとRepositoryクラスをサービスコンテナに登録します。これで依存性が逆転し、サービスクラスはインターフェース に依存し、インターフェースを呼び出すだけでRepositoryクラスが呼ばれます。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->app->bind(
\App\Domain\Domain\UserRepositoryInterface::class,
\App\Repository\UserRepository::class,
);
}
}
実際に実行してみる
今回は試しに、GET api/user/1
を実行してみます。Seeder(またはFactory)についてはDatabaseSeeder.php
のコメントアウトを解除してマイグレーションしてください。
取得できました!
メリット
チームでソースの開発・保守がしやすい
-
データアクセスロジックを一元化することで、コードの理解と管理が容易になります。
-
すべてのデータアクセス操作が一貫性のあるインターフェースを通じて行われるため、チームメンバー全員がデータアクセスの方法を統一して理解することができます。
-
データアクセスはRepositoryクラスに、ビジネスロジックはサービスクラスにそれぞれ責任が分けられるため、チームメンバーが特定の部分に集中して作業しやすくなります。
-
一貫した設計により、コードレビュー時にデータアクセスロジックの不整合を見つけやすくなり、修正も迅速に行えます。
データの構築、データソース、ビジネスロジックに変更が発生する場合、ソースの変更が少なく済む
-
データアクセスロジックがRepositoryクラスにカプセル化されているため、データソースやビジネスロジックの変更が発生した場合でも、変更の影響範囲が限定されます。
-
新しいデータソース(例えば、RDSからDynamoDBへの移行)を導入する場合、対応する新しいRepositoryクラスを作成し、サービスクラスではインターフェースを通じて新しいRepositoryクラスを注入するだけで済みます。
-
また、ビジネスロジックが変更される場合でも、サービスクラス内でリポジトリを通じてデータアクセスを行っているため、データアクセスロジックに影響を与えずに変更を行えます。
ビジネスロジックとデータソースを分けて、テストすることができる
- ビジネスロジックとデータアクセスロジックが分離されているため、各コンポーネントを個別にテストすることが容易です。
- ビジネスロジックを含むサービスクラスは、リポジトリのモックやスタブを使用してテストすることができます。これにより、データベースに依存せずにビジネスロジックのテストが可能になります。
また、リポジトリクラス自体も独立してテストすることができます。これにより、データアクセスロジックが正しく機能しているかを検証できます。
アウトプットのデータの標準化
リポジトリクラスを通じてデータをアクセスすることで、取得するデータの形式や構造を一貫して標準化できます。
- データベースから取得したデータをアプリケーションの標準形式に変換するロジックをリポジトリ内で統一的に管理できます。
- 異なるデータソースから取得したデータでも、リポジトリ内で統一的に処理することで、アプリケーション全体で一貫したデータ形式を維持できます。
ちなみにメリットはこちらを参考に深堀してみました。
まとめ
今回はデザインパターンの1つであるRepositoryパターンをLaravelで試してみました。ビジネスロジックとデータアクセス層の分離という意味では、比較的どのプロジェクトでも考え方自体は導入されているものなのかなと思います。引き続きデザインパターン、クリーンアーキテクチャを深堀して、より良い設計ができるエンジニアを目指します!!
参考