叫ぶうさぎの悪ふざけ

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

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

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

学習記録:12月9日(日):【輪読会まとめ 後編パート1】Laravel Webアプリケーション開発 Chapter5 データベース の後編の続きなのですが、まずもって「リポジトリパターン」をOOPの理解を含めて自分の言葉にするのと、リファクタリングを行う前のソースコードを生成していくので2時間ほどかかりました。

よって後編パートをさらに分割し、今日はリファクタリング前のコードを書くところまで実行します。

【抽象化って】読み進め、その前に【なんやねん】

抽象化って言葉を掘り下げたい。いったいなんやねんな。まじわからんし、ふわっとしすぎなんじゃー!って思ったそこのアナタ。そう(強引)、僕もそうでした。

なので僕なりに解釈した「抽象化」を表してみます。

  • 書籍販売のECがあるとする
  • プログラム、層、サービス、などそれぞれに責務がある
  • 例えばサービス(ドメイン?)の責務はユーザーにサービスを提供すること
    • そこに「どのデータストアを使うのか」という責務は含まれない
  • 書籍販売ならただ以下ができればいいはず(細かいことは割愛)
    • 「書籍一覧を取得」できて
    • 「購入ボタンが押せ」て
    • 「カートに商品が入っ」て
    • 「購入できる」
  • つまり、書籍販売のECの責務からしたら、書籍販売ができれば良い
    • 本来、MySQLなのかPostgreSQLなのかは意識させてはいけない
  • より本質的な責務に集中し、追加しやすく変更に強い作りにする必要がある
    • そのため、本質的なところ以外は「外出し」して、自分の責務に集中させる
  • 本質的以外のところは意識の外に出す
  • これが抽象化!

もちろん全然違うよ!こうだよ!って意見があると思うし、僕自身、理解の途上にいるので、異論反論などバンバン受け付けています!マサカリ大歓迎!

リポジトリパターン

  • データ周りの要求変更に負けない設計パターンを取り入れる
  • ビジネスロジックからデータ操作を別レイヤへ
    • リポジトリ層へ移し分離・隠蔽
    • メンテナンス性やテスト容易性を高める

5-5-1 リポジトリパターンの概要

  • アプリケーションのデータストアは様々
  • RDB、NoSQL、キャッシュ、ファイル、SaaSAPI、etc...
  • テストコードでは本番とは違うデータベース使うこともある
    • そもそもDB使わない場合もある
  • そんなデータストアの参照先が変わっても
    • プログラムの変更範囲は限定的にしたい!
    • それ、リポジトリパターンで解決できるかも!

ただ図をみるのもなんなので、書いてみました。僕の場合、図は自分で書かないと理解できないのです…

image.png (204.7 kB)

リポジトリパターン、僕なりの解説

①密結合

  • 状態
    • ビジネスロジックMVCならModel、DDDならサービスクラス)の中に
    • データアクセスクラス(SQL書いてるやつ)があって
    • RDBMSに読み書きしている
  • 問題点
    • データストアが変わった場合、ビジネスロジックの層に改修が入る
    • 影響範囲が大きい
    • 適切に切り離していないのでテストしにくい

ビジネスロジックからデータストアを切り離す

  • 状態
  • 問題点
    • データストアが変わればインジェクションされる型などは修正必要
    • 依然、ビジネスロジックに修正は必要な状態

③インターフェースを用いて疎結合な状態に

  • 状態
    • インターフェースクラスを用意、データストアそのものを抽象化
    • インターフェースクラスをインジェクションされるビジネスロジック
      • データストアがある、というインターフェースだけ意識すれば良い
      • ここの型などはインターフェースで抽象化し具象クラスを吸収
    • 具象クラスが変わっても、インターフェースクラスで吸収している
      • データストアが変わったり複数になってもビジネスロジックに影響はない
      • ここの理解はまだ甘い
  • 問題点
    • 最初に設計し、インターフェースを実装しておく必要がある
    • やはり後からの変更は大変
    • 設計大事

文中の解説

  • ビジネスロジックからデータアクセス処理を切り離した
    • 特定のデータストアに依存しなくなった
    • 「何らかのデータ格納庫(リポジトリ)に対して操作する」レイヤを用意
      • インターフェース
    • ビジネスロジックはデータストアが何か意識することなく
      • 保存、検索の操作が可能になる
  • 本節では実際にコードのリファクタリングを行っていく

5-5-2 リポジトリパターンの実装

  • 最初はサービスクラスとデータベースアクセスが密結合
  • リファクタリングで他のデータストアへの変更、モック差し替え

1.アプリケーション仕様

  • いいねをつける機能をWebAPI
  • URLは /api/action/favorite
  • テーブルはLaravel標準のusers, 「5-1」で作ったbooks, 新規作成のfavorite

表5.5.2.1:favoritesテーブル

カラム 備考
book_id integer FK
user_id integer FK
created_at timestamp
updated_at timestamp

2.テーブルの作成

リスト5.5.2.2:favoritesテーブル作成するマイグレーションファイル

$ php artisan make:migration create_favorites_table
Created Migration: 2018_12_11_140814_create_favorites_table
$ ls -la database/migrations/*favorites*.php
-rw-r--r-- 1 laradock laradock 598 Dec 11 14:08 database/migrations/2018_12_11_140814_create_favorites_table.php

リスト5.5.2.3:favoritesテーブルを作成するコードを記述

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateFavoritesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('favorites', function (Blueprint $table) {
            $table->integer('book_id');
            $table->integer('user_id');
            $table->timestamps();
            $table->unique(['book_id', 'user_id'], 'UNIQUE_IDX_FAVORITES');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('favorites');
    }
}

リスト5.5.2.4:マイグレーション実行 無事テーブル作成完了。ちなみにLaravelは複合主キーを扱えない(扱いにくい?)ため、 $table->unique(['book_id', 'user_id'], 'UNIQUE_IDX_FAVORITES'); とuniqueで代用しているそうです。

$ php artisan migrate
Migrating: 2018_12_11_140814_create_favorites_table
Migrated:  2018_12_11_140814_create_favorites_table

リスト5.5.2.5:テーブル定義

mysql> desc favorites;
+------------+-----------+------+-----+---------+-------+
| Field      | Type      | Null | Key | Default | Extra |
+------------+-----------+------+-----+---------+-------+
| book_id    | int(11)   | NO   | PRI | NULL    |       |
| user_id    | int(11)   | NO   | PRI | NULL    |       |
| created_at | timestamp | YES  |     | NULL    |       |
| updated_at | timestamp | YES  |     | NULL    |       |
+------------+-----------+------+-----+---------+-------+
4 rows in set (0.01 sec)

mysql> show create table favorites\G
*************************** 1. row ***************************
       Table: favorites
Create Table: CREATE TABLE `favorites` (
  `book_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  UNIQUE KEY `UNIQUE_IDX_FAVORITES` (`book_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)

3.コードの作成

以下3つのファイルを作る。

  1. データベースアクセスを受け持つEloquent(Favorite)
  2. ビジネスロジックを受け持つサービスクラス(FavoriteService)
  3. リクエストを受けるコントローラクラス(FavoriteAction)

Eloquentクラスの実装

以下のコマンドでファイル作成する。

リスト5.5.2.6:Favoriteモデルの生成

$ php artisan make:model DataProvider/Eloquent/Favorite
Model created successfully.

リスト5.5.2.7:Favoriteモデルの実装

<?php

namespace App\DataProvider\Eloquent;

use Illuminate\Database\Eloquent\Model;

class Favorite extends Model
{
    protected $fillable = [
        'book_id',
        'user_id',
        'created_at'
    ];
}

サービスクラスの実装

  • ここはartisanではなく手動でファイルを設置
  • 主な仕様
    • switchFavoriteメソッド内でEloquentを呼び出し
    • 「いいね」データ登録を行う
    • ただし、書籍コードとユーザーIDの組み合わせが既に存在する場合
      • レコードを削除して「いいね」を取り消す

リスト5.5.2.8:ビジネスロジックを受け持つFavoriteServiceクラスの実装 (「これはいかんな」ってのが自分にもわかるぞ、と思いながら書いてた。)

<?php
declare(strict_types=1);

namaspace App\Services;

use App\DataProvider\Eloquent\Favorite;

class FavoriteService
{
    public function switchFavorite(int $bookId, int $userId, string $createdAt) : int
    {
        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;
            }
        );
    }
}

Action(Controller)の実装

  • 主な仕様
    • ユーザーリクエストを受ける
    • FavoriteServiceクラスに処理を渡す
    • コンストラクタインジェクションでFavoriteServiceクラスのインスタンスを内部保持
    • 戻り値はHTTP_OK
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\FavoriteService;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Symfony\Component\HttpFoundation\Response;
class FavoriteAction extends Controller
{
    /**
     * @var \App\Services\FavoriteService
     */
    private $favorite;
    /**
     * Create a new Controller instance.
     *
     * @return void
     */
    public function __construct(FavoriteService $favorite)
    {
        $this->favorite = $favorite;
    }
    /**
     * Switch to Favorite Status
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return response
     */
    public function switchFavorite(Request $request)
    {
        $this->favorite->switchFavorite(
            (int)$request->get('book_id'),
            (int)$request->get('user_id', 1),
            Carbon::now()->toDateTimeString()
        );
        return response('', Response::HTTP_OK);
    }
}

routing設定

エンドポイント(外部から叩くURI)を登録する。

<?php

Route::post('/action/favorite', 'FavoriteAction@switchFavorite');

動作確認

エラー出てますな…。あとで直します。

修正しました。Eloquentでsyntax出てるやん…。お恥ずかしい。

ちゃんと200が返ってきました。

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

データ確認

ちゃんとデータ登録されていますね。いやーよかったよかった。

mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       1 |       2 | 2018-12-11 15:44:23 | 2018-12-11 15:44:23 |
+---------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

やってみて

分量が…

明らかに分量が増えすぎていて、今回でも終わりませんでしたな(遠い目

しかし、それだけ理解するためのスタートは切れているってことだし、これをアウトプットの形に結実させれば、リポジトリパターンと抽象化と疎結合に関しては、一度道を通ったことになるかなって思ってます。

抽象化

抽象化を本当に1分で言えと言われるとこうなるのかな、とか思いつつ、まだまだ自分の言葉にできてないよなって思ってます。そもそもこの章のコード動いてないし。もうちょいがんばろ。

抽象化を解説してみた

自分が本当に解説できるのか、というのと、エンジニアリングにまるで関係のない人に通じるのか、どう思うのか、を聞いてみました。思った通りというか、11月前半の僕と大して変わらない認識でちょっと安心。

これを奥さんに理解できるように解説できたらまずはよし、って感じかなあ。

  • プログラムってのがあってだな
  • 奥さん:(ぬいぐるみを頷かせる)
  • こいつはユーザーにサービスを提供するのが責務
  • 奥さん:(ぬいぐるみを頷かせる)
  • その責務にどのデータベースを使うのか、は関係ない
  • 奥さん:(ぬいぐるみを左右に傾かせて「はてー?」)
  • データベースはMySQLとかPostgreSQLとかOracleとかいっぱいある
  • 奥さん:(ぬいぐるみを頷かせる)
  • それがサービスの成長にしたがって他のデータベースも併用しなくちゃいけなくなった
  • 奥さん:(ぬいぐるみを頷かせる)
  • もしこの時点でプログラムが「MySQLしか使えませーん」となってると全部修正しなくちゃいけない
  • 奥さん:(ぬいぐるみをビクッと動かして驚いたアクション)
  • めっちゃ時間もお金もかかる
  • 奥さん:(ぬいぐるみを大きく動かして「大変だー」のアクション)
  • じゃあどうするか。じゃーん!「抽象化!」
  • 奥さん:(抽象化!と手を突き上げるアクション)

というわけで引き続き…

というわけで、リファクタリングは次回に続きます!楽しい!