[RUBY] Refactor the Fat Model

I mentioned how to refactor the ActiveRecord model in "Refactor Fat Controller to make it look like DDD", but this entry goes deeper. I will.

For those who have read and understood the Enterprise Application Architecture Pattern (PofEAA), the following is a brief description.

If you haven't read PofEAA, or if you have read it but forgot it, please continue.

First, find a Value Object etc. and make it independent

Independent classes like ValueObject, State, and CalcRule should have merged into ActiveRecord's model, so we'll start by separating them. Please refer to another entry for explanation.

Why ActiveRecord's model becomes Fat Model

That's because ActiveRecord models are used for a variety of purposes. ActiveRecordのモデルがFat Modelになってしまう原因.png "Method for drafting a report" "Method for requesting a review" "Method for reviewing" All of them are created in one Report class, so it becomes Fat.

This is fate because PofEAA's ActiveRecord pattern is responsible for all CRUD, not Rails' ActiveRecord. By all means, adding a method to ActiveRecord's model violates the single-responsibility principle.

Divide by use

Therefore, the ActiveRecord model is treated as a row data gateway (PofEAA) and associated with the domain model (PofEAA) prepared for each application by the data mapper (PofEAA). 用途ごとにドメインモデルを分ける.png

Briefly, the row data gateway (PofEAA) is an instance corresponding to each row of the SELECT statement, and the data mapper (PofEAA) is a mapping process that eliminates the mismatch between the RDB and the object model.

What to do specifically

I will write the important things first. The domain model (PofEAA) has an instance of the ActiveRecord model, but it is used only as a data holder, and the ActiveRecord method is not used except for a small part. This is because it becomes difficult to change when it becomes a data source other than RDB.

First, I would like to create a domain model (PofEAA), but before that, I will consider the range in which the domain model (PofEAA) is valid. For example, if DraftReport is valid only within the scope of research, create aResearchModule and define it in it.

app/domains/research_module/draft_report.rb


module ResearchModule
  class DraftReport
    #report is an instance of Report that is a model of ActiveRecord
    attr_accessor :report
    #Make necessary columns visible in delegate
    #If you need to change column values in your domain model, title=Delegate the method
    delegate :title, to: :report
    def initialize(report)
      self.report = report
    end
  end
end

Since the ResearchModule contains a domain model (PofEAA) for research and research, a class for progress reporting may also be created.

What to do with the data mapper (PofEAA) is to add a method to the ActiveRecord model.

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
    #Add any required data to the constructor
    ResearchModule::ReviewRequestReport.new(self, reporter.name)
  end

  def to_research_module_review
    #DDD Aggregate can also be realized by preparing a data mapper at the association destination.
    ResearchModule::ReviewReport.new(self, reporter.to_research_module_review)
  end
end

At the beginning, I wrote, "I don't use ActiveRecord methods except for a few." A small part of this is the case where the domain model (PofEAA) is an update process. If you want to add validation error information, it's easier to use ActiveRecord's validation mechanism. In addition, there are cases where associations increase or decrease in the domain model (PofEAA), but they are omitted here.

How to use

Find, build, and store instances of ActiveRecord models outside the domain model (PofEAA).

report = Report.new(report_params)
draft_report_entity = report.to_research_module_draft
if draft_report_entity.valid?
  # draft_report_entity.report and report are the same instance
  draft_report_entity.report.save
end

For reference processing, it will look like this.

review_report = Report.find(id).to_research_module_review
#Returns the report instance and auto-scoring scores
return { report: review_report.report, auto_score: review_report.auto_scoring }

Regarding ActiveRecord search, even if you define scope etc., you can basically write it solidly. It is unlikely that you will stop using ActiveRecord, and even if you switch to something other than RDB, the change cost will not change that much for some tables.


This entry is for Data Modeling (https://linyclar.github.io/software_development/requirements_analysis_driven_desgin/#%E3%83) in Requirements Analysis Driven Design (https://linyclar.github.io/software_development/requirements_analysis_driven_desgin/) % 87% E3% 83% BC% E3% 82% BF% E3% 83% A2% E3% 83% 87% E3% 83% AA% E3% 83% B3% E3% 82% B0) It was rewritten from the perspective of model refactoring. It's a long story, but it's a story about DDD-like things in Rails.

Recommended Posts

Refactor the Fat Model
About the new Java release model @ Seki Java (2018/07/20)
Get the column name from the Model instance
What is the model test code testing
Declare the model as an optional type