Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

データモデルとドメインモデルを使い分けよう

この記事は hey / STORES advent calendar 2020 18日目の記事です。

いきなり本題からですが、筆者はよく船に乗って釣りをするので、遊漁船予約時の乗船料金算出ロジックを題材に、データモデルとドメインモデル、両モデルでの実装を提示しつつ、両者の使い分け(役割の違い)についてまとめてみました。

まずは、データモデルとドメインモデルについて簡単に用語の整理からしていきます。

データモデルとは

データベースの各テーブルに対応したモデルのこと

ドメインモデルとは

システムが扱うドメインの問題を解決するためのデータとロジックをひとまとめにしたモデルのこと

両方のパターンで実装してみる

まずはデータモデルとして実装してみる

前提として遊漁船の予約情報の管理のために以下のようなテーブル定義がされているとしましょう。

Reservation
id
user_id
course_id
number_of_people 予約人数
Course
id
name ex) 午前マダイ船
scheme 0→乗合い 1→仕立て
User
id
name
sex 0→男性 1→女性

これらをまずは愚直にデータモデルとして落とし込むと以下のような感じになるかと思います。

class Reservation {
  private readonly id: number;
  private readonly user: User;
  private readonly course: Course;
  private readonly number_of_people: number;

  // constructorは中略
}

ここに乗船料金の算出ロジックを実装していきましょう。

class Reservation {
  private readonly id: number;
  private readonly user: User;
  private readonly course: Course;
  private readonly number_of_people: number;

  // constructorは中略

  get boarding_fee(): number {
    if (this.course.scheme === 仕立て) {
      // 仕立ての料金算出ロジック
    } else if (this.course.scheme === 乗合い) {
      // 乗合いの料金算出ロジック
    }
  }
}

こんな感じに、乗船料金算出用のメソッド内で仕立て or 乗合いかによってロジックを分岐して算出するのがあるあるな実装になるのではないかと思います。

ある程度成熟してきたRailsシステムのモデル(ActiveRecord)には、↑のように一つのメソッドで複数のコンテキストを処理するような実装をする場面が頻発するので、それがロジックの見通しを悪くして変更を難しくしてたりします。

今度はドメインモデルとして実装してみる

ドメインモデルとして実装するため、まずはシステム化対象のドメインを抽出する必要があります。

遊漁船の予約方法には
- 仕立て(グループでの貸し切り)
- 乗合い(たまたま同じ日に予約した人と同船)
の2種類の予約方法があり、これを遊漁船の予約のドメインとして捉えてみたいと思います。

ドメインが捉えられたら、それぞれのドメインに持たせるロジックに対して必要な値をカプセル化したクラスを定義していきます。
今回は、乗船料金の算出に必要な値のみを定義しています。

仕立てのドメインモデル

仕立ての場合、乗船料金を算出するに当たって必要な情報は、基本料金とトータルの乗船人数なので、それらをカプセル化します。

class 仕立て {
  private readonly BASE_FEE = 120000;
  private readonly number_of_people: number;

  // constructorは中略
}

乗合い

乗合い船の場合、乗船料金の算出に必要な情報は乗船者の性別です。

class 乗合い船 {
  private readonly sex: number;

  // constructorは中略

}

それぞれに料金算出のロジックを実装していく

仕立て船の場合、乗船料金は基本料金 ÷ 人数で算出されます。

class 仕立て船 {
  private readonly BASE_FEE = 120000;
  private readonly number_of_people: number;

  // constructorは中略

  get boarding_fee(): number {
    return BASE_FEE / this.number_of_people
  }
}

続いて乗合い船の場合

乗合い船の場合、乗船料金は性別によって決定されます。
悲しいことに現実世界の乗合い船も同様です。

安くした分、女性が多めに乗ってくれると、魚の前におっさんが釣れるので、結果として遊漁船の運営元が儲かります。
ついつい話が脱線しました。

class 乗合い {
  private readonly sex: number;

  // constructorは中略

  get boarding_fee(): number {
    if (this.sex === ) return 12000;
    return 7000;
  }
}

仕立ては仕立ての料金算出、乗合いは乗合いの料金算出に特化したロジックが組めたことでロジックの見通しがよくなっているのではと思います。

この後、「仕立ての場合、人数に応じた割引きを追加で実装したい」みたいな用件が発生したとしても、どこに実装を追加したらいいか一目瞭然ですね!
かつ、自信を持ってこの追加実装に伴って乗合いの料金算出にデグレが発生することはありません! と断言できると思います。

また、副次的な効果として、boarding_feeメソッドを持つことでどちらも遊漁船として振舞えるので利用側でダックタイピングのように扱えます。

どう使い分けるか

タイトルでも使い分けと書いていますが、両者は明確に役割が異なるモデルです。
今まで区別されずに一箇所で実装されていた、永続化とビジネスロジックの責務を分離して実装していると言ったほうが正しいです。

まとめ

  • データモデルは、データの永続化に特化したモデルであり、それ自体にロジックは(基本的に)実装しない
  • システム固有の制約や演算(ビジネスロジック)とそれに関心のある値を一つのクラスとしてまとめる

明日はyougaiさんによる、「BigQueryMLでサービス数値の月末着地予測をする」です!

yahagin
逃した魚はでかかった...
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away