この記事は 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でサービス数値の月末着地予測をする」です!