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

ActiveRecordのモデルをリファクタリングする方法は「[Fat ControllerをリファクタリングしてDDDっぽくする](https://qiita.com/linyclar/items/52ee50fd5d6452bc0370)」でも言及しましたが、このエントリではさらに深掘りします。

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

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

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

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

 * [状態遷移があるならStateクラスを作ろう](https://qiita.com/linyclar/items/659c70e99f6717ef81d4)
 * [複雑な計算があるならCalcRuleクラスを作ろう](https://qiita.com/linyclar/items/caba0d3a104c0c52c37a)
 * [メソッドがあると便利な値を探してValueObjectにしよう](https://qiita.com/linyclar/items/8de01d7f9646e7c1781a)

# ActiveRecordのモデルがFat Modelになってしまう理由
それは様々な用途でActiveRecordのモデルが使われるからです。
![ActiveRecordのモデルがFat Modelになってしまう原因.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/829617/ca0da451-4aeb-b2c8-cb32-8499df036ec1.png)
「レポートの下書きのためのメソッド」「レビューをお願いするためのメソッド」「レビューをするためのメソッド」それら全てが、一つのReportクラスに作られるためFatになってしまうというわけです。

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

# 用途ごとに分ける
なので、ActiveRecordのモデルを行データゲートウェイ（PofEAA）として扱い、データマッパー（PofEAA）で用途ごとに用意したドメインモデル（PofEAA）と対応づけます。
![用途ごとにドメインモデルを分ける.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/829617/3e4a9f86-7858-e240-e83a-2a494101c6ad.png)

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

 * [行データゲートウェイ](https://bliki-ja.github.io/pofeaa/RowDataGateway/)
 * [データマッパー](https://bliki-ja.github.io/pofeaa/DataMapper/)

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

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

```ruby: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のモデルにメソッドを追加する形で実現させます。

```ruby: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）の外で行います。

```ruby
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
```
参照系の処理なら、こんな感じになるでしょう。

```ruby
review_report = Report.find(id).to_research_module_review
# レポートのインスタンスと自動採点の点数を返す
return { report: review_report.report, auto_score: review_report.auto_scoring }
```
ActiveRecordの検索については、scopeなどは定義するにせよ、基本的にベタに書いてよいと思います。ActiveRecordを使わなくなることはまずありませんし、RDB以外に切り替えることがあっても一部のテーブルだけでそこまで変更コストは変わらないでしょう。
***
このエントリは[要求分析駆動設計](https://linyclar.github.io/software_development/requirements_analysis_driven_desgin/)の[データモデリング](https://linyclar.github.io/software_development/requirements_analysis_driven_desgin/#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%A2%E3%83%87%E3%83%AA%E3%83%B3%E3%82%B0)を再編しActiveRecordのモデルのリファクタリングという観点で書き換えたものです。
めっちゃ長いけど、RailsでDDDっぽいことする話です。