LaravelにおけるRepositoryパターンの問題点

LaravelにおけるRepositoryパターンの問題点

こんにちは!

今回はLaravelの開発現場でもよく使われるRepositoryパターンとその問題点について共有していきます。

Repositoryパターンを適用することで様々な恩恵を受けられる反面、Laravelにおいては一部問題点もあるため、安易にRepositoryパターンを適用しない方がいいよ!ということを主張する記事になります。

それでは、見ていきましょう!

Repositoryパターンとは

そもそもRepositoryパターンとは何かというと、DBなどの外部リソースを操作する責務を持つクラスを切り出しましょうというデザインパターンのことです。

これの何が良いかというと、

  • DB操作などの具体的な処理が抽象化され、隠蔽できる
  • 共通のロジックの使いまわしなど、処理の再利用性が高まる
  • テストコードを書きやすくなる

などの利点があります。

具体的なコード例を示すと、以下のようになります。

Repositoryの実装

class EloquentUserRepository implements UserRepositoryInterface {
    public function findById(int $id): ?User {
        // DBからデータを取得
        $record = EloquentUserModel::find($id);
        if (!$record) return null;

        // ドメインモデル(User)に変換して返す
        return new User($record->id, $record->name, $record->email);
    }

    public function save(User $user): void {
        EloquentUserModel::updateOrCreate(
            ['id' => $user->id],
            ['name' => $user->name, 'email' => $user->email]
        );
    }
}

Repositoryのインターフェース

interface UserRepositoryInterface {
    public function findById(int $id): ?User;
    public function save(User $user): void;
}

Service層での利用

class UserService {
    // インターフェースを型指定することで、実装が何であれ動作する
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}

    public function updateUserName(int $id, string $newName): void {
        $user = $this->repository->findById($id);
        
        if ($user) {
            $user->name = $newName;
            $this->repository->save($user);
        }
    }
}

これはなぜかあまり語られていないのですが、InterfaceによるDIP(依存性逆転の原則)が適用されていないRepository層は上述の利点が半減します。そのため、Repository層を切り出すならDIPもセットで行った方が効果は高いです。これについては深い内容になるため、また別の記事などで詳しく説明したいと思います。

LaravelにおけるRepositoryパターンの問題点

ここからが本題です。

RepositoryパターンはLaravelにおいては、その有用性が一部疑問視されています。なぜかというと、以下のような複数の問題を抱えているからです。

  1. 薄すぎるラッパー問題
  2. 抽象化の漏洩
  3. 重複するコードとメンテナンスコスト

それぞれ見ていきます。

薄すぎるラッパー問題

LaravelではModelというDB操作の抽象化層を標準で備えています。つまり、Repository層を作るということは、すでにラップされたものをさらにラップすることになります。そのため、Modelの薄いラッパーが量産されることになります。

イメージとしては、以下のようなとても薄いラッパーが作られがちです。

class UserRepository implements UserRepositoryInterface {
    public function find(int $id) {
        // Eloquentのメソッドを呼んでいるだけ
        return User::find($id);
    }

    public function all() {
        // これもEloquentを呼んでいるだけ
        return User::all();
    }
}

このRepositoryクラスでは、EloquentのメソッドをRepository層でそのまま横流ししているだけになっています。これだと抽象化の意味がないため、Eloquentを直接使った方がコード量が抑えられるためよいです。

抽象化の漏洩

以下のコードを見てください。

// Repositoryの実装
public function findActiveUsers() {
    // クエリを完了させず、ビルダを返してしまう
    return User::where('active', true); 
}

// 利用側(Controllerなど)
$users = $repository->findActiveUsers()
    ->where('age', '>', 20) // ここでEloquentの知識が必要になる
    ->orderBy('created_at')
    ->get();

この例では、Repository層でLaravelのBuilderオブジェクトを返してしまっています。ということは、呼び出し側はfindActiveUsersメソッドがBuilderオブジェクトを返すことを知っている必要があることになり、DB操作を完全に隠蔽できていません。

また、以下のコードは引数でリレーション名を指定しています。

// 呼び出し側でリレーションを指定したい場合
$user = $repository->find(1, ['posts', 'comments']);

interface UserRepositoryInterface {
    public function find(int $id, array $relations = []);
}

しかし、もしDBのリレーションが変わったらどうなるでしょう?

呼び出し側もそれに合わせてリレーション名の引数を修正することになります。ということは、呼び出し側はDBの構造を知っていることになります。これもDBレイヤーの隠蔽に失敗しています。

このように、Repository層で「DBレイヤーを隠蔽したい、でもLaravelのQuery BuilderやEager Loadingは便利だから使いたい」となると、結局DBレイヤーが持つべき知識が上位レイヤーに漏れ出し、Repositoryの本来の目的であるアプリケーション外部層の隠蔽という目的が完全に失われます。

重複するコードとメンテナンスコスト

Laravelには、Repositoryを介さずにコードを綺麗に保つ仕組み(Query Scopesなど)が標準で備わっています。これらを無視してRepositoryを導入すると、以下のような二重管理が発生します。

項目 Eloquent Repositoryパターン適用後
データ取得 User::active()->get() $repo->getActiveUsers()
作成/更新 $user->save() $repo->save($user)
テスト DatabaseTransactionsで高速 InterfaceをMock化する手間が必要

実はLaravelのコミュニティでは、Repositoryパターンを無理に適用するよりも、Serviceクラスでビジネスロジックをまとめ、複雑なクエリはEloquentのQuery Scopeを活用する方が、シンプルでLaravelらしい設計(Laravel Way)とされています。

安易にRepositoryパターンを適用しない!

Repositoryパターンの導入は、クリーンな設計を目指すエンジニアにとって魅力的な選択肢に見えます。しかし、Laravelにおいて「とりあえずリポジトリを作る」という判断は、多くの場合、メリットを享受できないまま開発スピードだけを落とす結果に繋がります。

導入を検討する際は、以下の3つの「代償」を払う覚悟があるかを自問自答すべきと考えます。

1. 「Laravelの強み」を捨てる覚悟があるか

Laravelの最大の魅力は、EloquentやCollection、API Resourceといった強力なエコシステムが「密に連携していること」です。Repositoryによってこれらを抽象化しようとすると、便利なメソッドの多くが使えなくなるか、あるいは「抽象化の漏洩」によって形骸化したラッパーが増えることになります。フレームワークのポテンシャルを殺してまで隠蔽すべき「詳細」がそこにあるのかを慎重に判断する必要があります。

2. インターフェースの二重管理コストに見合うか

メソッドを一つ追加するたびに「インターフェース」と「実装クラス」の両方を修正し、さらにテスト用のモックを更新する。このコストは、プロジェクトの規模が大きくなるほど開発の足枷となります。特に仕様変更が激しいスタートアップや新規事業において、この「薄いラッパー」の保守コストは無視できない負担になります。

3. 「DBの差し替え」は本当に起こるか

「将来的にDBをMySQLから他へ変えるかもしれない」という動機は、現実にはほとんど起こりません。万が一発生したとしても、Repositoryパターンを用意していたからといって、アプリケーション全体の整合性を保ったまま簡単に移行できるほど現実は甘くありません。起きるかどうかわからない未来の不確実性のために、現在進行形のコードを複雑にするのは、典型的なオーバーエンジニアリングです。

結論:まずは「Laravel Way」から始める

まずは、Laravelが標準で提供する機能を使い倒すことの検討から始めるのが賢明だと考えます。

  • Serviceクラス: ビジネスロジックの分離が必要なら、まずここに集約する。
  • Query Scope: 複雑なクエリに名前を付けて再利用したいなら、Eloquentのスコープ機能を使う。
  • API Resource: データの出力形式を変換したいなら、リポジトリで詰め替えるのではなくResource層で処理する。

これらを活用してもなお、​「複数のデータソースを横断して扱う必要がある」​、あるいは​「ドメインロジックが極めて複雑で、DB操作を完全に隔離しないとテストが書けない」​といった切実な課題に直面したときに初めて、Repositoryパターンの導入を検討すべきというのが筆者の考えです。

コメント (0件)

コメントを投稿

まだコメントはありません。最初のコメントを投稿しましょう!