LoginSignup
4
1

More than 3 years have passed since last update.

Fat Modelをリファクタリングする

Posted at

ActiveRecordのモデルをリファクタリングする方法は「Fat ControllerをリファクタリングしてDDDっぽくする」でも言及しましたが、このエントリではさらに深掘りします。

エンタープライズアプリケーションアーキテクチャパターン(以下、PofEAA)を読んで理解している人向けに手短に表現すると以下の通り。

  • ActiveRecord(Rails)のモデルを行データゲートウェイ(PofEAA)として使用する
  • ActiveRecord(Rails)のモデルのインスタンスはドメインモデル(PofEAA)のデータホルダーとして使用する
  • データマッパー(PofEAA)によってActiveRecord(Rails)のモデルとドメインモデル(PofEAA)の対応付けを行う

PofEAAを読んでいない、読んだけど忘れたという人は続きをどうぞ。

まずはValueObjectなどを見つけて独立させる

ValueObjectやState、CalcRuleのような独立できるクラスがActiveRecordのモデルに融合してしまっているはずなので、まずはそれらの分離を目指します。
解説については別エントリを参照してください。

ActiveRecordのモデルがFat Modelになってしまう理由

それは様々な用途でActiveRecordのモデルが使われるからです。
ActiveRecordのモデルがFat Modelになってしまう原因.png
「レポートの下書きのためのメソッド」「レビューをお願いするためのメソッド」「レビューをするためのメソッド」それら全てが、一つのReportクラスに作られるためFatになってしまうというわけです。

これはRailsのActiveRecordではなく、PofEAAのアクティブレコードパターンがCRUDすべてを担うパターンであるが故の宿命です。
どうやっても、ActiveRecordのモデルにメソッドを追加すれば単一責任の原則に違反します。

用途ごとに分ける

なので、ActiveRecordのモデルを行データゲートウェイ(PofEAA)として扱い、データマッパー(PofEAA)で用途ごとに用意したドメインモデル(PofEAA)と対応づけます。
用途ごとにドメインモデルを分ける.png

簡単に説明すると行データゲートウェイ(PofEAA)はSELECT文の1行1行に対応するインスタンス、データマッパー(PofEAA)はRDBとオブジェクトモデルのミスマッチを解消する対応付け処理です。

具体的にどうするか

重要なことを先に書きます。ドメインモデル(PofEAA)にActiveRecordのモデルのインスタンスを持たせますが、あくまでデータホルダーとして使い、ごく一部を除いてActiveRecordのメソッドは使いません。RDB以外のデータソースになったとき、変更が大変になるためです。

まず、ドメインモデル(PofEAA)を作ろうと思いますが、その前にそのドメインモデル(PofEAA)がどの範囲で通用するのかを考えます。例えば、DraftReportは研究調査に関する範囲でのみ通用するならResearchModuleを作ってその中に定義しましょう。

app/domains/research_module/draft_report.rb
module ResearchModule
  class DraftReport
    # reportはActiveRecordのモデルであるReportのインスタンス
    attr_accessor :report
    # 必要なカラムはdelegateで参照できるようにする
    # もし、ドメインモデルの中でカラム値を変更する必要があるならtitle=メソッドもdelegateする
    delegate :title, to: :report
    def initialize(report)
      self.report = report
    end
  end
end

ResearchModuleには研究調査に関するドメインモデル(PofEAA)を入れるので、進捗報告に関するクラスとかも作られるかもしれません。

データマッパー(PofEAA)はどうするかというと、ActiveRecordのモデルにメソッドを追加する形で実現させます。

app/models/report.rb
class Report < ApplicationRecord
  belongs_to :reporter
  def to_research_module_draft
    ResearchModule::DraftReport.new(self)
  end

  def to_research_module_review_request
    # 必要なデータがあればコンストラクタに追加する
    ResearchModule::ReviewRequestReport.new(self, reporter.name)
  end

  def to_research_module_review
    # アソシエーション先にもデータマッパーを用意することでDDDのAggregateも実現できる
    ResearchModule::ReviewReport.new(self, reporter.to_research_module_review)
  end
end

冒頭で「ごく一部を除いてActiveRecordのメソッドは使いません」と書きました。そのごく一部とは、ドメインモデル(PofEAA)が更新系の処理のケースです。
バリデーションエラーの情報を追加したいときは、ActiveRecordのバリデーションの仕組みを利用した方が簡単になります。
他に、ドメインモデル(PofEAA)の中でアソシエーションが増減するケースもありますが、ここでは省略します。

どう使うか

ActiveRecordのモデルのインスタンスの検索、構築、保存はドメインモデル(PofEAA)の外で行います。

report = Report.new(report_params)
draft_report_entity = report.to_research_module_draft
if draft_report_entity.valid?
  # draft_report_entity.reportとreportは同じインスタンス
  draft_report_entity.report.save
end

参照系の処理なら、こんな感じになるでしょう。

review_report = Report.find(id).to_research_module_review
# レポートのインスタンスと自動採点の点数を返す
return { report: review_report.report, auto_score: review_report.auto_scoring }

ActiveRecordの検索については、scopeなどは定義するにせよ、基本的にベタに書いてよいと思います。ActiveRecordを使わなくなることはまずありませんし、RDB以外に切り替えることがあっても一部のテーブルだけでそこまで変更コストは変わらないでしょう。


このエントリは要求分析駆動設計データモデリングを再編しActiveRecordのモデルのリファクタリングという観点で書き換えたものです。
めっちゃ長いけど、RailsでDDDっぽいことする話です。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1