Eliminate Rails FatModel with value object

Introduction

Rails has a policy of "Skinny Controller, Fat Model".

Put the main logic of the application in the model The policy is to limit the role of the controller (or view) to something that can only be done at that layer.

By following this policy, the dependencies between each layer can be easily understood. This can be done by clarifying the scope of influence.

But to focus the business logic on the model If you don't create the model properly, the model will grow too large.

One of the countermeasures against the bloat of the model is the introduction of `` value object''.

What is a value object?

Value objects come from Domain Driven Design This is one of the patterns for expressing the domain model in code.

The domain is the problem domain targeted by the application. Analyzing the domain and extracting the construct is called modeling. And the concept obtained as a result of modeling is called domain model. The domain model has attributes and behaviors related to that concept.

As a representation of this domain model as an object There is a "entity" and the "value object" mentioned earlier.

Characteristics of entities and value objects

Each entity and value object has the following characteristics.

entity Value object
identity If the identifiers are the same, they are considered to be the same If all the attribute values ​​are the same, they are considered to be the same.
Variability Attributes can be changed after generation Attributes do not change after generation

** Entity example **

--Employee ――Even if there are two employees with the same name, they are different people ――Even if the attribute of age changes on your birthday, you will not be another person. --If the employee ID is the same, it can be judged as the same employee. --Employee ID is an identifier --Since it is judged by the identifier whether they are the same, it can be considered as an entity.

** Example of value object **

--Currency ――When comparing currencies only by monetary value, two 1000-yen bills are considered to be the same even if they are manufactured in different years. --If the attributes match, it is judged to be the same, so it can be considered as a value object.

By the way, the instance of the model using ActiveRecord is ** "id" is used as an identifier ** and is used to implement the entity.

Example of using value objects in Rails

There is a User model with an attribute called "email address" Suppose you want to add logic that returns only the domain name of the email address that the User has.

Implementation that Model tends to grow

** Implement logic as an instance method of User model **

python


class User < ApplicationRecord
  validates :email, format: { with: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ }
  
  def email_domain
    email.split("@").last 
  end
end

** Problems with this implementation ** --In the future, if the logic related to email addresses increases, the User model will become bloated. --When a model with an email address other than User appears, it is necessary to implement the same method in that model as well.

Next, to solve these problems, let's implement the logic using value objects.

Implementation using value objects

** Separate from User model with email address as value object **

Introduced a value object called Email Implement email address attributes and email address logic in that object.

python


class Email
  attr_reader :value
  delegate :hash, to: :value
  
  def initialize(value)
    raise "Email is invalid" unless value.match?(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/) # (*1)
    @value = value.frozen? ? value : value.dup.freeze # (*2)
  end
  
  def ==(other)
    self.class == other.class && value == other.value # (*3)
  end
  
  def domain
    email.split("@").last 
  end
end

# (*1) 
#The validation process could also be separated from the User model.
# (*2) 
#One of the characteristics that value objects must satisfy is invariance.
#Therefore, the attributes do not change after the object is created.
# (*3) 
#Equivalence is a feature that value objects must satisfy.
#Therefore, it is possible to compare with other Email objects.

python


class User < ApplicationRecord
  composed_of :email, mapping: %w[email value] # (*4)
end

# (*4)
# composed_of is a Rails model, a method to make value objects easier to work with.
#I won't explain it in detail here, so please check it out.

** Benefits of separating email addresses as value objects **

--The User model does not grow even if the logic of the email address increases --Clarified where to implement logic for email addresses --Email address logic can be reused in models other than User

at the end

When modeling with Rails, it tends to be forcibly implemented as an entity because ActiveRecord is used. If you try to express the domain model entirely with entities, there is a high possibility that it will become a FatModel, so it is important to properly examine whether it should be implemented as an entity or a value object !!

References

Perfect Ruby on Rails The idea of ​​Value Object Value object in 3 minutes DDD basic explanation: What are Entity and Value Object Handle value objects in Rails with composite_of Realistic regular expression for email address

tomorrow

Ateam Hikkoshi samurai Inc. × Ateam Connect Inc. Advent Calendar 2020 How was the article on the 19th day? Tomorrow is @ cheez921's article !! Please read it !!

Recommended Posts

Eliminate Rails FatModel with value object
Hashing object arrays with Rails (inject, index_by, etc.)
How to delete a new_record object built with Rails
Rails deploy with Docker
[Rails 6] RuntimeError with $ rails s
Handle devise with Rails
[Rails] Learning with Rails tutorial
Value object in 3 minutes
[Rails] Test with RSpec
[Rails] Development with MySQL
Supports multilingualization with Rails!
Double polymorphic with Rails
Mapping to a class with a value object in How to MyBatis
Check CSV value with RSpec
Introduced graph function with rails
[Rails] Express polymorphic with graphql-ruby
[Rails] Upload videos with Rails (ActiveStorage)
Try using view_component with rails
[Vue Rails] "Hello Vue!" Displayed with Vue + Rails
Japaneseize using i18n with Rails
API creation with Rails + GraphQL
Preparation for developing with Rails
Finally implemented Rails Form object
Run Rails whenever with docker
[Docker] Rails 5.2 environment construction with docker
Use multiple databases with Rails 6.0
[Rails] Specify format with link_to
Login function implementation with rails
[Docker] Use whenever with Docker + Rails
How to get boolean value with jQuery in rails simple form