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のモデルが使われるからです。
「レポートの下書きのためのメソッド」「レビューをお願いするためのメソッド」「レビューをするためのメソッド」それら全てが、一つのReportクラスに作られるためFatになってしまうというわけです。
これはRailsのActiveRecordではなく、PofEAAのアクティブレコードパターンがCRUDすべてを担うパターンであるが故の宿命です。
どうやっても、ActiveRecordのモデルにメソッドを追加すれば単一責任の原則に違反します。
用途ごとに分ける
なので、ActiveRecordのモデルを行データゲートウェイ(PofEAA)として扱い、データマッパー(PofEAA)で用途ごとに用意したドメインモデル(PofEAA)と対応づけます。
簡単に説明すると行データゲートウェイ(PofEAA)はSELECT文の1行1行に対応するインスタンス、データマッパー(PofEAA)はRDBとオブジェクトモデルのミスマッチを解消する対応付け処理です。
具体的にどうするか
重要なことを先に書きます。ドメインモデル(PofEAA)にActiveRecordのモデルのインスタンスを持たせますが、あくまでデータホルダーとして使い、ごく一部を除いてActiveRecordのメソッドは使いません。RDB以外のデータソースになったとき、変更が大変になるためです。
まず、ドメインモデル(PofEAA)を作ろうと思いますが、その前にそのドメインモデル(PofEAA)がどの範囲で通用するのかを考えます。例えば、DraftReport
は研究調査に関する範囲でのみ通用するならResearchModule
を作ってその中に定義しましょう。
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のモデルにメソッドを追加する形で実現させます。
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っぽいことする話です。