DEV Community

Kaziu
Kaziu

Posted on

エンティティ・値オブジェクト・サービス・UseCase をざっくり整理

まぁRailsというか、DDD(ドメイン駆動設計)やクリーンアーキテクチャの範囲ですけどな


一覧

用語 ひとことで 例
エンティティ ID で見分けるもの ユーザー、案件、注文
値オブジェクト 中身で見分けるもの 金額、電話番号、住所、期間
サービスオブジェクト 手続きを入れる箱 税額を計算する、S3 にアップロードする
UseCase ユーザーがやりたいこと1つ 案件を登録する、注文を確定する

エンティティ ─ ID で見分けるもの

人は、名字が変わっても引っ越しても同じ人です。逆に、同姓同名・同い年の人が2人いても別人です。

このように、中身ではなく ID(その人であること)で見分けるものがエンティティです。

user = User.find(1)
user.update!(name: '鈴木') # 名前が変わっても id: 1 のまま。同じユーザー
Enter fullscreen mode Exit fullscreen mode

Rails では、id を持つ ActiveRecord モデルの大半がエンティティです。


値オブジェクト ─ 中身で見分けるもの

財布の1000円札とあなたの1000円札は、交換しても誰も困りません。お札の番号は気にせず、中身(金額)が同じなら同じものとして扱います。これが値オブジェクトです。

PhoneNumber = Data.define(:value) do
  def initialize(value:)
    raise ArgumentError, '不正な電話番号' unless value.match?(/\A0\d{9,10}\z/)

    super
  end

  def mobile?
    value.match?(/\A0[7-9]0\d{8}\z/)
  end
end

PhoneNumber.new(value: '09012345678') == PhoneNumber.new(value: '09012345678') # => true
Enter fullscreen mode Exit fullscreen mode

特徴は次の3つです。

  • ID を持たない
  • 中身を変えない(変えたいときは新しく作る)
  • ルールを持てる(「0 から始まる10〜11桁」「携帯番号か判定する」など)

電話番号をただの String で持っていると、「携帯番号か判定する」処理が User にも Company にもコピペされていきます。値オブジェクトにすれば、電話番号のルールが1か所にまとまります。


サービスオブジェクト ─ 手続きを入れる箱

「税額を計算する」「S3 にファイルをアップロードする」のように、特定のモデルに入れるとモデルが太ってしまう手続きを入れておくクラスです。

class TaxCalculator
  def initialize(amount)
    @amount = amount
  end

  def call
    (@amount * 0.1).floor
  end
end
Enter fullscreen mode Exit fullscreen mode

よくある約束ごとは次のとおりです。

  • 1クラス1つの仕事
  • 公開メソッドは call などの1つだけ
  • 名前は「何をするか」がわかるものにする(UserService のようなあいまいな名前は避ける)

サービス = ビジネスロジック?

イコールではありません。業務のルールはエンティティや値オブジェクトにも入りますし、サービスには「S3 にアップロードする」のような技術的な処理も入ります。サービスは「ビジネスロジックの置き場所の1つ」くらいに考えておくのが正確です。


UseCase ─ ユーザーがやりたいこと1つ

「案件を登録する」「注文を確定する」のように、ユーザーがアプリでやりたいこと1つ分をクラスにしたものです。アプリの機能一覧をそのままクラスにしたイメージです。

UseCase は指揮役です。自分では細かい作業をせず、エンティティやサービスを呼び出して手順を組み立てます。

class ConfirmOrderUseCase
  def call(user:, order_id:)
    order = Order.find(order_id)
    raise '権限がありません' unless order.user_id == user.id     # 権限チェック

    ActiveRecord::Base.transaction do
      tax = TaxCalculator.new(order.subtotal).call                # サービスを使う
      order.update!(tax: tax, status: :confirmed)                 # エンティティを更新する
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

現場によっては、UseCase を Operation、Interactor、Command などの別の名前で呼ぶこともあります。名前は違っても「ユーザーの操作1つ分の手続き」という役割は同じです。


4つの関係

UseCase「注文を確定する」(指揮役)
   │
   ├─ エンティティを読み書きする   … 注文(Order)
   │      └─ 値オブジェクトを持つ  … 金額、電話番号
   │
   └─ サービスを呼ぶ              … 税額の計算、メール送信、S3
Enter fullscreen mode Exit fullscreen mode
  • UseCase が段取りを決める
  • その中で、エンティティを操作したり、サービスに作業を任せたりする
  • 値オブジェクトは、エンティティやサービスの中で「値」として使われる

呼び出す向きは上から下への一方通行です。サービスが UseCase を呼ぶことはありません。

Top comments (0)