叫ぶうさぎの悪ふざけ

うさぎが目印のWebエンジニアが、得たことや思ったことを言の葉に乗せて叫ぶ場所です。

学習記録:12月12日(水):【輪読会まとめ 後編パート2・完結編】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン)

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の12日目のエントリーです。

そして昨日のエントリー 学習記録:12月11日(火):【輪読会まとめ 後編パート2】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン) の続きで、データベースの章の読了完結編です。

昨日はリポジトリパターンの実装で、

  • データ周りの要求変更に負けない設計パターンを取り入れる
  • ビジネスロジックからデータ操作を別レイヤへ

という内容を読み進め、まずは サービスクラスとデータベースアクセスが密結合 な実装をするところまでで終わっていました。

よって今日は リファクタリングで他のデータストアへの変更、モック差し替え のところまで進め、本章を読了したいと思います。

4. リファクタリング

ビジネスロジックを受け持つFavoriteServiceクラスを改めて確認してみるところから。

  • 「いいね」のデータ登録部分は EloquentであるFavoriteクラスに依存
    • つまりMySQLに接続できるEloquentが前提になっている

ビジネスロジックから特定のデータベース操作を取り除く

以下の順序で本質部分以外を取り除き、抽象化を進めていきます。

  1. Repositoryを抽象化するインターフェースを作成
  2. データベース操作を担当するRepositoryクラス作成
  3. Serviceクラスはインターフェースを参照
  4. インターフェースと具象クラスを紐付ける

「いいね」データのON・OFFが可能である、というのがビジネスロジック(Serviceクラス)の本質だったので、「ON・OFF切り替え操作を持つ、いいねデータのリポジトリ」にデータベースの処理を抽象化します。

で、データベース処理の抽象化を表現したインターフェースとして、次のコードを作成します。

リスト5.5.2.13:リポジトリインターフェース

というわけでインターフェースのコードです。このインターフェースの役割を表すということにもなるので、具体的な内容は書かず、メソッド定義のみになります。

具体的な内容は、それこそ具象クラスである、この後の具象クラスに実装します。

インターフェースのファイル名は app/DataProvider/FavoriteProviderInterface.php です。

<?php
declare(strict_types=1);

namespace App\DataProvider;

interface FavoriteRepositoryInterface
{
    public function switch(int $bookId, int $userId, string $createdAt): int;
}

余談

余談ですが、インターフェースはちょうど先日のぺちオブでも出てきて、実際のコードや具体例と共に解説を受けてきました。

そのあとでこの部分を実装してみると、わかりみが深いです。いずれ自分の言葉でSOLID原則をきっちり説明する試みを実行したいと思っています。

インプットはいただいたので、あとはアウトプットだ。

リスト5.5.2.14:リポジトリインターフェースを実装した具象クラス

  • app/DataProvider/FavoriteRepository.php を作成
  • コンストラクタインジェクション(依存性の注入)でデータベースアクセスを行うFavoriteクラスを注入
    • 注入自体はサービスクラスで実施
  • これでデータ操作の実処理はリポジトリクラスに移動
<?php
declare(strict_types=1);

namespace App\DataProvider;

use \App\DataProvider\Eloquent\Favorite;

class FavoriteRepository implements FavoriteRepositoryInterface
{
    private $favorite;

    public function __construct(Favorite $favorite)
    {
        $this->favorite = $favorite;
    }

    public function switch(int $bookId, int $userId, string $createdAt) : int
    {
        return \DB::transaction(
            // いいねがなければ作り、あれば削除、結果を0/1で返す
            function () use ($bookId ,$userId, $createdAt) {
                $count = $this->favorite->where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    $this->favorite->create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt
                    ]);
                    return 1;
                }
                $this->favorite->where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
    }
}

リスト5.5.2.15:サービスクラスのリファクタリング

インターフェースと、その中身を表す具象クラスを作成しました。

次は実際のサービスクラス(Controller/Actionから利用されるビジネスロジック)をリファクタリングします。

前回までは use App\DataProvider\Eloquent\Favorite; でEloquentを直接利用してました。これを抽象クラスであるインターフェースを用い、コンストラクタインジェクションで注入する形式に置き換えていきます。

僕自身の理解のため、既存のコードをコメントアウトし、なぜ変更したかを自分なりの言葉で書いて進めます。

<?php
declare(strict_types=1);

namespace App\Services;

// Eloquentの直接利用をやめ、インターフェースクラスを利用
//use App\DataProvider\Eloquent\Favorite;
use App\DataProvider\FavoriteRepositoryInterface;

class FavoriteService
{
    /**
     * @var FavoriteRepositoryInterface コンストラクタインジェクションで注入されるインターフェースを受け取る変数を用意
     */
    private $favorite;

    /**
     * Create a new service instance.(クラスのインスタンスが作成される際に、インターフェースの注入を受け取り保存するコンストラクタ)
     *
     * @return void
     */
    public function __construct(FavoriteRepositoryInterface $favorite)
    {
        $this->favorite = $favorite;
    }

    /**
     * Switch to Favorite Status
     *
     * @param  int $bookId
     * @param  int $userId
     * @param  string $createdAt
     *
     * @return int  turn on:1 / turn off: 0
     */
    public function switchFavorite(int $bookId, int $userId, string $createdAt): int
    {
        return $this->favorite->switch($bookId, $userId, $createdAt);
        // ここを丸ごとRepositoryクラスへ外出し、インターフェースを通して抽象化
        // サービスはインターフェースだけ考えればいい
        /*
        return \DB::transaction(
            function () use ($bookId, $userId, $createdAt) {
                $count = Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    Favorite::create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt
                    ]);
                    return 1;
                }
                Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
        */
    }
}

解説

これで、同じインターフェースを持つクラスであれば、なんでも動作する状態になりました。

何が嬉しいかというと、例えばテストで データベースにアクセスしない、モッククラスを作った として簡単に差し替えが可能です。

また、他のデータストアを使うことになっても、サービスクラスには関係なく、具象クラスを作って差し替えるだけで良い、という状態にもなりました。

コントローラもこのサービスクラスを使うので、データストアが変わっても影響を受けない状態となります。

リスト5.5.2.16:インターフェースと具象クラスのバインド

現段階では、インターフェースも具象クラス(Repository)も作っただけで、関連づいていません。

そこで、関連づけ(バインディング)を実行します。これでControllerからRepositoryまで一通り繋がり、動作することになります。

なお、書籍には 新たにサービスプロバイダを作成しても構いません とありますが、いったんは書籍の例で動作させてみます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // インターフェースクラスとリポジトリ(具象)クラスを関連づけ
        $this->app->bind(
            \App\DataProvider\FavoriteRepositoryInterface::class,
            \App\DataProvider\FavoriteRepository::class
        );
    }
}

curlコマンドで実行確認

実行してみると、実際に動きますね(当たり前)

写経してるのでtypoしたりreturn忘れて返り値の型が違うよと怒られたりしましたが、スルッと動くと気持ちいいですね!

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=1&user_id=2' --write-out '%{http_code}\n'
200

というわけでDBを確認してみます。

# 動かす前、レコードなし
mysql> select * from favorites;
Empty set (0.00 sec)

# 動作後、いいねレコード作成
mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       1 |       2 | 2018-12-12 14:28:09 | 2018-12-12 14:28:09 |
+---------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

# 再実行、いいね取り消しのためレコード削除
mysql> select * from favorites;
Empty set (0.00 sec)

別のbook_idで複数レコード作るのを確認

book_idが違うリクエストでAPIを実行します。

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=2&user_id=2' --write-out '%{http_code}\n'
200
$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=3&user_id=2' --write-out '%{http_code}\n'
200

2レコード作られていることがわかります。

mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       2 |       2 | 2018-12-12 14:29:20 | 2018-12-12 14:29:20 |
|       3 |       2 | 2018-12-12 14:29:36 | 2018-12-12 14:29:36 |
+---------+---------+---------------------+---------------------+
2 rows in set (0.01 sec)

章の終わり

いいねの操作などは、Queueを使用して非同期でもいいでしょうとあります。別の方法も採用できる実装例で進んできたことをここで知ることになるとは、うまい構成だなあとか思ったりしてました。

また、前述しましたが、データストア先を変更する場合は、同じインターフェース(FavoriteRepositoryInterface)を持ったデータ操作クラス(具象クラス)を作成して、バインドしなおせば、ビジネスロジック(サービス)を変更せずとも差し替えが可能になります。

リポジトリパターンは、今まで登場してきた各クラスを疎結合にできる反面、当然ながらクラス数が増えるため、デモや期間限定の機能では不要かもしれません。

しかしシステムの要件や規模の拡張が見込まれるサービスでは、検討に値するデザインパターンです、という言葉で章が締められています。

やってみて

で、ここからは意訳で僕の言葉ですが、

「今まで登場してきた各クラスを疎結合にできる反面、当然ながらクラス数が増えるため、デモや期間限定の機能では不要かもしれません」

という話は、システム全体からみたら逆にレアケースだと思います。要件が追加されないサービスなんか見たこともないし、拡張がされなかったシステムも見たことがないです。

リポジトリパターンが全てを解決するわけじゃ決してないと思いますが、漫然と今のままの実装を続けるのではなく、サービスの特徴を鑑みて適切なデザインパターンを検討し採用する ってことだよな、と思いながら読みました。

また、実際に僕が実装してみた印象でも、どんどん責務が分割されていき、綺麗に繋がっていくのを体感できました。 責務が分割され単一になるということは、より1つのことに集中してコードが書けるということだよな、と実感しました。

クラス数が増える=繁雑になる、では決してない と思います。 むしろ 適切に責務が分割され、1つずつ確実に実装を進められ、悩むことが少なくなる ことを実感しました。

さいごに

オブジェクト指向での実装を進めると、1つのクラスの責務、1つのメソッドの責務は単一責任の原則(だったかな)に則るのが普通だとも思います。

実際に業務で書いているコードに関しても、どんどん分割し、責務を分け、メソッド名を付ける際に2つ以上の責務を持たないか考え、実装を進めたりしています。

非常にスッキリしますし、互いのメソッドやクラスが疎結合になっていくので、のちのメンテも楽になるなあ、って話を上司としたこともあります。

データベースの章を通読し、コードを全部実行し、自分なりの言葉でアウトプットしてみたわけですが、ぺちオブでの勉強会のインプットも相まって、とてもすんなり内容が入ってきました。

今後もデザインパターンを意識しつつ、納期、リソース、要件、などなど、様々な内容から、最適な技術を採用していこうと思わせていただきました。

この章だけでも購入するに十分な理由になると思います。

Laravelやってみたいな、オブジェクト指向で実装するってなんだっけ?って思ってる人がもしいらっしゃいましたら、いろんな意味で読んでみることをオススメします。

別に回し者でもないですが、時間をかけ、自分の中の情報を関連づけて進めていくことで、割とバラバラだった知識が繋がりましたし、自分の中に浸透していなかったインプットが着実に刻まれていく感覚を味わうことができました。

かいつまんで読み進めているので全部読み切るのにまだ結構かかりそうですが、少しずつ進めて、確実にモノにしていこうと思います。

はー楽しかった!!!!