UIが変わりやすい状況ならデータアクセスレイヤーの実装を後回しにすると楽だよというお話


こんにちは。
弁護士ドットコムでサーバーサイドエンジニアをやっている @ubonsa です。
この記事は 弁護士ドットコム Advent Calendar 2020 の14日目の記事です。

Webアプリケーションをどこから開発するか

Webアプリケーションを開発する時はどこから着手することが多いでしょうか。要件がある程度明確になったらユースケースを洗い出してモデリングしてDB設計に落とす。実装段階に入ってマイグレーションファイルを作成、Modelクラスを実装して、ControllerからViewにデータを渡してレンダリングする。

こんなふうに最初にDBスキーマを確定させてModelから作ることも割とあるんじゃないでしょうか。

DBスキーマは意外と変わりやすい

DBは変更しづらいレイヤーの割にはスキーマが変わることが多い印象です。UIから一番遠い場所にあるように見えてUIの要素に連動していることが多いです。まだ見ぬUI要素が追加され、それは永続化が必要となればDBスキーマにも反映させなければいけません。

例えばとあるECサイトに割引クーポン発行機能を追加するプロジェクトがあるとします。

最初は店舗オーナーが設定できる項目として「クーポン名」と「割引率」だけを想定していました。さらに購入者向けのクーポン利用上の注意事項はすべて共通の固定文章を表示する想定でいました。しかし「店舗オーナーが独自に注意事項を設定できるようにしたい」という要望が発生すると、早速DBスキーマに変更が入ります。

この調子でUIの詳細化が進んでUI要素の変更があると、リリースまでにいくつのマイグレーションファイルを作成してModelを修正しないといけないのでしょうか。

変わりやすい場所と変わりにくい場所

このようにUIに近い場所とDBに近い場所は変わりやすいと言えるかもしれません。View側の変わりやすい場所には、ViewファイルやViewに表示するデータを準備するControllerのロジックがあります。DB側の変わりやすい場所には、マイグレーションファイルやDBにアクセスするための処理があります。

逆に言えば空白の部分が変わりにくそうです。

変わりにくい場所にあるもの

この場所には「ビジネスロジック」と「アプリケーション依存の処理フロー」など抽象度が高いロジックがあります。ECサイトにクーポン発行機能を追加する例であれば下記のようなものです。

ビジネスロジック

  • ユニークなクーポンコードが発行される
  • 商品の価格とクーポンの割引率から割引金額を算出する

アプリケーション依存の処理フロー

  • ビジネスロジックを呼び出す
    • クーポンコードを生成する
  • 永続化するクーポンデータの生成手順
    • ユーザーが入力したクーポン名をセットする
    • ユーザーが入力した割引率をセットする
    • クーポンコードをセットする
  • 複数テーブルに跨る永続化のトランザクション管理
  • エラーハンドリング
  • 上記の手続きの流れを担保

Webアプリケーションをどこから開発するか

結論、このあたりの「ビジネスロジック」と「アプリケーション依存の処理フロー」から実装すると手戻りや修正が少なくなりそうです。

こんな真ん中らへんからどうやって実装するの?

クーポン発行機能を例に具体的なコードで見ていきましょう。実装のざっくりイメージは下図です。

「クーポン」と「割引」は別のモデルとして定義しています。「ビジネスロジック」はユニークなクーポンコード発行だけです。「アプリケーション依存の処理フロー」はビジネスロジックを使ってクーポンデータを用意するところから永続化するところまでの手続きを持っています。ビジネスロジック1つとアプリケーション依存の処理フロー1つを実装してみます。

ユニークなクーポンコードを発行するビジネスロジックを実装する

ざっくりこんなものを作ります。

  1. クーポンコードを生成する
  2. ユニークかデータアクセスレイヤーに問い合わせる
  3. ユニークでなければ1.に戻る

抽象化したデータアクセスレイヤーを定義する

  • クーポンコードをキーにクーポンTableにレコードが存在するか確認するメソッド
interface CouponRepositoryInterface
{
    public function existsByCode(string $code): bool;
}

ユニークなクーポンコードを発行するクラスを実装する

class CouponCodeGenerator
{
    private $repository;

    public function __construct(CouponRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function generate(): string
    {
        $trial = 3;
        for ($i = 0; $i < $trial; $i++) {
            $code = $this->getRandomStr();
            if ($this->repository->existsByCode($code) === false) {
                return $code;
            }
        }
        throw new Exception();
    }

    private function getRandomStr(): string
    {
        // ランダムな文字列を生成して返す
    }
}

これでユニークなクーポンコードを発行するビジネスロジックを実装できました。

クーポンを発行するアプリケーション依存の処理フローを実装する

ざっくりこんな処理を実装します。

  1. クーポンコード生成を呼び出す
  2. 永続化用のクーポンデータを整形する
  3. DBにトランザクションを張る
  4. 割引テーブルに永続化する
  5. クーポンテーブルに永続化する

抽象化したデータアクセスレイヤーを定義する

  • 割引データを永続化するメソッド
  • クーポンデータを永続化するメソッド
  • トランザクションを管理するメソッド
interface DiscountRepositoryInterface
{
    /**
     * @return int Discount ID
     */
    public function create(float $rate): int;
}
interface CouponRepositoryInterface
{
    public function existsByCode(string $code): bool;

    /**
     * @return int Coupon ID
     */
    public function create(string $name, string $code, int $discountId): int;
}
interface TransactionInterface
{
    public function begin(): void;
    public function commit(): void;
    public function rollback(): void;
}

アプリケーション依存の処理フローを実装する

class CreateDiscountCoupon
{
    private $transaction
    private $discountRepository;
    private $couponRepository;
    private $couponCodeGenerator;

    public function __construct(
        TransactionInterface $transaction,
        DiscountRepositoryInterface $discountRepository,
        CouponRepositoryInterface $couponRepository,
        CouponCodeGenerator $couponCodeGenerator
    ) {
        $this->transaction = $transaction;
        $this->discountRepository = $discountRepository;
        $this->couponRepository = $couponRepository;
        $this->couponCodeGenerator = $couponCodeGenerator;
    }

    public function __invoke(string $couponName, string $discountRate): int
    {
        $couponCode = $this->couponCodeGenerator->generate();

        $this->transaction->begin();
        try {
            $discountId = $this->discountRepository->create($discountRate);
            $couponId = $this->couponRepository->create($couponName, $couponCode, $discountId);
            $this->transaction->commit();
        catch (Exception $e) {
            $this->transaction->rollback();
            throw $e;
        }
        return $couponId;
    }
}

これでクーポンを発行するアプリケーション依存の処理フローを実装できました。

真ん中らへんの実装終わり

ここまでで「ビジネスロジック」と「アプリケーション依存の処理フロー」の実装が終わりました(ユニットテストはちゃんと書きましょう)。実装したCouponCodeGeneratorCreateDiscountCouponは依存しているRepositoryなどをモックに差し替えれば単体で動かすことができます。

「アプリケーション依存の処理フロー」の実装を見て気づいたかもしれませんが、CreateDiscountCouponは永続化する要素が増えると引数が増えたり$this->couponRepository->create()に渡す値が増えたりするので実は変わっていますね。大きな処理フローは変更されていないので許してください。

後回しにしたデータアクセスレイヤーを実装する

インターフェイスに則ってメソッドに具体的なロジックを実装するだけです。引数で受け取ったデータをそのまま永続化するだけなので、たいして実装する内容がありません。後半に回してもすぐ実装できる内容だと思います。

class DiscountDbRepository implements DiscountRepositoryInterface
{
    private $db;

    public function __construct(DB $db)
    {
        $this->db = $db;
    }

    public function create(float $rate): int
    {
        $this->db->insert('insert into discounts (rate) values (?)', [$rate]);
        return $this->db->getLastInsertId();
    }
}

まとめ

Webアプリケーション開発で最初にDBスキーマをガチガチに決めてModelクラスを実装してというような進め方もありますが、UIの変更が多いと意外とDBスキーマも変わりやすいものです。

それに比べて「ビジネスロジック」と「アプリケーション依存の処理フロー」は変わりにくいので、この部分から実装していき、DBのマイグレーションやデータアクセスレイヤーの実装を後回しにすることで、UI変更によるDB周りの変更を取り込む作業を減らすことができます。

また「ビジネスロジック」と「アプリケーション依存の処理フロー」をデータアクセスレイヤーやControllerから分離しておくことで、変更の影響を受けにくくなり、デグレが発生するリスクを下げることも期待できます。

要件やユースケースはある程度明確になっているけどUIの詳細化はこれから決めていくようなプロジェクトであれば、こんな進め方をすると楽をできるかもしれません。