10
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【完走賞めざす!】データベースとSQLのススメAdvent Calendar 2023

Day 23

リポジトリパターンで、データの要求変更に強い設計パターンを実装

Last updated at Posted at 2023-12-23

はじめに

リポジトリパターンとは、ビジネスロジックからデータの保存や復元を別レイヤへ分離することで、コードのメンテナンス性やテストの容易性を高める実装パターンです。
本記事では、Laravelを使って、リポジトリパターンを実装します。

リポジトリパターンとは

アプリケーションにおいて、データの保存先はさまざまです。RDBやNoSQL、SaasのAPIを利用するケースもあります。また、テストコードで本番と違うデータベースを使用することもあります。
そのとき、データのストア先が変わってもプログラムの変更範囲をできる限り小さくしたいです。

その手段の一つとして、リポジトリパターンがあります。

スクリーンショット 2023-12-23 17.09.20.png

リポジトリパターンはビジネスロジックからデータストアに対して直接操作する処理を切り離します。インターフェースに対し、データ保存処理を具体的に扱うクラスを用意します。
ビジネスロジックからはデータストア先が何であるかを意識することなく、保存や検索の操作が用意になります。

リポジトリパターンのメリット

  • チームでソースの開発・保守がし易い
  • データの構築、データソース、ビジネスロジックに変更が発生する場合、修正箇所が少なくて済む
  • ビジネスロジックとデータソースを分けて、テストする事ができる

リポジトリパターンのデメリット

  • ファイル量、コード量が多くなりがち
  • 小規模の案件は導入しなくても良い

Laravelで実装してみる

サンプルとして出版社を新規に追加するAPIを実装します。
作成するコードは以下の6ファイルです。

  • モデルクラス(App\Models\Publisher):データベース(RDB)アクセスを受け持つ
  • エンティティクラス(App\Domain\Entity\Publisher):汎用的なエンティティクラス
  • リポジトリインターフェース(PublisherRepositoryInterface):インターフェース
  • 具象クラス(PublisherRepository):RDBとやり取りするための具象クラス
  • サービスクラス(PublisherService):ビジネスロジックを受け持つ
  • コントローラクラス(PublisherController):リクエストを受け持つ

ファイル数が多いですが、ひとつづつみていきましょう。

モデルクラスの作成

まず、出版社モデルクラスを作成します。$fillableでnameとaddressを登録可能とします。
これはEloquentつまりMySQLをデータの保存先とするためのモデルクラスです。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Publisher extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'address'
    ];
}

エンティティクラスの作成

エンティティクラスでは、どのデータ保存先でも対応できるよう、汎用的なクラスを作成します。

<?php

namespace App\Domain\Entity;

class Publisher
{
    protected $id;
    protected $name;
    protected $address;

    public function __construct(?int $id, string $name, string $address)
    {
        $this->id = $id;
        $this->name = $name;
        $this->address = $address;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getAddress(): string
    {
        return $this->address;
    }
}

リポジトリインターフェースの作成

インターフェースとは、クラスに含まれるメソッドの具体的な処理内容を記述せず、変数とメソッドの型のみを定義したものです。
出版社がすでに登録済みか確認するfindByNameと登録処理をおこうなうstoreを記述します。

<?php

namespace App\DataProvider;

use App\Domain\Entity\Publisher as EntityPublisher;

interface PublisherRepositoryInterface
{
    // 出版社名の名前から出版社を取得
    public function findByName(string $name): ?EntityPublisher;
    
    // 登録処理
    public function store(EntityPublisher $publisher): int;
}

リポジトリ具象クラスの作成

インターフェースクラスをもとに、Eloquentの使用を前提とした具象クラスを作成します。もし、他のデータベース接続先を増やす場合、この具象クラスを追加します。

<?php

namespace App\Repository;

use App\DataProvider\PublisherRepositoryInterface;
use App\Domain\Entity\Publisher as EntityPublisher;
use App\Models\Publisher;

class PublisherRepository implements PublisherRepositoryInterface
{
    public function findByName(string $name): ?EntityPublisher
    {
        $record = Publisher::whereName($name)->first();
        if($record === null) {
            return null;
        }

        return new EntityPublisher(
            $record->id,
            $record->name,
            $record->address
        );
    }
    
    public function store(EntityPublisher $publisher): int
    {
        $eloquentPublisher = Publisher::create(
            [
                'name' => $publisher->getName(),
                'address' => $publisher->getAddress()
            ]
        );
        $eloquentPublisher->save();
        return (int)$eloquentPublisher->id;
    }
}

サービスクラスの作成

ビジネスロジックを受け持つサービスクラスを作成します。

<?php

declare(struct_types=1);

namespace App\Services;

use App\DataProvider\PublisherRepositoryInterface;
use App\Domain\Entity\Publisher as EntityPublisher;

class PublisherService
{
    private $repository;

    // インタフェースを呼び出し
    public function __construct(PublisherRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    // 名前から存在チェック
    public function exist(string $name) :bool
    {
        if($this->repository->findByName($name)){
            return true;
        }
        return false;
    }

    // 登録処理
    public function store(string $name, string $address) :int 
    {
        return $this->repository->store(new EntityPublisher(null, $name, $address));
    }
}

コントローラクラスの作成

コントローラクラスではユーザーからのリクエストを処理します。
nameが出版社すでに登録済みか確認し、存在していればメッセージを返します。
出版社が未登録の場合、登録し、IDを返します。

コントローラークラスはサービスクラスしか見えません。

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\PublisherService;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class PublisherController
{
    private $service;
    // サービスクラスを呼び出す
    public function __construct(PublisherService $service)
    {
        $this->service = $service;
    }

    // 出版社登録
    public function create(Request $request)
    {
        $exit_publisher = $this->service->exist($request->name);
        // すでに存在していれば、メッセージを返す
        if($exit_publisher){
            return response('同じ出版社が存在しています', Response::HTTP_OK);
        }
        // 登録処理
        $id = $this->service->store($request->name, $request->address);
        return response('登録しました', Response::HTTP_CREATED)
                ->header('Location', '/api/publisher/' . $id);
    }
}

エンドポイントは以下になります。

api.php
Route::post('publishers', [App\Http\Controllers\PublisherController::class, 'create']);

また、サービスプロバイダーにインタフェースとリポジトリクラスを登録してください。

AppServiceProvider.php
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->bind(
            \App\DataProvider\PublisherRepositoryInterface::class,
            \App\Domain\Repository\PublisherRepository::class,
        );
    }

curlで実行確認してみる

curlコマンドで出版社を登録してみます。

curl 'http://localhost/api/publishers' \
 --request POST \
 --data 'name=テスト出版社&address=東京都千代田区1-2-2'

登録できました!
スクリーンショット 2023-12-23 18.21.19.png

おわりに

リポジトリパターンは変化に強い設計として、一考してみてはいかがでしょうか。

参考文献

10
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?